4o Ingenier´ Inform´tica
                                   ıa      a
                        II26 Procesadores de lenguaje
                        An´lisis sem´ntico
                          a         a

Esquema del tema
      1. Introducci´n
                   o
      2. Esquemas de traducci´n dirigidos por la sintaxis
                             o
      3. El ´rbol de sintaxis abstracta
            a
      4. Comprobaciones sem´nticas
                           a
      5. Interpretaci´n
                     o
      6. Introducci´n a metacomp
                   o
      7. Algunas aplicaciones
      8. Resumen del tema



1.    Introducci´n
                o
    Hay determinadas caracter´   ısticas de los lenguajes de programaci´n que no pueden ser mode-
                                                                         o
ladas mediante gram´ticas incontextuales y que es necesario comprobar en una fase posterior al
                       a
an´lisis sint´ctico. Por otro lado, las fases posteriores de la compilaci´n o interpretaci´n necesitan
   a         a                                                           o                o
una representaci´n de la entrada que les permita llevar a cabo sus funciones de manera adecuada.
                  o
    Estas dos vertientes —detecci´n de errores y representaci´n de la informaci´n— est´n muy
                                     o                             o                  o       a
relacionadas y se solapan en la pr´ctica. Supongamos, por ejemplo, que nuestro lenguaje permite
                                     a
asignaciones seg´n la regla
                  u

                                   Asignaci´n → id:= Expresi´n ;
                                           o                o

Es habitual que se impongan ciertas restricciones. En nuestro caso, estas podr´ ser:
                                                                              ıan

      El identificador de la parte izquierda debe estar declarado previamente.
      El tipo de la expresi´n debe ser compatible con el del identificador.
                           o

El analizador sem´ntico deber´ comprobar que estas dos restricciones se cumplen antes de decla-
                   a            a
rar que la sentencia de asignaci´n est´ bien formada. Pero sucede que la informaci´n necesaria
                                   o    a                                              o
para comprobarlas es util tambi´n para generar c´digo. Esto quiere decir que si tuvi´ramos una
                       ´           e                o                                   e
separaci´n estricta entre las fases del compilador, para generar c´digo deber´
         o                                                        o            ıamos volver a mirar
el identificador para saber a qu´ objeto (variable, funci´n, constante, etc.) corresponde y qu´ tipo
                                 e                       o                                    e
tiene y tambi´n deber´
              e        ıamos volver a comprobar los tipos de la expresi´n para generar el c´digo
                                                                         o                    o
adecuado.
    Por otro lado, aunque en teor´ el arbol de an´lisis ser´ suficiente para fases posteriores de la
                                    ıa ´           a       ıa
compilaci´n o interpretaci´n, es una representaci´n que contiene mucha informaci´n redundante.
           o               o                      o                                  o
As´ el ´rbol correspondiente a a:= b+c; bien podr´ tener el aspecto del ´rbol de la izquierda,
   ı,   a                                             ıa                      a
cuando el de la derecha contiene esencialmente la misma informaci´n y resulta m´s c´modo para
                                                                    o               a o
2                                                                               II26 Procesadores de lenguaje


    trabajar:
                          Asignaci´n
                                  o


                   ida :=       Expresi´n
                                       o      ;
                                                                           asignaci´n
                                                                                   o
                     Expresi´n
                            o       +   T´rmino
                                         e
                                                                   variablea            suma
                      T´rmino
                       e                    Factor
                                                                               variableb constantec

                       Factor                idc


                        idb
       El segundo ´rbol se conoce como ´rbol de sintaxis abstracta (o AST, de las iniciales en ingl´s).
                   a                     a                                                         e
    Como hemos comentado, durante el an´lisis sem´ntico se recoge una serie de informaciones que
                                            a         a
    resultan de utilidad para fases posteriores. Estas informaciones se pueden almacenar en el ´rbol,
                                                                                                 a
    “decor´ndolo”:
           a
                                                     asignaci´n
                                                             o


                                            variable             suma
                                        nombre: a          tipo: entero
                                        tipo: entero

                                                      variable      constante
                                                   nombre: b        nombre: c
                                                   tipo: entero     tipo: entero
                                                                    valor: 5

        As´ el objetivo de la fase de an´lisis sem´ntico ser´ doble: por un lado detectaremos errores que
          ı                              a        a         a
    no se han detectado en fases previas y por otro lado obtendremos el AST decorado de la entrada.
        Para ello utilizaremos esquemas de traducci´n dirigidos por la sintaxis, que permitir´n asociar
                                                      o                                        a
    acciones a las reglas de la gram´tica. Estas acciones realizar´n comprobaciones y construir´n el
                                       a                              a                             a
    AST que despu´s se recorrer´ para terminar las comprobaciones y ser´ la base para la interpreta-
                     e             a                                          a
    ci´n o la generaci´n de c´digo.
      o                o       o


    2.     Esquemas de traducci´n dirigidos por la sintaxis
                               o
        Nuestro objetivo es especificar una serie de acciones que se realizar´n durante el an´lisis de
                                                                            a               a
    la entrada y tener un mecanismo que nos permita obtener la informaci´n necesaria para realizar
                                                                           o
    estas acciones. Para esto a˜adimos a las gram´ticas dos elementos nuevos:
                               n                  a
           Acciones intercaladas en las reglas.
           Atributos asociados a los no terminales de la gram´tica.
                                                             a

    2.1.     Acciones intercaladas en las reglas
       Supongamos que tenemos el no terminal A que deriva dos partes diferenciadas y queremos
    que se escriba el mensaje “Cambio de parte” tras analizarse la primera. Podemos describir esto
An´lisis sem´ntico
  a         a                                                                                            3


de forma sencilla si aumentamos nuestra gram´tica incluyendo la acci´n en la parte derecha de la
                                            a                       o
regla de A :

                       A     →     Parte1 {escribe(“Cambio de parte”);} Parte2

   Podemos entender esta regla extendida como la receta: “analiza la primera parte, una vez
encontrada, escribe el mensaje y despu´s analiza la segunda parte”.
                                      e


2.2.    Atributos asociados a los no terminales
    Si nuestras acciones se limitaran a escribir mensajes que indicaran la fase del an´lisis en la que
                                                                                      a
nos encontramos, no necesitar´  ıamos mucho m´s. En general, querremos que las acciones respondan
                                               a
al contenido de los programas que escribimos. Para ello, vamos a a˜adir a los no terminales una serie
                                                                   n
de atributos. Estos no son m´s que una generalizaci´n de los atributos que tienen los terminales.
                               a                      o
Vamos a ver un ejemplo.
    Supongamos que en un lenguaje de programaci´n se exige que en las subrutinas aparezca un
                                                     o
identificador en la cabecera que debe coincidir con el que aparece al final. Tenemos el siguiente
fragmento de la gram´tica del lenguaje:
                       a

                                 Subrutina   → Cabecera Cuerpo Fin
                                 Cabecera    → subrutina id( Par´metros );
                                                                a
                                       Fin   → fin id

    En la regla correspondiente a Fin querremos comprobar que el identificador es el mismo que
en la cabecera. Vamos a definir un atributo del no terminal Fin que ser´ el que nos diga el nombre
                                                                      a
de la funci´n:
           o

               Fin     → fin id {si id.lexema = Fin .nombre entonces error fin si}

    El atributo nombre es lo que se conoce como un atributo heredado: su valor se calcular´ en las
                                                                                             a
reglas en las que Fin aparezca en la parte derecha y se podr´ utilizar en las reglas en las que Fin
                                                            a
aparezca en la izquierda; ser´ informaci´n que “heredar´” de su entorno. F´
                              a          o               a                    ıjate en c´mo hemos
                                                                                         o
empleado un atributo de id para hacer comprobaciones. El analizador sem´ntico es el que emplea
                                                                            a
la informaci´n contenida en los atributos de los componentes l´xicos. Recuerda que el sint´ctico
             o                                                 e                               a
s´lo ve las etiquetas de las categor´ En cuanto a error, suponemos que representa las acciones
 o                                  ıas.
necesarias para tratar el error.
    Ahora tenemos que completar las acciones de modo que Fin tenga alg´n valor para el nombre.
                                                                           u
Podemos a˜adir una acci´n a la regla de Subrutina para calcular el valor de Fin .nombre:
            n             o

           Subrutina       →     Cabecera Cuerpo { Fin .nombre:= Cabecera .nombre} Fin

    Lo que hacemos es copiar el atributo nombre que nos “devolver´” Cabecera . No debes confundir
                                                                 a
el atributo nombre de Fin con el de Cabecera , son completamente independientes. De hecho, el
atributo nombre de Cabecera es un atributo sintetizado: su valor se calcular´ en las reglas en las
                                                                             a
que Cabecera aparezca en la parte izquierda y se utilizar´ en las reglas donde Cabecera aparezca
                                                         a
en la parte derecha; es informaci´n que Cabecera “sintetiza” para su entorno. Debemos calcular
                                  o
el valor de nombre en la regla de Cabecera :

           Cabecera        → subrutina id( Par´metros ); { Cabecera .nombre:= id.lexema}
                                              a


2.3.    Otros elementos de las acciones
   Como habr´s comprobado, no hemos definido ning´n lenguaje formal para escribir las acciones.
            a                                     u
Asumiremos que se emplean construcciones corrientes de los algoritmos tales como condicionales

c Universitat Jaume I 2006-2007
4                                                                            II26 Procesadores de lenguaje


    o bucles. Tambi´n haremos referencia en ellos a subrutinas y a variables. Las variables las inter-
                   e
    pretaremos como variables locales a la regla que estamos analizando o como variables globales si
    lo indicamos expl´
                     ıcitamente. As´ en:
                                   ı,


           Valor   → opsum {si opsum.lexema=“+” entonces signo:= 1 si no signo:= -1 fin si}
                           Valor 1 { Valor .v:= signo* Valor 1 .v}

    no hay ninguna “interferencia” entre las distintas variables signo que se puedan utilizar en un mo-
    mento dado. F´ ıjate tambi´n en c´mo hemos distinguido mediante un sub´
                              e      o                                         ındice las dos apariciones
    del no terminal Valor .
        Un recurso muy util son los atributos que contienen listas. Por ejemplo, para una declaraci´n
                          ´                                                                            o
    de variables del tipo:

                                       DeclVariables    →     ListaIds : Tipo
                                             ListaIds   → id, ListaIds |id

    podemos hacer lo siguiente:

           DeclVariables     →    ListaIds : Tipo
                                  {para v en ListaIds .l hacer: declara_var(v, Tipo .t) fin para}
                ListaIds     → id, ListaIds 1 { ListaIds .l:= concat( [id.lexema], ListaIds 1 .l)}
                ListaIds     → id { ListaIds .l:= [id.lexema]}


    2.4.     Recopilaci´n
                       o
       Con lo que hemos visto, podemos decir que un esquema de traducci´n consta de:
                                                                       o

           Una gram´tica incontextual que le sirve de soporte.
                   a
           Un conjunto de atributos asociados a los s´
                                                     ımbolos terminales y no terminales.
           Un conjunto de acciones asociadas a las partes derechas de las reglas.

       Dividimos los atributos en dos grupos:

           Atributos heredados.
           Atributos sintetizados.

       Sea A → X1 . . . Xn una producci´n de nuestra gram´tica. Exigiremos que:
                                       o                 a

           Las acciones de la regla calculen todos los atributos sintetizados de A .
           Las acciones situadas a la izquierda de Xi calculen todos los atributos heredados de Xi .
           Ninguna acci´n haga referencia a los atributos sintetizados de los no terminales situados a
                        o
           su derecha en la producci´n.
                                    o

    Las dos ultimas reglas nos permitir´n integrar las acciones sem´nticas en los analizadores descen-
            ´                          a                            a
    dentes recursivos. Cuando se emplean, se dice que el esquema de traducci´n est´ basado en una
                                                                                 o     a
    gram´tica L-atribuida. La L indica que el an´lisis y evaluaci´n de los atributos se pueden hacer de
         a                                      a                o
    izquierda a derecha.
An´lisis sem´ntico
  a         a                                                                                             5



Ejercicio 1
   Las siguientes reglas representan parte de las sentencias estructuradas de un lenguaje de pro-
gramaci´n:
       o

               Programa    →      Sentencias
              Sentencias   →      Sentencia Sentencias | Sentencia
              Sentencia    → mientras Expresi´n hacer Sentencias finmientras
                                             o
              Sentencia    → si Expresi´n entonces Sentencias sino Sentencias finsi
                                       o
              Sentencia    → interrumpir
              Sentencia    → otros

   A˜ade las reglas necesarias para comprobar que la instrucci´n interrumpir aparece unicamente
     n                                                        o                      ´
dentro de un bucle.

Ejercicio 2
   Sea G la siguiente gram´tica:
                          a

                                       A   →     B C |a
                                       B   → A a B b| C a
                                       C   → a C C |aba C |a

    A˜ade a G las reglas sem´nticas necesarias para que el atributo ia de A contenga el n´mero
     n                         a                                                               u
de aes al inicio de la cadena generada. Por ejemplo, dadas las cadenas aaaaba, abaaaa y aaa, los
valores de ia ser´ 4, 1 y 3, respectivamente.
                 ıan
    Puedes utilizar los atributos adicionales que consideres necesarios, pero ninguna variable global.
Adem´s, los atributos que a˜adas deben ser de tipo entero o l´gico.
      a                       n                                  o



2.5.    Algunas cuestiones formales
      Una pregunta razonable es si puede el s´   ımbolo inicial tener atributos heredados y, si esto es
as´ de d´nde proceden. La respuesta var´ seg´n los textos. Los que se oponen, defienden que el
   ı,      o                                  ıa    u
significado del programa debe depender unicamente de ´l. Los que est´n a favor de los atributos
                                             ´               e               a
heredados, sostienen que permiten formalizar la entrada de informaci´n acerca de, por ejemplo, el
                                                                           o
entorno donde se ejecutar´ el programa o las opciones de compilaci´n.
                             a                                           o
      Una situaci´n similar se da con los s´
                 o                         ımbolos terminales: hay autores que piensan que no deber´ıan
tener atributos en absoluto; otros defienden que s´lo deben tener atributos sintetizados; y los hay
                                                      o
que opinan que pueden tener tanto atributos heredados como sintetizados.
      Otro aspecto sobre el que hay diferencias de interpretaci´n es el de los efectos laterales de las
                                                                  o
reglas. En algunos textos, las reglas de evaluaci´n de los atributos no pueden tener efectos laterales.
                                                   o
Otros autores defienden que esta distinci´n no es m´s que un problema de implementaci´n ya que
                                             o          a                                    o
es posible a˜adir un nuevo atributo que represente el entorno e ir actualiz´ndolo adecuadamente.
              n                                                                a
Esto unicamente supone una incomodidad, pero no un problema formal. Esta es la posici´n que
       ´                                                                                        o
seguiremos nosotros, permitiendo que haya efectos laterales en el c´lculo de atributos.
                                                                        a
      Quiz´ los ejemplos m´s claros de existencia de efectos laterales sean el manejo de la tabla de
          a                 a
s´
 ımbolos y el control de errores. Cuando se encuentra una declaraci´n en un programa es necesario
                                                                        o
actualizar la tabla de s´  ımbolos de modo que sea posible reconocer ocurrencias posteriores del
identificador. Podemos suponer que existe una tabla de s´       ımbolos global o tener un atributo que
lleve copias de la tabla de un sitio a otro.
      Por otro lado, en caso de que se encuentre un error, es necesario tomar medidas como detener
la generaci´n de c´digo. Una manera sencilla de marcar esta circunstancia es tener una variable
             o       o

c Universitat Jaume I 2006-2007
6                                                                      II26 Procesadores de lenguaje


    global de tipo l´gico que indique si se ha encontrado alg´n error. Nuevamente, ser´ posible, pero
                    o                                        u                        ıa
    no necesario, tener un atributo que transporte esa informaci´n.
                                                                 o
    2.6.   Eliminaci´n de recursividad por la izquierda y atributos
                    o
        En el tema de an´lisis sint´ctico vimos c´mo se pueden modificar las producciones con recursivi-
                         a         a             o
    dad por la izquierda para lograr que la gram´tica sea LL(1). El problema con las transformaciones
                                                   a
    es que dejan una gram´tica que tiene muy poca relaci´n con la original, lo que hace que escribir los
                           a                              o
    atributos y las reglas correspondientes sea dif´ sobre la gram´tica transformada. Sin embargo,
                                                     ıcil            a
    se pueden escribir los atributos sobre la gram´tica original y despu´s convertirlos de una manera
                                                     a                  e
    bastante mec´nica.
                  a
        Como las GPDRs no suelen necesitar recursividad por la izquierda, es f´cil que no tengas que
                                                                                a
    utilizar esta transformaci´n. Pese a todo, vamos a ver c´mo se hace la transformaci´n sobre un
                               o                              o                            o
    ejemplo. Partimos del siguiente esquema de traducci´n:o

                                  E   →    E 1 + T { E .v:= E 1 .v+ T .v}
                                 E    → T { E .v:= T .v}
                                 T    → num { T .v:= num.v}

       Comenzamos por transformar la gram´tica:
                                         a

                                              E   →     T E’
                                             E’   → + T E’
                                             E’   → λ
                                              T   → num

        Como vemos, el problema es que cuando vemos el sumando derecho en E’ , no tenemos acceso
    al izquierdo. La idea ser´ crear un atributo heredado que nos diga el valor del sumando izquierdo.
                             a
    En la primera regla lo podemos calcular directamente. En la segunda regla, realizamos la suma y
    la transmitimos:

                                 E    →    T { E’ .h:= T .v} E’
                                E’    → + T { E’ 1 .h:= E’ .h+ T .v} E’ 1

       Con esto, el atributo h contendr´ siempre la suma, que es el valor que tenemos que devolver
                                       a
    finalmente. Por eso hay que “transmitir” el nuevo valor al atributo v:

                        E   →     T { E’ .h:= T .v } E’ { E .v:= E’ .v}
                       E’   → + T { E’ 1 .h:= E’ .h+ T .v} E’ 1 { E’ .v:= E’ 1 .v}

       ¿Qu´ hacemos cuando E’ se reescribe como la cadena vac´ En este caso, basta con devolver
           e                                                    ıa?
    como sintetizado el valor que se hereda. El esquema de traducci´n completo queda:
                                                                   o

                        E   →     T { E’ .h:= T .v} E’ { E .v:= E’ .v}
                       E’   → + T { E’ 1 .h:= E’ .h+ T .v} E’ 1 { E’ .v:= E’ 1 .v}
                       E’   → λ { E’ .v:= E’ .h}
                       T    → num { T .v:= num.v}

    Ejercicio 3
       Escribe el ´rbol de an´lisis de 3+4 con el esquema original y el transformado. Dec´ralos.
                  a          a                                                           o
An´lisis sem´ntico
  a         a                                                                                          7



Ejercicio 4
   Transforma el siguiente esquema de traducci´n para eliminar la recursividad por la izquierda:
                                              o

                              E   →     E 1 + T { E .v:= E 1 .v+ T .v}
                              E   →     T { E .v:= T .v}
                              T   →     T 1 * F { T .v:= T 1 .v* F .v}
                              T   → F { T .v:= F .v}
                              F   → ( E ) { F .v:= E .v}
                              F   → num { F .v:= num.v}


   El siguiente ejercicio presenta la transformaci´n de una manera m´s general.
                                                  o                 a
Ejercicio* 5
   Si tenemos las siguientes reglas en un esquema de traducci´n
                                                             o

                             A    →    A 1 Y { A .a:= g( A 1 .a, Y .y)}
                             A    →    X { A .a:= f( X .x)}


podemos transformarlas en

                     A   →    X { A’ .h:= f( X .x)} A’ { A .a:= A’ .s}
                    A’   →    Y { A’ 1 .h:= g( A’ .h, Y .y)} A’ 1 { A’ .s:= A’ 1 .s}
                    A’   → λ { A’ .s:= A’ .h}

Comprueba que la transformaci´n es correcta analizando X Y 1 Y 2 mediante las dos versiones y
                             o
comparando el valor de a.


2.7.     Implementaci´n de los esquemas de traducci´n
                     o                             o
    La interpretaci´n de las acciones como sentencias que se ejecutan al pasar el an´lisis por ellas
                   o                                                                a
permite implementar los esquemas de traducci´n de manera sencilla. Para ello se modifica la
                                                o
implementaci´n del analizador recursivo descendente correspondiente a la gram´tica original de la
              o                                                                a
siguiente manera:
       Los atributos heredados del no terminal A se interpretan como par´metros de entrada de
                                                                        a
       la funci´n Analiza_A.
               o
       Los atributos sintetizados del no terminal A se interpretan como par´metros de salida de
                                                                           a
       la funci´n Analiza_A.
               o
       Las acciones sem´nticas, una vez traducidas al lenguaje de programaci´n correspondiente, se
                        a                                                    o
       insertan en la posici´n correspondiente seg´n su orden en la parte derecha donde aparecen.
                            o                     u
   En la pr´ctica, es frecuente que, si el lenguaje de programaci´n (como C) no permite devolver
           a                                                     o
m´s de un valor, los atributos sintetizados del no terminal se pasen por referencia.
 a
   En Python existe una soluci´n bastante c´moda. Comenzamos por definir una clase vac´
                                o              o                                          ıa:
       class Atributos:
         pass
    Antes de llamar a una funci´n o m´todo de an´lisis, creamos un objeto de esta clase y le
                                  o        e          a
a˜adimos los atributos heredados del no terminal. La correspondiente funci´n de an´lisis crear´
 n                                                                            o       a            a
los atributos sintetizados. Este es el unico par´metro que se pasa. As´ la traducci´n de la regla:
                                       ´        a                     ı,           o

c Universitat Jaume I 2006-2007
8                                                                         II26 Procesadores de lenguaje




                                 E    →    T { R .h:= T .v} R { E .v:= R .v}

    es la siguiente:

           def analiza_E(E):
             T= Atributos() # Atributos de T
             R= Atributos() # Atributos de R
             analiza_T(T)
             R.h= T.v # Creamos un atributo heredado de R
             analiza_R(R)
             E.v= R.v # Creamos un atributo sintetizado de E

    L´gicamente, tendr´
     o                ıamos que haber a˜adido el c´digo de control de errores.
                                       n          o


    2.8.     Atributos en GPDR
        La interpretaci´n que hemos hecho de los esquemas de traducci´n se traslada de forma natural
                       o                                             o
    a las GPDR. Por ejemplo, la traducci´n de:
                                         o

                        E    →       T 1 { E .v:= T 1 .v}(+ T 2 { E .v:= E .v+ T 2 .v})∗

    es simplemente:

           def analiza_E(E):
             T1= Atributos() # Atributos de T1
             T2= Atributos() # Atributos de T2
             analiza_T(T1)
             E.v= T1.v
             while token.cat=="suma":
               token= alex.siguiente()
               analiza_T(T2)
               E.v= E.v+T2.v

    Como antes, habr´ que a˜adir el correspondiente c´digo para controlar errores.
                    ıa     n                         o


    3.      El ´rbol de sintaxis abstracta
               a
        Como hemos comentado en la introducci´n, una de las posibles representaciones sem´nticas de
                                                 o                                            a
    la entrada es el ´rbol de sintaxis abstracta o AST.
                     a
        Aunque es similar a los ´rboles de an´lisis sint´ctico, tiene algunas diferencias importantes:
                                 a            a         a

           No aparecen todos los componentes l´xicos del programa. Por ejemplo:
                                              e

              • No es necesario incluir los par´ntesis de las expresiones.
                                               e
              • No se necesitan los separadores o terminadores de las sentencias.
              • ...

           Pueden aparecer otros componentes no estrictamente sint´cticos, como acciones de coerci´n
                                                                  a                               o
           de tipos.
An´lisis sem´ntico
  a         a                                                                                               9


3.1.         Construcci´n
                       o
    Para construir los ´rboles, debemos comenzar por definir qu´ elementos emplearemos para cada
                       a                                      e
estructura:

       Estructura               Representaci´n
                                            o                Estructura                Representaci´n
                                                                                                   o
                                         si                                             sentencias
       if E then LS end                                      begin S 1 . . . S n end
                                    E         LS                                       S 1 ...         Sn
                                   mientras                                              asignaci´n
                                                                                                 o
       while C do LS end                                     id:= E ;
                                    C         LS                                            id     E
                                    repetir                                                 suma
       repeat LS until C ;                                    E 1+ E 2
                                    LS        C                                           E1       E2
       ...                              ...                  ...                             ...

    F´
     ıjate que el ´rbol resultante puede representar programas en diversos lenguajes de programa-
                  a
ci´n del estilo C, Pascal, etc.
  o
    Ahora debemos utilizar los atributos para construir el ´rbol. Utilizando el atributo arb para
                                                            a
devolver el ´rbol que construye cada no terminal, podemos hacer algo parecido a:
            a
    Sentencia       → if Expresi´n then Sentencias end
                                o
                            { Sentencia .arb:= NodoSi( Expresi´n .arb, Sentencias .arb)}
                                                              o
    Sentencia       → while Expresi´n do Sentencias end
                                   o
                            { Sentencia .arb:= NodoMientras( Expresi´n .arb, Sentencias .arb)}
                                                                    o
    Sentencia       → repeat Sentencias until Expresi´n ;
                                                     o
                             { Sentencia .arb:= NodoRepetir( Sentencias .arb, Expresi´n .arb)}
                                                                                     o
    Sentencia       → id:= Expresi´n ;
                                  o
                           { Sentencia .arb:= NodoAsignaci´n(id.lexema, Expresi´n .arb)}
                                                          o                    o
Para la implementaci´n hay dos opciones principales:
                    o
        Utilizar funciones (NodoSi, NodoMientras, . . . ) que devuelvan una estructura de datos ade-
        cuada.
        Utilizar objetos (NodoSi, NodoMientras, . . . ) para cada uno de los nodos.
Probablemente, la segunda opci´n sea la m´s c´moda para el trabajo posterior con el ´rbol.
                                o          a o                                       a
   En cuanto a la lista de sentencias, podemos utilizar nodos que tengan un grado variable, para
eso almacenamos una lista con los hijos:
                     Sentencias    →          {l:=λ} ( Sentencia {l:=l+ Sentencia .arb})∗
                                                   { Sentencias .arb:= NodoSentencias(l)}


   El tratamiento de las expresiones es sencillo. Por ejemplo, para la suma podemos hacer:
                Expresi´n
                       o    →     T´rmino 1 {arb:= T´rmino 1 .arb}
                                   e                e
                                  ( + T´rmino 2 {arb:=NodoSuma(arb, T´rmino 2 .arb)})∗
                                       e                             e
                                  { Expresi´n .arb:= arb}
                                           o

c Universitat Jaume I 2006-2007
10                                                                           II26 Procesadores de lenguaje


         Las restas, productos, etc, se tratar´ de forma similar. Puedes comprobar que de esta manera
                                              ıan
     el AST resultante respeta la asociatividad por la izquierda.
     Ejercicio* 6

        En el caso de la asociatividad por la derecha tenemos dos opciones: crear una lista de operandos
     y recorrerla en orden inverso para construir el ´rbol o utilizar recursividad por la derecha, que
                                                       a
     no da problemas para el an´lisis LL(1). Utiliza ambas posibilidades para escribir sendos esque-
                                   a
     mas de traducci´n que construyan los AST para expresiones formadas por sumas y potencias de
                     o
     identificadores siguiendo las reglas habituales de prioridad y asociatividad.




     3.2.    Evaluaci´n de atributos sobre el AST
                     o

        Un aspecto interesante del AST es que se puede utilizar para evaluar los atributos sobre ´l, ene
     lugar de sobre la gram´tica inicial. Si reflexionas sobre ello, es l´gico. Todo el proceso de evaluaci´n
                           a                                            o                                 o
     de los atributos se puede ver como el etiquetado de un ´rbol (el ´rbol de an´lisis), pero no hay
                                                                 a          a            a
     nada que impida que el ´rbol sobre el que se realiza la evaluaci´n sea el AST.
                             a                                            o
         Un ejemplo ser´ el c´lculo de tipos. Si tenemos la expresi´n (2+3.5)*4, podemos calcular los
                        ıa    a                                    o
     tipos sobre el ´rbol de an´lisis:
                    a          a




                                                            Expresi´n
                                                                   o
                                                             t: real



                                                            T´rmino
                                                             e
                                                             t: real



                                              Factor              *        Factor
                                              t: real                     t: entero



                                (           Expresi´n
                                                   o                  )     num
                                              t: real                     v: 4
                                                                          t: entero

                                    T´rmino
                                     e          +      T´rmino
                                                        e
                                    t: entero           t: real



                                     Factor             Factor
                                    t: entero           t: real



                                      num                num
                                    v: 2                v: 3.5
                                    t: entero           t: real
An´lisis sem´ntico
  a         a                                                                                                       11


                                o       ıamos utilizar una gram´tica similar a la siguiente1 :
     Para realizar esta evaluaci´n, podr´                      a

 Expresi´n
        o       →    T´rmino 1 {t:= T´rmino 1 .t}(+ T´rmino 2 {t:= m´sgeneral(t, T´rmino 2 .t)})∗
                      e              e               e              a             e
                           { Expresi´n .t:= t}
                                    o
          ...
   T´rmino
    e           →    Factor 1 {t:= Factor 1 .t}(* Factor 2 {t:= m´sgeneral(t, Factor 2 .t)})∗
                                                                 a
                           { T´rmino .t:= t}
                              e
          ...
      Factor    → num { Factor .t:= num.t}
      Factor    → ( Expresi´n ) { Factor .t:= Expresi´n .t}
                           o                         o
          ...



     Tambi´n podemos realizar el c´lculo sobre el AST:
          e                       a

                                                         producto
                                                           t: real



                                                  suma               num
                                                 t: real         v: 4
                                                                 t: entero

                                            num         num
                                          v: 2      v: 3.5
                                          t: entero t: real

Esto se har´ junto con el resto de comprobaciones sem´nticas.
             ıa                                          a
     La elecci´n acerca de si evaluar atributos en el AST o durante el an´lisis descendente es b´si-
              o                                                           a                      a
camente una cuesti´n de simplicidad. Generalmente, podemos decir que los atributos sintetizados
                     o
tienen una dificultad similar en ambos casos. Sin embargo, cuando la gram´tica ha sufrido trans-
                                                                             a
formaciones o hay muchos atributos heredados, suele ser m´s f´cil evaluarlos sobre el AST.
                                                             a a
     Otro aspecto interesante a la hora de decidir qu´ atributos evaluar sobre el ´rbol de an´lisis y
                                                      e                           a          a
cu´les sobre el AST est´ en los propios nodos del ´rbol. Supongamos que queremos tener nodos
   a                     a                            a
diferentes para la suma entera y la suma real. En este caso, tendremos que comprobar los tipos
directamente sobre el ´rbol de an´lisis (o crear un AST y despu´s modificarlo, pero esto puede ser
                       a           a                             e
forzarlo demasiado).



4.      Comprobaciones sem´nticas
                          a
   Los atributos nos permitir´n llevar a cabo las comprobaciones sem´nticas que necesite el len-
                               a                                        a
guaje. En algunos casos, utilizaremos los atributos directamente (posiblemente evaluados sobre el
AST), por ejemplo en la comprobaci´n que hac´
                                     o           ıamos de que el identificador al final de la funci´n
                                                                                                 o
era el mismo que al principio.
   En otros casos, los atributos se utilizan indirectamente mediante estructuras globales, por
ejemplo la tabla de s´
                     ımbolos. Un ejemplo ser´ la comprobaci´n de que un identificador no se ha
                                              ıa               o
declarado dos veces. Si hemos utilizado un nodo similar a:
   1 Utilizamos la funci´n m´sgeneral que devuelve el tipo m´s general de los que se le pasan como par´metros (el
                        o   a                               a                                         a
tipo real se considera m´s general que el entero).
                         a


c Universitat Jaume I 2006-2007
12                                                                          II26 Procesadores de lenguaje


                                                  declaraci´n
                                                           o


                                                 Tipo    Listaids

     el m´todo de comprobaci´n ser´
         e                  o     ıa:
        Objeto NodoDeclaraci´n: o
            ...
            M´todo compsem´nticas()
               e                a
                 ...
                 para i ∈ listaids hacer
                     si TablaS´ımbolos.existe(i) entonces
                          error
                     si no
                          TablaS´ ımbolos.inserta(i, tipo)
                     fin si
                 fin para
                 ...
            fin compsem´nticas
                         a
            ...
        fin NodoDeclaraci´no
     Utilizando directamente el esquema de traducci´n habr´ que duplicar parte del c´digo:
                                                   o      ıa                        o

                Declaraci´n
                         o     → Tipo { Listaids .t:= Tipo .t } Listaids
                   Listaids    → id, { Listaids 1 .t:= Listaids .t} Listaids 1
                                          {si TablaS´
                                                    ımbolos.existe(id.lexema)
                                           entonces error
                                           si no TablaS´
                                                       ımbolos.inserta(id.lexema, Listaids .t)}
                    Listaids   → id
                                          {si TablaS´
                                                    ımbolos.existe(id.lexema)
                                           entonces error
                                           si no TablaS´
                                                       ımbolos.inserta(id.lexema, Listaids .t)}

     4.1.     La tabla de s´
                           ımbolos
         Durante la construcci´n del AST, las comprobaciones sem´nticas y, probablemente, durante la
                               o                                   a
     interpretaci´n y la generaci´n de c´digo necesitaremos obtener informaci´n asociada a los distintos
                 o               o      o                                    o
     identificadores presentes en el programa. La estructura de datos que permite almacenar y recuperar
     esta informaci´n es la tabla de s´
                   o                  ımbolos.
         En principio, la tabla debe ofrecer operaciones para:

            Insertar informaci´n relativa a un identificador.
                              o
            Recuperar la informaci´n a partir del identificador.
                                  o

        La estructura que puede realizar estas operaciones de manera eficiente es la tabla hash (que
     en Python est´ disponible mediante los diccionarios). La implementaci´n habitual es una tabla
                     a                                                              o
     que asocia cada identificador a informaci´n tal como su naturaleza (constante, variable, nombre de
                                                 o
     funci´n, etc.), su tipo (entero, real, booleano, etc.), su valor (en el caso de constantes), su direcci´n
          o                                                                                                 o
     (en el caso de variables y funciones), etc.
        Es importante tener unicamente una tabla; si tenemos varias, por ejemplo, una para constantes
                               ´
     y otra para variables, el acceso se hace m´s dif´ ya que para comprobar las propiedades de un
                                                   a    ıcil
     identificador hay que hacer varias consultas en lugar de una.
An´lisis sem´ntico
  a         a                                                                                          13


   Una cuesti´n importante es c´mo se relacionan la tabla y el AST. Tenemos distintas posibili-
             o                   o
dades, que explicaremos sobre el siguiente ´rbol, correspondiente a la sentencia a= a+c:
                                           a
                                             asignaci´n
                                                     o


                                      variable          suma
                                      nombre: a      tipo: entero



                                               variable      variable
                                              nombre: a     nombre: c

    La primera posibilidad es dejar el ´rbol tal cual y cada vez que necesitemos alguna informaci´n,
                                       a                                                         o
por ejemplo el valor de a, consultar la tabla. Esta opci´n ser´ la m´s adecuada para situaciones
                                                           o     ıa     a
en las que se vaya a recorrer el ´rbol pocas veces, por ejemplo en una calculadora donde se eval´e
                                 a                                                                u
cada expresi´n una sola vez.
             o
    Otra posibilidad es ir decorando cada una de las hojas con toda la informaci´n que se vaya
                                                                                     o
recopilando. Por ejemplo, si durante el an´lisis averiguamos el tipo y direcci´n de las variables,
                                             a                                   o
pasar´ıamos a almacenar la nueva informaci´n: o
                                                                        Tabla de s´
                                                                                  ımbolos

                     asignaci´n
                             o                                      a: {tipo:entero, dir:1000}
                                                                    ...

              variable            suma                              c: {tipo:entero, dir:1008}
            nombre: a         tipo: entero
            tipo: entero                                            ...
            dir: 1000
                      variable           variable
                     nombre: a        nombre: c
                     tipo: entero     tipo: entero
                     dir: 1000        dir: 1008

    Esto tiene costes muy elevados en tiempo —tendremos que actualizar repetidamente un n´mero
                                                                                         u
de nodos que puede ser elevado— y espacio —hay mucha informaci´n que se almacena repetida—.
                                                                 o
Podemos reducir estos costes guardando en cada nodo un puntero a la entrada correspondiente en
la tabla:
                                                                          Tabla de s´
                                                                                    ımbolos
                                                                    a: {tipo:entero, dir:1000}
                         asignaci´n
                                 o                                  ...

                                                                    c: {tipo:entero, dir:1008}
                 variable         suma
                                                                    ...
                              tipo: entero



                           variable variable



   Finalmente, la opci´n m´s adecuada cuando tenemos lenguajes que presenten ´mbitos anidados
                      o   a                                                    a
(veremos en otro tema c´mo representar los ´mbitos en la tabla) es guardar la informaci´n sobre
                        o                  a                                           o

c Universitat Jaume I 2006-2007
14                                                                              II26 Procesadores de lenguaje


     los objetos en otra estructura de datos a la que apunta tanto la tabla como los nodos del ´rbol:
                                                                                               a
                                                                   Informaci´n objetos
                                                                              o
                                                                          {tipo:entero, dir:1000}
                             asignaci´n
                                     o                                    ...

                                                                          {tipo:entero, dir:1008}
                       variable        suma
                                                                          ...
                                   tipo: entero

                                                                              Tabla de s´
                                                                                        ımbolos
                                variable variable
                                                                          a
                                                                          c
                                                                          ...



     4.2.     Comprobaciones de tipos
        Un aspecto importante del an´lisis sem´ntico es la comprobaci´n de los tipos de las expresiones.
                                    a         a                      o
     Esta comprobaci´n se hace con un doble objetivo:
                     o
            Detectar posibles errores.
            Averiguar el operador o funci´n correcto en casos de sobrecarga y polimorfismo.
                                         o
         Para representar los tipos en el compilador, se emplean lo que se denominan expresiones de
     tipo (ET). La definici´n de estas expresiones es generalmente recursiva. Un ejemplo ser´
                           o                                                               ıa:
            Los tipos b´sicos: real, entero,. . . , adem´s de los tipos especiales error de tipo y ausencia de
                       a                                a
            tipo (void) son ETs.
            Si n1 y n2 son enteros, rango(n1 , n2 ) es una ET.
            Si T es una ET, tambi´n lo es puntero(T ).
                                 e
            Si T1 y T2 son ETs, tambi´n lo es T1 → T2 .
                                     e
            Si T1 y T2 son ETs, tambi´n lo es vector(T1 , T2 ).
                                     e
            Si T1 y T2 son ETs, tambi´n lo es T1 × T2 .
                                     e
            Si T1 ,. . . ,Tk son ETs y N1 ,. . . ,Nk son nombres de campos, registro((N1 , T1 ), . . . , (Nk , Tk ))
            es una ET.
     Ten en cuenta que seg´n los lenguajes, estas definiciones cambian. Es m´s, los lenguajes pueden
                            u                                                   a
     restringir qu´ expresiones realmente resultan en tipos v´lidos para los programas (por ejemplo, en
                  e                                          a
     Pascal no se puede definir un vector de funciones).
         Algunos ejemplos de declaraciones y sus expresiones correspondientes ser´ ıan:

                               Declaraci´n
                                        o                 Expresi´n de tipo
                                                                 o
                               int v[10];                 vector(rango(0, 9),entero)
                               struct {                   registro((a,entero), (b,real))
                                int a;
                                float b;
                               } st;
                               int *p;                    puntero(entero)
                               int f(int, char);          entero × car´cter → entero
                                                                      a
                               void f2(float);            real → void
An´lisis sem´ntico
  a         a                                                                                           15


4.2.1.   Equivalencia de tipos
    Comprobar si dos expresiones de tipo, T1 y T2 , son equivalentes es muy sencillo. Si ambas
corresponden a tipos elementales, son equivalentes si son iguales. En caso de que correspondan a
tipos estructurados, hay que comprobar si son el mismo tipo y si los componentes son equivalentes.
    En muchos lenguajes se permite dar nombre a los tipos. Esto introduce una sutileza a la hora
de comprobar la equivalencia. La cuesti´n es, dada una declaraci´n como
                                       o                           o

  typedef int a;
  typedef int b;

¿son equivalentes a y b? La respuesta depende del lenguaje (o, en casos como el Pascal, de la
implementaci´n). Existen dos criterios para la equivalencia:
             o

Equivalencia de nombre: dos expresiones de tipo con nombre son equivalentes si y s´lo si tienen
                                                                                  o
    el mismo nombre.
Equivalencia estructural: dos expresiones de tipo son equivalentes si y s´lo si tienen la misma
                                                                         o
    estructura.

Hay argumentos a favor de uno y otro criterio. En cualquier caso, hay que tener en cuenta que
para comprobar la equivalencia estructural ya no sirve el m´todo trivial presentado antes. En este
                                                             e
caso, puede haber ciclos, lo que obliga a utilizar algoritmos m´s complicados.
                                                               a

4.2.2.   Comprobaci´n de tipos en expresiones
                   o
    Para comprobar los tipos en las expresiones podemos utilizar un atributo que indique el tipo
de la expresi´n. Este tipo se infiere a partir de los distintos componentes de la expresi´n y de las
              o                                                                            o
reglas de tipos del lenguaje.
    A la hora de calcular el tipo hay varias opciones: podemos calcularlo sobre la gram´tica, sobre
                                                                                          a
el AST o al construir el AST. En cualquier caso, puede ser util tener una funci´n similar a la de
                                                                ´                  o
la secci´n 3.2 que refleje las peculiaridades de los tipos del lenguaje. Por ejemplo, si tenemos tipos
        o
entero y real con las reglas habituales de promoci´n, esta funci´n devolver´ los resultados seg´n
                                                     o             o           ıa                  u
esta tabla:

                                    Primero      Segundo       Resultado
                                    entero        entero       entero
                                    entero        real         real
                                    real          entero       real
                                    real          real         real
                                              otros            error tipo

    El c´lculo de atributos de tipo deber´ ser trivial sobre la gram´tica o sobre el AST. En cuanto
        a                                ıa                         a
al c´lculo al construir el AST, se puede, por ejemplo, hacer que el constructor compruebe los tipos
    a
de los componentes que recibe. En este caso, si es necesario, se puede a˜adir un nodo de promoci´n
                                                                        n                        o
de tipos. Por ejemplo, si vamos a crear un nodo suma a partir de los ´rboles:
                                                                        a

                                  producto                        suma
                                  t: entero                       t: real

                                                     y                             ,
                          variable       num               variable         num
                         t: entero t: entero               t: real   t: real
                         nombre: a valor: 3                nombre: z valor: 3.14

podemos a˜adir un nodo intermedio para indicar la promoci´n:
         n                                               o

c Universitat Jaume I 2006-2007
16                                                                       II26 Procesadores de lenguaje


                                                   suma
                                                   t: real



                                   enteroAreal                 suma
                                                              t: real

                                     producto
                                     t: entero          variable        num
                                                       t: real   t: real
                                                       nombre: z valor: 3.14
                                variable     num
                               t: entero t: entero
                               nombre: a valor: 3

        Respecto a la propagaci´n de los errores de tipo, hay que intentar evitar un exceso de mensajes
                                 o
     de error. Para ello se pueden utilizar distintas estrategias:
          Se puede hacer que error tipo sea compatible con cualquier otro tipo.
          Se puede intentar inferir cu´l ser´ el tipo en caso de no haber error. Por ejemplo: si el
                                      a     ıa
          operador en que se ha detectado el error es el y-l´gico, se puede devolver como tipo el tipo
                                                            o
          l´gico.
           o


     5.    Interpretaci´n
                       o
        La utilizaci´n del AST hace que la interpretaci´n sea bastante sencilla. Una vez lo hemos
                    o                                   o
     construido, tenemos dos aproximaciones para la interpretaci´n:
                                                                o
          Podemos representar la entrada mediante una serie de instrucciones similares a un lenguaje
          m´quina con algunas caracter´
            a                         ısticas de alto nivel. Este es el caso, por ejemplo, de Java y el
          Java bytecode. Esta aproximaci´n representa pr´cticamente una compilaci´n.
                                        o                 a                           o
          La otra alternativa es representar la entrada de manera similar al AST y recorrerlo para
          ejecutar el programa.
        Utilizaremos la segunda opci´n. Los nodos correspondientes a las expresiones tendr´n un m´-
                                       o                                                     a       e
     todo al que llamaremos eval´a y que devolver´ el resultado de evaluar el sub´rbol correspondiente.
                                 u                a                               a
     Por ejemplo, en el nodo de la suma tendr´ ıamos:
        Objeto NodoSuma:
             ...
             M´todo eval´a()
                e          u
                  devuelve i.eval´a() + d.eval´a(); // i y d son los hijos del nodo
                                   u           u
             fin eval´a
                     u
             ...
        fin NodoSuma
        Aquellos nodos que representen sentencias tendr´n el m´todo interpreta. Por ejemplo, para un
                                                         a      e
     nodo que represente un mientras, el m´todo correspondiente ser´
                                             e                        ıa:
        Objeto NodoMientras:
             ...
             M´todo interpreta()
                e
                  mientras condici´n.eval´a()=cierto:
                                     o      u
                       sentencias.interpreta()
                  fin mientras
             fin interpreta
             ...
        fin NodoMientras
An´lisis sem´ntico
  a         a                                                                                          17




    Las variables se representar´ mediante entradas en una tabla (que, en casos sencillos, puede
                                ıan
ser la tabla de s´
                 ımbolos) que contenga el valor correspondiente en cada momento. Una asignaci´n   o
consiste simplemente en evaluar la expresi´n correspondiente y cambiar la entrada de la tabla.
                                           o
    La ejecuci´n del programa consistir´ en una llamada al m´todo interpreta del nodo ra´ del
              o                         a                        e                            ız
programa. Alternativamente, si hemos guardado el programa como una lista de sentencias, la
ejecuci´n consistir´ en un bucle que recorra la lista haciendo las llamadas correspondientes.
        o          a
    L´gicamente, existen otras maneras de recorrer el ´rbol. En particular, si no lo hemos represen-
      o                                                a
tado con objetos, podemos recorrerlo mediante funciones recursivas o de manera iterativa. En este
ultimo caso, podemos emplear una pila auxiliar o ir “enhebrando” el ´rbol durante su construcci´n.
´                                                                     a                          o
Esta es probablemente la opci´n m´s r´pida con una implementaci´n cuidadosa, pero es tambi´n
                               o    a a                               o                           e
la que m´s esfuerzo exige (especialmente si hay que recorrer el ´rbol en distintos ´rdenes).
          a                                                      a                   o


6.      Introducci´n a metacomp
                  o
    Mediante metacomp se pueden traducir esquemas de traducci´n a programas Python que im-
                                                                  o
plementan los correspondientes analizadores descendentes recursivos.
    Un programa metacomp se divide en varias secciones. Cada secci´n se separa de la siguiente
                                                                      o
por un car´cter “ %” que ocupa la primera posici´n de una l´
           a                                      o           ınea. La primera secci´n contiene la
                                                                                    o
especificaci´n del analizador l´xico. Las secciones pares contienen c´digo de usuario. La tercera y
            o                 e                                     o
sucesivas secciones impares contienen el esquema de traducci´n. Es decir, un programa metacomp
                                                              o
se escribe en un fichero de texto siguiendo este formato:
    Especificaci´n l´xica
                o e
    %
    C´digo de usuario
      o
    %
    Esquema de traducci´no
    %
    C´digo de usuario
      o
    %
    Esquema de traducci´no
    .
    .
    .
    Las secciones de “c´digo de usuario” se utilizan para declarar estructuras de datos y variables,
                       o
definir funciones e importar m´dulos utiles para el procesador de lenguaje que estamos especifican-
                              o      ´
do. Estos elementos deber´n codificarse en Python. La herramienta metacomp copia literalmente
                           a
estos fragmentos de c´digo en el programa que produce como salida.
                      o


6.1.     Especificaci´n l´xica
                    o e
     La especificaci´n l´xica consiste en una serie de l´
                   o e                                 ıneas con tres partes cada una:

       Un nombre de categor´ o la palabra None si la categor´ se omite.
                           ıa                               ıa
       Un nombre de funci´n de tratamiento o None si no es necesario ning´n tratamiento.
                         o                                               u
       Una expresi´n regular.
                  o

    Los analizadores l´xicos generados por metacomp dividen la entrada siguiendo la estrategia
                       e
avariciosa y, en caso de conflicto, prefiriendo las categor´ que aparecen en primer lugar. Por
                                                          ıas
cada lexema encontrado en la entrada se llama a la correspondiente funci´n de tratamiento, que
                                                                           o
normalmente se limita a calcular alg´n atributo adicional a los tres que tienen por defecto todos
                                     u
los componentes: lexema, nlinea y cat, que representan, respectivamente, el lexema, el n´mero de
                                                                                        u
l´
 ınea y la etiqueta de categor´ del componente.
                              ıa

c Universitat Jaume I 2006-2007
18                                                                                II26 Procesadores de lenguaje


         Un ejemplo de especificaci´n l´xica para una calculadora sencilla ser´
                                  o e                                        ıa:
                          categor´
                                 ıa       expresi´n regular
                                                 o                  acciones            atributos
                                                 +
                          num             [0–9]                     calcular valor      valor
                                                                    emitir
                          sum             [-+]                      copiar lexema       lexema
                                                                    emitir
                          abre            (                        emitir
                          cierra          )                        emitir
                          espacio         [ t]+                    omitir
                          nl              n                        emitir

         En metacomp, lo podemos escribir as´
                                            ı:
       num       valor [0-9]+
       sum       None [-+]
       abre      None (
       cierra    None )
       None      None [ t]+
       nl        None n
         Donde valor ser´ una funci´n como la siguiente:
                        ıa         o
       def valor(componente):
         componente.v= int(componente.lexema)
     No necesitamos ning´n tratamiento para sum ya que metacomp a˜ade por defecto el atributo lexema
                        u                                           n
     (que es el que hemos usado para calcular el valor de los enteros).

     6.2.     Esquema de traducci´n
                                 o
         Las secciones de esquema de traducci´n contienen las reglas de la GPDR utilizando una sintaxis
                                              o
     muy similar a la de la asignatura. Cada regla tiene una parte izquierda, un s´ımbolo ->, una parte
     derecha y un punto y coma. Los no terminales se escriben marcados mediante < y >. Los terminales
     tienen que coincidir con los declarados en la especificaci´n l´xica2 . Se pueden emplear los opera-
                                                               o e
     dores regulares de concatenaci´n (impl´
                                   o        ıcita), disyunci´n (mediante |), opcionalidad (mediante ?),
                                                            o
     clausura (mediante *) y clausura positiva (mediante +). Por ejemplo, la regla
                                                     E   →   T (sum T )∗
     se escribir´ en metacomp as´
                ıa              ı:
       <E> -> <T> ( sum <T> )*;
         Las acciones de las reglas se escriben encerradas entre arrobas (@) y sin fin de l´ ınea entre
     ellas. Para poder referirse a los terminales y no terminales, estos est´n numerados por su orden
                                                                            a
     de aparici´n en la parte derecha de la regla. El no terminal de la izquierda no tiene n´mero y,
                o                                                                             u
     si no hay ambig¨edad, la primera aparici´n de cada s´
                      u                         o           ımbolo no necesita n´mero. Si tenemos las
                                                                                 u
     acciones de la regla anterior:
       E    →     T 1 {v:= T 1 .v}
                 (sum T 2 {si sum.lexema= “+” entonces v:= v+ T 2 .v si no v:= v− T 2 .v fin si})∗
                  { E .v:= v}
     podemos escribirlas en metacomp as´
                                       ı:
       2 Existe tambi´n la posibilidad de utilizar una sintaxis especial para categor´
                     e                                                               ıas con un s´lo lexema, pero no lo
                                                                                                 o
     comentaremos.
An´lisis sem´ntico
   a         a                                                                                     19


      <E> -> <T> @v= T.v@
             ( sum <T>
               @if sum.lexema=="+":@
               @ v+= T2.v@
               @else:@
               @ v-= T2.v@
             )*
             @E.v= v@;


 7.        Algunas aplicaciones
     Vamos a aprovechar metacomp para reescribir los ejemplos que vimos en el tema de an´lisis
                                                                                          a
 l´xico: la suma de las columnas de un fichero con campos separados por tabuladores y la lectura
  e
 de ficheros de configuraci´n.
                           o

 7.1.       Ficheros organizados por columnas
        La representaci´n en metacomp de la especificaci´n l´xica podr´ ser:
                       o                               o e           ıa

1       numero        valor     [0-9]+
2       separacion    None      t
3       nl            None      n
4       None          None      [ ]+

    La funci´n valor se define en la zona de auxiliares junto con dos funciones para tratamiento
             o
 de errores:

 5      %
 6      def valor(componente):
 7        componente.v= int(componente.lexema)
 8
 9      def error(linea, mensaje):
10        sys.stderr.write("Error en l´nea %d: %sn" % (linea, mensaje))
                                      ı
11        sys.exit(1)
12
13      def error_lexico(linea, cadena):
14        if len(cadena)> 1:
15          error(linea, "no he podido analizar la cadena %s." % repr(cadena))
16        else:
17          error(linea, "no he podido analizar el car´cter %s." % repr(cadena))
                                                      a
18
19      ncol= 0

     La funci´n error la utilizamos para escribir mensajes gen´ricos. La funci´n error_lexico es
             o                                                  e              o
 llamada por metacomp cuando encuentra un error l´xico. Los dos par´metros que recibe son la
                                                      e                  a
 l´
  ınea donde se ha producido el error y el car´cter o caracteres que no se han podido analizar.
                                              a
     Podemos emplear el siguiente esquema de traducci´n:o

     Fichero   →     {suma:=0}( L´                      ınea .v})∗ { Fichero .suma:= suma}
                                 ınea nl {suma:= suma+ L´
       L´
        ınea   → numero1 {global ncol; col:=1; si col= ncol entonces L´
                                                                      ınea .v:= numero.v fin si}
                 (separaci´n n´mero2
                          o u
                     {col:= col+1; si col= ncol entonces L´
                                                          ınea .v:= numero2 .v fin si}
                    )∗

     c Universitat Jaume I 2006-2007
20                                                                      II26 Procesadores de lenguaje


         Hemos supuesto que el s´
                                ımbolo inicial tiene un atributo sintetizado que leer´ el entorno. Ade-
                                                                                     a
      m´s, la columna deseada est´ en la variable global ncol. Representamos esto en metacomp as´
       a                         a                                                                 ı:

     20   %
     21   <Fichero>->     @suma= 0@
     22                 ( <Linea> @suma+= Linea.v@ nl )*
     23                   @Fichero.suma= suma@ ;
     24
     25   <Fichero>-> error
     26                 @error(mc_al.linea(), "l´nea mal formada")@
                                                ı
     27               ;
     28
     29   <Linea>-> numero
     30               @col= 1@
     31               @if col== ncol:@
     32               @ Linea.v= numero.v@
     33             ( separacion numero
     34                 @col+= 1@
     35                 @if col== ncol:@
     36                 @ Linea.v= numero2.v@
     37             )*
     38             @if col< ncol:@
     39             @ error(numero.nlinea, "no hay suficientes columnas")@
     40             ;
     41

      La regla <Fichero>-> error nos permite capturar los errores que se produzcan durante el an´lisis.
                                                                                                   a
      Hay varias maneras de tratar los errores, pero hemos optado por abortar el programa. Hemos
      utilizado mc_al, que contiene el analizador l´xico, para averiguar el n´mero de l´
                                                   e                         u         ınea del error.
          Finalmente, hemos decidido que el programa tenga un par´metro que indique qu´ columna
                                                                       a                      e
      sumar. Si no se especifica ning´n par´metro adicional, se leer´ la entrada est´ndar. En caso de que
                                     u      a                       a              a
      haya varios par´metros, se sumar´ la columna deseada de cada uno de los ficheros especificados,
                      a                  a
      escribi´ndose el total. Con esto, el programa principal es:
             e

     42   %
     43   def main():
     44     global ncol
     45     if len(sys.argv)== 1:
     46       sys.stderr.write("Error, necesito un n´mero de columna.n")
                                                    u
     47       sys.exit(1)
     48     try:
     49       ncol= int(sys.argv[1])
     50     except:
     51       sys.stderr.write("Error, n´mero de columna mal formado.n")
                                        u
     52       sys.exit(1)
     53
     54     if len(sys.argv)== 2:
     55       A= AnalizadorSintactico(sys.stdin)
     56       suma= A.Fichero.suma
     57     else:
     58       suma= 0
     59       for arg in sys.argv[2:]:
     60         try:
     61           f= open(arg)
     62         except:
An´lisis sem´ntico
   a         a                                                                                            21


63              sys.stderr.write("Error, no he podido abrir el fichero %s.n" % arg)
64              sys.exit(1)
65            A= AnalizadorSintactico(f)
66            suma+= A.Fichero.suma
67        print "La suma es %d." % suma

 Por defecto, metacomp genera una funci´n main que analiza sys.stdin. Si queremos modificar ese
                                         o
 comportamiento, debemos crear nuestra propia funci´n. Como ves, la mayor parte del c´digo se
                                                       o                                  o
 dedica a analizar posibles errores en los par´metros. La construcci´n m´s interesante est´ en las
                                              a                     o   a                 a
 dos l´
      ıneas que crean el analizador. La asignaci´n
                                                o
      A= AnalizadorSintactico(f)
 hace que se cree un analizador que inmediatamente analizar´ el fichero f (que debe estar abierto).
                                                             a
 Cuando termina el an´lisis, el objeto A tiene un atributo con el nombre del s´
                        a                                                     ımbolo inicial y que
 tiene sus atributos sintetizados. As´ es como transmitimos el resultado.
                                     ı

 7.2.       Ficheros de configuraci´n
                                  o
     Vamos ahora a programar el m´dulo de lectura de los ficheros de configuraci´n. Haremos que
                                      o                                           o
 el resultado del an´lisis sea un diccionario en el que las claves sean las variables y los valores
                      a
 asociados sean los le´ıdos del fichero.
     El analizador l´xico se escribir´ as´
                    e                ıa ı:

1       variable      None      [a-zA-Z][a-zA-Z0-9]*
2       igual         None      =
3       valor         None      [0-9]+|"[^"n]*"
4       nl            None      (#[^n]*)?n
5       None          None      [ t]+

        Los unicos auxiliares que necesitamos son las funciones de tratamiento de errores:
            ´

 6      %
 7      def error(linea, mensaje):
 8        sys.stderr.write("Error en l´nea %d: %sn" % (linea, mensaje))
                                      ı
 9        sys.exit(1)
10
11      def error_lexico(linea, cadena):
12        if len(cadena)> 1:
13          error(linea, "no he podido analizar la cadena %s." % repr(cadena))
14        else:
15          error(linea, "no he podido analizar el car´cter %s." % repr(cadena))
                                                      a

        Nuestro esquema de traducci´n ser´:
                                   o     a

     Fichero    →    {dic:= {}}(( L´
                                   ınea {dic[ L´              ınea .dcha}|λ) nl)∗ { Fichero .dic:= dic}
                                               ınea .izda]:= L´
       L´
        ınea    → variable { L´
                              ınea .izda:= variable.lexema } igual valor { L´
                                                                            ınea .dcha:= valor.lexema}

        Pasado a metacomp:

16      %
17      <Fichero>->      @dic= {}@
18                     ( (<Linea> @dic[Linea.izda]= Linea.dcha@)? nl )*
19                       @Fichero.dic= dic@ ;
20
21      <Fichero>-> error

     c Universitat Jaume I 2006-2007
22                                                                        II26 Procesadores de lenguaje


     22                      @error(mc_al.linea(), "l´nea mal formada")@
                                                     ı
     23                  ;
     24
     25    <Linea>-> variable
     26                @Linea.izda= variable.lexema@
     27              igual valor
     28                @Linea.dcha= valor.lexema@
     29              ;
     30

           Como ves, hemos utilizado el operador de opcionalidad para representar la disyunci´n ( Linea |λ).
                                                                                             o



      8.      Resumen del tema
             El analizador sem´ntico tiene dos objetivos:
                              a

                • Hacer comprobaciones que no se hagan durante el an´lisis l´xico o sint´ctico.
                                                                      a     e           a
                • Crear una representaci´n adecuada para fases posteriores.
                                        o

             Implementaremos el an´lisis sem´ntico en dos partes:
                                  a         a

                • Mediante esquemas de traducci´n dirigidos por la sintaxis.
                                               o
                • Recorriendo el AST.

             Un esquema de traducci´n dirigido por la sintaxis a˜ade a las gram´ticas:
                                   o                            n              a

                • Acciones intercaladas en las partes derechas de las reglas.
                • Atributos asociados a los no terminales.

             Dos tipos de atributos: heredados y sintetizados.
             Las acciones deben garantizar que se eval´an correctamente los atributos.
                                                      u
             Se pueden implementar los esquemas de traducci´n sobre los analizadores sint´cticos inter-
                                                            o                             a
             pretando los atributos como par´metros y a˜adiendo el c´digo de las acciones al c´digo del
                                            a          n            o                         o
             analizador.
             El c´lculo de algunos atributos y algunas comprobaciones sem´nticas son m´s f´ciles sobre
                 a                                                       a            a a
             el AST.
             La tabla de s´
                          ımbolos se puede implementar eficientemente mediante una tabla hash.
             Las comprobaciones de tipos se complican si se introducen nombres. Dos criterios:

                • Equivalencia de nombre.
                • Equivalencia estructural.

             La interpretaci´n se puede realizar mediante recorridos del AST.
                            o
             metacomp permite implementar c´modamente esquemas de traducci´n dirigidos por la sin-
                                           o                              o
             taxis.

Más contenido relacionado

PDF
Tr asem-ver
PPT
Clase15
PPT
Download.php
PDF
Ps2 u5
PPT
Curso lenguaje c_segundo_modulo_
PPTX
3. Elementos basicos de un programa
PDF
Analisis semantico
PPTX
La programación informática o programación algorítmica, acortada
Tr asem-ver
Clase15
Download.php
Ps2 u5
Curso lenguaje c_segundo_modulo_
3. Elementos basicos de un programa
Analisis semantico
La programación informática o programación algorítmica, acortada

La actualidad más candente (20)

PPTX
Analisis semantico
PDF
Unidad4 analisis-semantico
DOCX
Compiladores analizadores gramática y algo mas
PDF
Compiladores, Analisis Lexico Conceptos
PDF
Automatas y compiladores analisis sintactico
PDF
ANALIZADOR SINTACTICO: INTRODUCION, CONCEPTOS, CARACTERISTICAS
PDF
Analizador Sintactico
PDF
Gramatica libre de contexto
PPTX
Analizador Sintáctico
PDF
Analizador sintactico
PDF
Clase6 conceptos del analisis lexico
PPTX
Presentación 2014 profe gabriel
PPT
Claselexico
PPT
Analizador LÉxico
DOCX
37 tarazona karen programacion
PDF
Analisis sintactico
PDF
Materia unidad compiladores
PPTX
Clase8 2-explicacion analizador lexico-sintactico mini dev
PPTX
Análisis léxico y análisis sintáctico
Analisis semantico
Unidad4 analisis-semantico
Compiladores analizadores gramática y algo mas
Compiladores, Analisis Lexico Conceptos
Automatas y compiladores analisis sintactico
ANALIZADOR SINTACTICO: INTRODUCION, CONCEPTOS, CARACTERISTICAS
Analizador Sintactico
Gramatica libre de contexto
Analizador Sintáctico
Analizador sintactico
Clase6 conceptos del analisis lexico
Presentación 2014 profe gabriel
Claselexico
Analizador LÉxico
37 tarazona karen programacion
Analisis sintactico
Materia unidad compiladores
Clase8 2-explicacion analizador lexico-sintactico mini dev
Análisis léxico y análisis sintáctico
Publicidad

Destacado (6)

PPT
Código intermedio
PPT
Analisis Lexico
PPT
Generador de codigo intermedio
PPT
Analizador léxico
PDF
Compiladores, Analisis Lexico, Ejemplo Minilenguaje
PPTX
Analizador léxico
Código intermedio
Analisis Lexico
Generador de codigo intermedio
Analizador léxico
Compiladores, Analisis Lexico, Ejemplo Minilenguaje
Analizador léxico
Publicidad

Similar a Semantico.apun (20)

DOCX
Prolog cinthya
DOC
SeúDocodigo
PPT
Curso prog sist
PPTX
Compilador2
PPTX
Logica tipos de datos operadores
PDF
Resumen (semana 9)
PPT
Lenguajec intorduccionui
PPTX
Programación Introducción al lenguaje C
PDF
AlgoríTmica Y ProgramacióN
DOCX
Solucion del taller numero1
DOCX
Primer trabajo mtro octavio
DOCX
Compiladores
PPTX
Lenguaje c (expresiones logicas)
PPT
Compiladores - Incorporacion de una Tabla de Simbolos Compiladores
PPT
Incorporacion De Una Tabla De Simbolos Compiladores
PPTX
Programación
PPTX
PPT
lenguaje c
PDF
Capitulo 4
Prolog cinthya
SeúDocodigo
Curso prog sist
Compilador2
Logica tipos de datos operadores
Resumen (semana 9)
Lenguajec intorduccionui
Programación Introducción al lenguaje C
AlgoríTmica Y ProgramacióN
Solucion del taller numero1
Primer trabajo mtro octavio
Compiladores
Lenguaje c (expresiones logicas)
Compiladores - Incorporacion de una Tabla de Simbolos Compiladores
Incorporacion De Una Tabla De Simbolos Compiladores
Programación
lenguaje c
Capitulo 4

Semantico.apun

  • 1. 4o Ingenier´ Inform´tica ıa a II26 Procesadores de lenguaje An´lisis sem´ntico a a Esquema del tema 1. Introducci´n o 2. Esquemas de traducci´n dirigidos por la sintaxis o 3. El ´rbol de sintaxis abstracta a 4. Comprobaciones sem´nticas a 5. Interpretaci´n o 6. Introducci´n a metacomp o 7. Algunas aplicaciones 8. Resumen del tema 1. Introducci´n o Hay determinadas caracter´ ısticas de los lenguajes de programaci´n que no pueden ser mode- o ladas mediante gram´ticas incontextuales y que es necesario comprobar en una fase posterior al a an´lisis sint´ctico. Por otro lado, las fases posteriores de la compilaci´n o interpretaci´n necesitan a a o o una representaci´n de la entrada que les permita llevar a cabo sus funciones de manera adecuada. o Estas dos vertientes —detecci´n de errores y representaci´n de la informaci´n— est´n muy o o o a relacionadas y se solapan en la pr´ctica. Supongamos, por ejemplo, que nuestro lenguaje permite a asignaciones seg´n la regla u Asignaci´n → id:= Expresi´n ; o o Es habitual que se impongan ciertas restricciones. En nuestro caso, estas podr´ ser: ıan El identificador de la parte izquierda debe estar declarado previamente. El tipo de la expresi´n debe ser compatible con el del identificador. o El analizador sem´ntico deber´ comprobar que estas dos restricciones se cumplen antes de decla- a a rar que la sentencia de asignaci´n est´ bien formada. Pero sucede que la informaci´n necesaria o a o para comprobarlas es util tambi´n para generar c´digo. Esto quiere decir que si tuvi´ramos una ´ e o e separaci´n estricta entre las fases del compilador, para generar c´digo deber´ o o ıamos volver a mirar el identificador para saber a qu´ objeto (variable, funci´n, constante, etc.) corresponde y qu´ tipo e o e tiene y tambi´n deber´ e ıamos volver a comprobar los tipos de la expresi´n para generar el c´digo o o adecuado. Por otro lado, aunque en teor´ el arbol de an´lisis ser´ suficiente para fases posteriores de la ıa ´ a ıa compilaci´n o interpretaci´n, es una representaci´n que contiene mucha informaci´n redundante. o o o o As´ el ´rbol correspondiente a a:= b+c; bien podr´ tener el aspecto del ´rbol de la izquierda, ı, a ıa a cuando el de la derecha contiene esencialmente la misma informaci´n y resulta m´s c´modo para o a o
  • 2. 2 II26 Procesadores de lenguaje trabajar: Asignaci´n o ida := Expresi´n o ; asignaci´n o Expresi´n o + T´rmino e variablea suma T´rmino e Factor variableb constantec Factor idc idb El segundo ´rbol se conoce como ´rbol de sintaxis abstracta (o AST, de las iniciales en ingl´s). a a e Como hemos comentado, durante el an´lisis sem´ntico se recoge una serie de informaciones que a a resultan de utilidad para fases posteriores. Estas informaciones se pueden almacenar en el ´rbol, a “decor´ndolo”: a asignaci´n o variable suma nombre: a tipo: entero tipo: entero variable constante nombre: b nombre: c tipo: entero tipo: entero valor: 5 As´ el objetivo de la fase de an´lisis sem´ntico ser´ doble: por un lado detectaremos errores que ı a a a no se han detectado en fases previas y por otro lado obtendremos el AST decorado de la entrada. Para ello utilizaremos esquemas de traducci´n dirigidos por la sintaxis, que permitir´n asociar o a acciones a las reglas de la gram´tica. Estas acciones realizar´n comprobaciones y construir´n el a a a AST que despu´s se recorrer´ para terminar las comprobaciones y ser´ la base para la interpreta- e a a ci´n o la generaci´n de c´digo. o o o 2. Esquemas de traducci´n dirigidos por la sintaxis o Nuestro objetivo es especificar una serie de acciones que se realizar´n durante el an´lisis de a a la entrada y tener un mecanismo que nos permita obtener la informaci´n necesaria para realizar o estas acciones. Para esto a˜adimos a las gram´ticas dos elementos nuevos: n a Acciones intercaladas en las reglas. Atributos asociados a los no terminales de la gram´tica. a 2.1. Acciones intercaladas en las reglas Supongamos que tenemos el no terminal A que deriva dos partes diferenciadas y queremos que se escriba el mensaje “Cambio de parte” tras analizarse la primera. Podemos describir esto
  • 3. An´lisis sem´ntico a a 3 de forma sencilla si aumentamos nuestra gram´tica incluyendo la acci´n en la parte derecha de la a o regla de A : A → Parte1 {escribe(“Cambio de parte”);} Parte2 Podemos entender esta regla extendida como la receta: “analiza la primera parte, una vez encontrada, escribe el mensaje y despu´s analiza la segunda parte”. e 2.2. Atributos asociados a los no terminales Si nuestras acciones se limitaran a escribir mensajes que indicaran la fase del an´lisis en la que a nos encontramos, no necesitar´ ıamos mucho m´s. En general, querremos que las acciones respondan a al contenido de los programas que escribimos. Para ello, vamos a a˜adir a los no terminales una serie n de atributos. Estos no son m´s que una generalizaci´n de los atributos que tienen los terminales. a o Vamos a ver un ejemplo. Supongamos que en un lenguaje de programaci´n se exige que en las subrutinas aparezca un o identificador en la cabecera que debe coincidir con el que aparece al final. Tenemos el siguiente fragmento de la gram´tica del lenguaje: a Subrutina → Cabecera Cuerpo Fin Cabecera → subrutina id( Par´metros ); a Fin → fin id En la regla correspondiente a Fin querremos comprobar que el identificador es el mismo que en la cabecera. Vamos a definir un atributo del no terminal Fin que ser´ el que nos diga el nombre a de la funci´n: o Fin → fin id {si id.lexema = Fin .nombre entonces error fin si} El atributo nombre es lo que se conoce como un atributo heredado: su valor se calcular´ en las a reglas en las que Fin aparezca en la parte derecha y se podr´ utilizar en las reglas en las que Fin a aparezca en la izquierda; ser´ informaci´n que “heredar´” de su entorno. F´ a o a ıjate en c´mo hemos o empleado un atributo de id para hacer comprobaciones. El analizador sem´ntico es el que emplea a la informaci´n contenida en los atributos de los componentes l´xicos. Recuerda que el sint´ctico o e a s´lo ve las etiquetas de las categor´ En cuanto a error, suponemos que representa las acciones o ıas. necesarias para tratar el error. Ahora tenemos que completar las acciones de modo que Fin tenga alg´n valor para el nombre. u Podemos a˜adir una acci´n a la regla de Subrutina para calcular el valor de Fin .nombre: n o Subrutina → Cabecera Cuerpo { Fin .nombre:= Cabecera .nombre} Fin Lo que hacemos es copiar el atributo nombre que nos “devolver´” Cabecera . No debes confundir a el atributo nombre de Fin con el de Cabecera , son completamente independientes. De hecho, el atributo nombre de Cabecera es un atributo sintetizado: su valor se calcular´ en las reglas en las a que Cabecera aparezca en la parte izquierda y se utilizar´ en las reglas donde Cabecera aparezca a en la parte derecha; es informaci´n que Cabecera “sintetiza” para su entorno. Debemos calcular o el valor de nombre en la regla de Cabecera : Cabecera → subrutina id( Par´metros ); { Cabecera .nombre:= id.lexema} a 2.3. Otros elementos de las acciones Como habr´s comprobado, no hemos definido ning´n lenguaje formal para escribir las acciones. a u Asumiremos que se emplean construcciones corrientes de los algoritmos tales como condicionales c Universitat Jaume I 2006-2007
  • 4. 4 II26 Procesadores de lenguaje o bucles. Tambi´n haremos referencia en ellos a subrutinas y a variables. Las variables las inter- e pretaremos como variables locales a la regla que estamos analizando o como variables globales si lo indicamos expl´ ıcitamente. As´ en: ı, Valor → opsum {si opsum.lexema=“+” entonces signo:= 1 si no signo:= -1 fin si} Valor 1 { Valor .v:= signo* Valor 1 .v} no hay ninguna “interferencia” entre las distintas variables signo que se puedan utilizar en un mo- mento dado. F´ ıjate tambi´n en c´mo hemos distinguido mediante un sub´ e o ındice las dos apariciones del no terminal Valor . Un recurso muy util son los atributos que contienen listas. Por ejemplo, para una declaraci´n ´ o de variables del tipo: DeclVariables → ListaIds : Tipo ListaIds → id, ListaIds |id podemos hacer lo siguiente: DeclVariables → ListaIds : Tipo {para v en ListaIds .l hacer: declara_var(v, Tipo .t) fin para} ListaIds → id, ListaIds 1 { ListaIds .l:= concat( [id.lexema], ListaIds 1 .l)} ListaIds → id { ListaIds .l:= [id.lexema]} 2.4. Recopilaci´n o Con lo que hemos visto, podemos decir que un esquema de traducci´n consta de: o Una gram´tica incontextual que le sirve de soporte. a Un conjunto de atributos asociados a los s´ ımbolos terminales y no terminales. Un conjunto de acciones asociadas a las partes derechas de las reglas. Dividimos los atributos en dos grupos: Atributos heredados. Atributos sintetizados. Sea A → X1 . . . Xn una producci´n de nuestra gram´tica. Exigiremos que: o a Las acciones de la regla calculen todos los atributos sintetizados de A . Las acciones situadas a la izquierda de Xi calculen todos los atributos heredados de Xi . Ninguna acci´n haga referencia a los atributos sintetizados de los no terminales situados a o su derecha en la producci´n. o Las dos ultimas reglas nos permitir´n integrar las acciones sem´nticas en los analizadores descen- ´ a a dentes recursivos. Cuando se emplean, se dice que el esquema de traducci´n est´ basado en una o a gram´tica L-atribuida. La L indica que el an´lisis y evaluaci´n de los atributos se pueden hacer de a a o izquierda a derecha.
  • 5. An´lisis sem´ntico a a 5 Ejercicio 1 Las siguientes reglas representan parte de las sentencias estructuradas de un lenguaje de pro- gramaci´n: o Programa → Sentencias Sentencias → Sentencia Sentencias | Sentencia Sentencia → mientras Expresi´n hacer Sentencias finmientras o Sentencia → si Expresi´n entonces Sentencias sino Sentencias finsi o Sentencia → interrumpir Sentencia → otros A˜ade las reglas necesarias para comprobar que la instrucci´n interrumpir aparece unicamente n o ´ dentro de un bucle. Ejercicio 2 Sea G la siguiente gram´tica: a A → B C |a B → A a B b| C a C → a C C |aba C |a A˜ade a G las reglas sem´nticas necesarias para que el atributo ia de A contenga el n´mero n a u de aes al inicio de la cadena generada. Por ejemplo, dadas las cadenas aaaaba, abaaaa y aaa, los valores de ia ser´ 4, 1 y 3, respectivamente. ıan Puedes utilizar los atributos adicionales que consideres necesarios, pero ninguna variable global. Adem´s, los atributos que a˜adas deben ser de tipo entero o l´gico. a n o 2.5. Algunas cuestiones formales Una pregunta razonable es si puede el s´ ımbolo inicial tener atributos heredados y, si esto es as´ de d´nde proceden. La respuesta var´ seg´n los textos. Los que se oponen, defienden que el ı, o ıa u significado del programa debe depender unicamente de ´l. Los que est´n a favor de los atributos ´ e a heredados, sostienen que permiten formalizar la entrada de informaci´n acerca de, por ejemplo, el o entorno donde se ejecutar´ el programa o las opciones de compilaci´n. a o Una situaci´n similar se da con los s´ o ımbolos terminales: hay autores que piensan que no deber´ıan tener atributos en absoluto; otros defienden que s´lo deben tener atributos sintetizados; y los hay o que opinan que pueden tener tanto atributos heredados como sintetizados. Otro aspecto sobre el que hay diferencias de interpretaci´n es el de los efectos laterales de las o reglas. En algunos textos, las reglas de evaluaci´n de los atributos no pueden tener efectos laterales. o Otros autores defienden que esta distinci´n no es m´s que un problema de implementaci´n ya que o a o es posible a˜adir un nuevo atributo que represente el entorno e ir actualiz´ndolo adecuadamente. n a Esto unicamente supone una incomodidad, pero no un problema formal. Esta es la posici´n que ´ o seguiremos nosotros, permitiendo que haya efectos laterales en el c´lculo de atributos. a Quiz´ los ejemplos m´s claros de existencia de efectos laterales sean el manejo de la tabla de a a s´ ımbolos y el control de errores. Cuando se encuentra una declaraci´n en un programa es necesario o actualizar la tabla de s´ ımbolos de modo que sea posible reconocer ocurrencias posteriores del identificador. Podemos suponer que existe una tabla de s´ ımbolos global o tener un atributo que lleve copias de la tabla de un sitio a otro. Por otro lado, en caso de que se encuentre un error, es necesario tomar medidas como detener la generaci´n de c´digo. Una manera sencilla de marcar esta circunstancia es tener una variable o o c Universitat Jaume I 2006-2007
  • 6. 6 II26 Procesadores de lenguaje global de tipo l´gico que indique si se ha encontrado alg´n error. Nuevamente, ser´ posible, pero o u ıa no necesario, tener un atributo que transporte esa informaci´n. o 2.6. Eliminaci´n de recursividad por la izquierda y atributos o En el tema de an´lisis sint´ctico vimos c´mo se pueden modificar las producciones con recursivi- a a o dad por la izquierda para lograr que la gram´tica sea LL(1). El problema con las transformaciones a es que dejan una gram´tica que tiene muy poca relaci´n con la original, lo que hace que escribir los a o atributos y las reglas correspondientes sea dif´ sobre la gram´tica transformada. Sin embargo, ıcil a se pueden escribir los atributos sobre la gram´tica original y despu´s convertirlos de una manera a e bastante mec´nica. a Como las GPDRs no suelen necesitar recursividad por la izquierda, es f´cil que no tengas que a utilizar esta transformaci´n. Pese a todo, vamos a ver c´mo se hace la transformaci´n sobre un o o o ejemplo. Partimos del siguiente esquema de traducci´n:o E → E 1 + T { E .v:= E 1 .v+ T .v} E → T { E .v:= T .v} T → num { T .v:= num.v} Comenzamos por transformar la gram´tica: a E → T E’ E’ → + T E’ E’ → λ T → num Como vemos, el problema es que cuando vemos el sumando derecho en E’ , no tenemos acceso al izquierdo. La idea ser´ crear un atributo heredado que nos diga el valor del sumando izquierdo. a En la primera regla lo podemos calcular directamente. En la segunda regla, realizamos la suma y la transmitimos: E → T { E’ .h:= T .v} E’ E’ → + T { E’ 1 .h:= E’ .h+ T .v} E’ 1 Con esto, el atributo h contendr´ siempre la suma, que es el valor que tenemos que devolver a finalmente. Por eso hay que “transmitir” el nuevo valor al atributo v: E → T { E’ .h:= T .v } E’ { E .v:= E’ .v} E’ → + T { E’ 1 .h:= E’ .h+ T .v} E’ 1 { E’ .v:= E’ 1 .v} ¿Qu´ hacemos cuando E’ se reescribe como la cadena vac´ En este caso, basta con devolver e ıa? como sintetizado el valor que se hereda. El esquema de traducci´n completo queda: o E → T { E’ .h:= T .v} E’ { E .v:= E’ .v} E’ → + T { E’ 1 .h:= E’ .h+ T .v} E’ 1 { E’ .v:= E’ 1 .v} E’ → λ { E’ .v:= E’ .h} T → num { T .v:= num.v} Ejercicio 3 Escribe el ´rbol de an´lisis de 3+4 con el esquema original y el transformado. Dec´ralos. a a o
  • 7. An´lisis sem´ntico a a 7 Ejercicio 4 Transforma el siguiente esquema de traducci´n para eliminar la recursividad por la izquierda: o E → E 1 + T { E .v:= E 1 .v+ T .v} E → T { E .v:= T .v} T → T 1 * F { T .v:= T 1 .v* F .v} T → F { T .v:= F .v} F → ( E ) { F .v:= E .v} F → num { F .v:= num.v} El siguiente ejercicio presenta la transformaci´n de una manera m´s general. o a Ejercicio* 5 Si tenemos las siguientes reglas en un esquema de traducci´n o A → A 1 Y { A .a:= g( A 1 .a, Y .y)} A → X { A .a:= f( X .x)} podemos transformarlas en A → X { A’ .h:= f( X .x)} A’ { A .a:= A’ .s} A’ → Y { A’ 1 .h:= g( A’ .h, Y .y)} A’ 1 { A’ .s:= A’ 1 .s} A’ → λ { A’ .s:= A’ .h} Comprueba que la transformaci´n es correcta analizando X Y 1 Y 2 mediante las dos versiones y o comparando el valor de a. 2.7. Implementaci´n de los esquemas de traducci´n o o La interpretaci´n de las acciones como sentencias que se ejecutan al pasar el an´lisis por ellas o a permite implementar los esquemas de traducci´n de manera sencilla. Para ello se modifica la o implementaci´n del analizador recursivo descendente correspondiente a la gram´tica original de la o a siguiente manera: Los atributos heredados del no terminal A se interpretan como par´metros de entrada de a la funci´n Analiza_A. o Los atributos sintetizados del no terminal A se interpretan como par´metros de salida de a la funci´n Analiza_A. o Las acciones sem´nticas, una vez traducidas al lenguaje de programaci´n correspondiente, se a o insertan en la posici´n correspondiente seg´n su orden en la parte derecha donde aparecen. o u En la pr´ctica, es frecuente que, si el lenguaje de programaci´n (como C) no permite devolver a o m´s de un valor, los atributos sintetizados del no terminal se pasen por referencia. a En Python existe una soluci´n bastante c´moda. Comenzamos por definir una clase vac´ o o ıa: class Atributos: pass Antes de llamar a una funci´n o m´todo de an´lisis, creamos un objeto de esta clase y le o e a a˜adimos los atributos heredados del no terminal. La correspondiente funci´n de an´lisis crear´ n o a a los atributos sintetizados. Este es el unico par´metro que se pasa. As´ la traducci´n de la regla: ´ a ı, o c Universitat Jaume I 2006-2007
  • 8. 8 II26 Procesadores de lenguaje E → T { R .h:= T .v} R { E .v:= R .v} es la siguiente: def analiza_E(E): T= Atributos() # Atributos de T R= Atributos() # Atributos de R analiza_T(T) R.h= T.v # Creamos un atributo heredado de R analiza_R(R) E.v= R.v # Creamos un atributo sintetizado de E L´gicamente, tendr´ o ıamos que haber a˜adido el c´digo de control de errores. n o 2.8. Atributos en GPDR La interpretaci´n que hemos hecho de los esquemas de traducci´n se traslada de forma natural o o a las GPDR. Por ejemplo, la traducci´n de: o E → T 1 { E .v:= T 1 .v}(+ T 2 { E .v:= E .v+ T 2 .v})∗ es simplemente: def analiza_E(E): T1= Atributos() # Atributos de T1 T2= Atributos() # Atributos de T2 analiza_T(T1) E.v= T1.v while token.cat=="suma": token= alex.siguiente() analiza_T(T2) E.v= E.v+T2.v Como antes, habr´ que a˜adir el correspondiente c´digo para controlar errores. ıa n o 3. El ´rbol de sintaxis abstracta a Como hemos comentado en la introducci´n, una de las posibles representaciones sem´nticas de o a la entrada es el ´rbol de sintaxis abstracta o AST. a Aunque es similar a los ´rboles de an´lisis sint´ctico, tiene algunas diferencias importantes: a a a No aparecen todos los componentes l´xicos del programa. Por ejemplo: e • No es necesario incluir los par´ntesis de las expresiones. e • No se necesitan los separadores o terminadores de las sentencias. • ... Pueden aparecer otros componentes no estrictamente sint´cticos, como acciones de coerci´n a o de tipos.
  • 9. An´lisis sem´ntico a a 9 3.1. Construcci´n o Para construir los ´rboles, debemos comenzar por definir qu´ elementos emplearemos para cada a e estructura: Estructura Representaci´n o Estructura Representaci´n o si sentencias if E then LS end begin S 1 . . . S n end E LS S 1 ... Sn mientras asignaci´n o while C do LS end id:= E ; C LS id E repetir suma repeat LS until C ; E 1+ E 2 LS C E1 E2 ... ... ... ... F´ ıjate que el ´rbol resultante puede representar programas en diversos lenguajes de programa- a ci´n del estilo C, Pascal, etc. o Ahora debemos utilizar los atributos para construir el ´rbol. Utilizando el atributo arb para a devolver el ´rbol que construye cada no terminal, podemos hacer algo parecido a: a Sentencia → if Expresi´n then Sentencias end o { Sentencia .arb:= NodoSi( Expresi´n .arb, Sentencias .arb)} o Sentencia → while Expresi´n do Sentencias end o { Sentencia .arb:= NodoMientras( Expresi´n .arb, Sentencias .arb)} o Sentencia → repeat Sentencias until Expresi´n ; o { Sentencia .arb:= NodoRepetir( Sentencias .arb, Expresi´n .arb)} o Sentencia → id:= Expresi´n ; o { Sentencia .arb:= NodoAsignaci´n(id.lexema, Expresi´n .arb)} o o Para la implementaci´n hay dos opciones principales: o Utilizar funciones (NodoSi, NodoMientras, . . . ) que devuelvan una estructura de datos ade- cuada. Utilizar objetos (NodoSi, NodoMientras, . . . ) para cada uno de los nodos. Probablemente, la segunda opci´n sea la m´s c´moda para el trabajo posterior con el ´rbol. o a o a En cuanto a la lista de sentencias, podemos utilizar nodos que tengan un grado variable, para eso almacenamos una lista con los hijos: Sentencias → {l:=λ} ( Sentencia {l:=l+ Sentencia .arb})∗ { Sentencias .arb:= NodoSentencias(l)} El tratamiento de las expresiones es sencillo. Por ejemplo, para la suma podemos hacer: Expresi´n o → T´rmino 1 {arb:= T´rmino 1 .arb} e e ( + T´rmino 2 {arb:=NodoSuma(arb, T´rmino 2 .arb)})∗ e e { Expresi´n .arb:= arb} o c Universitat Jaume I 2006-2007
  • 10. 10 II26 Procesadores de lenguaje Las restas, productos, etc, se tratar´ de forma similar. Puedes comprobar que de esta manera ıan el AST resultante respeta la asociatividad por la izquierda. Ejercicio* 6 En el caso de la asociatividad por la derecha tenemos dos opciones: crear una lista de operandos y recorrerla en orden inverso para construir el ´rbol o utilizar recursividad por la derecha, que a no da problemas para el an´lisis LL(1). Utiliza ambas posibilidades para escribir sendos esque- a mas de traducci´n que construyan los AST para expresiones formadas por sumas y potencias de o identificadores siguiendo las reglas habituales de prioridad y asociatividad. 3.2. Evaluaci´n de atributos sobre el AST o Un aspecto interesante del AST es que se puede utilizar para evaluar los atributos sobre ´l, ene lugar de sobre la gram´tica inicial. Si reflexionas sobre ello, es l´gico. Todo el proceso de evaluaci´n a o o de los atributos se puede ver como el etiquetado de un ´rbol (el ´rbol de an´lisis), pero no hay a a a nada que impida que el ´rbol sobre el que se realiza la evaluaci´n sea el AST. a o Un ejemplo ser´ el c´lculo de tipos. Si tenemos la expresi´n (2+3.5)*4, podemos calcular los ıa a o tipos sobre el ´rbol de an´lisis: a a Expresi´n o t: real T´rmino e t: real Factor * Factor t: real t: entero ( Expresi´n o ) num t: real v: 4 t: entero T´rmino e + T´rmino e t: entero t: real Factor Factor t: entero t: real num num v: 2 v: 3.5 t: entero t: real
  • 11. An´lisis sem´ntico a a 11 o ıamos utilizar una gram´tica similar a la siguiente1 : Para realizar esta evaluaci´n, podr´ a Expresi´n o → T´rmino 1 {t:= T´rmino 1 .t}(+ T´rmino 2 {t:= m´sgeneral(t, T´rmino 2 .t)})∗ e e e a e { Expresi´n .t:= t} o ... T´rmino e → Factor 1 {t:= Factor 1 .t}(* Factor 2 {t:= m´sgeneral(t, Factor 2 .t)})∗ a { T´rmino .t:= t} e ... Factor → num { Factor .t:= num.t} Factor → ( Expresi´n ) { Factor .t:= Expresi´n .t} o o ... Tambi´n podemos realizar el c´lculo sobre el AST: e a producto t: real suma num t: real v: 4 t: entero num num v: 2 v: 3.5 t: entero t: real Esto se har´ junto con el resto de comprobaciones sem´nticas. ıa a La elecci´n acerca de si evaluar atributos en el AST o durante el an´lisis descendente es b´si- o a a camente una cuesti´n de simplicidad. Generalmente, podemos decir que los atributos sintetizados o tienen una dificultad similar en ambos casos. Sin embargo, cuando la gram´tica ha sufrido trans- a formaciones o hay muchos atributos heredados, suele ser m´s f´cil evaluarlos sobre el AST. a a Otro aspecto interesante a la hora de decidir qu´ atributos evaluar sobre el ´rbol de an´lisis y e a a cu´les sobre el AST est´ en los propios nodos del ´rbol. Supongamos que queremos tener nodos a a a diferentes para la suma entera y la suma real. En este caso, tendremos que comprobar los tipos directamente sobre el ´rbol de an´lisis (o crear un AST y despu´s modificarlo, pero esto puede ser a a e forzarlo demasiado). 4. Comprobaciones sem´nticas a Los atributos nos permitir´n llevar a cabo las comprobaciones sem´nticas que necesite el len- a a guaje. En algunos casos, utilizaremos los atributos directamente (posiblemente evaluados sobre el AST), por ejemplo en la comprobaci´n que hac´ o ıamos de que el identificador al final de la funci´n o era el mismo que al principio. En otros casos, los atributos se utilizan indirectamente mediante estructuras globales, por ejemplo la tabla de s´ ımbolos. Un ejemplo ser´ la comprobaci´n de que un identificador no se ha ıa o declarado dos veces. Si hemos utilizado un nodo similar a: 1 Utilizamos la funci´n m´sgeneral que devuelve el tipo m´s general de los que se le pasan como par´metros (el o a a a tipo real se considera m´s general que el entero). a c Universitat Jaume I 2006-2007
  • 12. 12 II26 Procesadores de lenguaje declaraci´n o Tipo Listaids el m´todo de comprobaci´n ser´ e o ıa: Objeto NodoDeclaraci´n: o ... M´todo compsem´nticas() e a ... para i ∈ listaids hacer si TablaS´ımbolos.existe(i) entonces error si no TablaS´ ımbolos.inserta(i, tipo) fin si fin para ... fin compsem´nticas a ... fin NodoDeclaraci´no Utilizando directamente el esquema de traducci´n habr´ que duplicar parte del c´digo: o ıa o Declaraci´n o → Tipo { Listaids .t:= Tipo .t } Listaids Listaids → id, { Listaids 1 .t:= Listaids .t} Listaids 1 {si TablaS´ ımbolos.existe(id.lexema) entonces error si no TablaS´ ımbolos.inserta(id.lexema, Listaids .t)} Listaids → id {si TablaS´ ımbolos.existe(id.lexema) entonces error si no TablaS´ ımbolos.inserta(id.lexema, Listaids .t)} 4.1. La tabla de s´ ımbolos Durante la construcci´n del AST, las comprobaciones sem´nticas y, probablemente, durante la o a interpretaci´n y la generaci´n de c´digo necesitaremos obtener informaci´n asociada a los distintos o o o o identificadores presentes en el programa. La estructura de datos que permite almacenar y recuperar esta informaci´n es la tabla de s´ o ımbolos. En principio, la tabla debe ofrecer operaciones para: Insertar informaci´n relativa a un identificador. o Recuperar la informaci´n a partir del identificador. o La estructura que puede realizar estas operaciones de manera eficiente es la tabla hash (que en Python est´ disponible mediante los diccionarios). La implementaci´n habitual es una tabla a o que asocia cada identificador a informaci´n tal como su naturaleza (constante, variable, nombre de o funci´n, etc.), su tipo (entero, real, booleano, etc.), su valor (en el caso de constantes), su direcci´n o o (en el caso de variables y funciones), etc. Es importante tener unicamente una tabla; si tenemos varias, por ejemplo, una para constantes ´ y otra para variables, el acceso se hace m´s dif´ ya que para comprobar las propiedades de un a ıcil identificador hay que hacer varias consultas en lugar de una.
  • 13. An´lisis sem´ntico a a 13 Una cuesti´n importante es c´mo se relacionan la tabla y el AST. Tenemos distintas posibili- o o dades, que explicaremos sobre el siguiente ´rbol, correspondiente a la sentencia a= a+c: a asignaci´n o variable suma nombre: a tipo: entero variable variable nombre: a nombre: c La primera posibilidad es dejar el ´rbol tal cual y cada vez que necesitemos alguna informaci´n, a o por ejemplo el valor de a, consultar la tabla. Esta opci´n ser´ la m´s adecuada para situaciones o ıa a en las que se vaya a recorrer el ´rbol pocas veces, por ejemplo en una calculadora donde se eval´e a u cada expresi´n una sola vez. o Otra posibilidad es ir decorando cada una de las hojas con toda la informaci´n que se vaya o recopilando. Por ejemplo, si durante el an´lisis averiguamos el tipo y direcci´n de las variables, a o pasar´ıamos a almacenar la nueva informaci´n: o Tabla de s´ ımbolos asignaci´n o a: {tipo:entero, dir:1000} ... variable suma c: {tipo:entero, dir:1008} nombre: a tipo: entero tipo: entero ... dir: 1000 variable variable nombre: a nombre: c tipo: entero tipo: entero dir: 1000 dir: 1008 Esto tiene costes muy elevados en tiempo —tendremos que actualizar repetidamente un n´mero u de nodos que puede ser elevado— y espacio —hay mucha informaci´n que se almacena repetida—. o Podemos reducir estos costes guardando en cada nodo un puntero a la entrada correspondiente en la tabla: Tabla de s´ ımbolos a: {tipo:entero, dir:1000} asignaci´n o ... c: {tipo:entero, dir:1008} variable suma ... tipo: entero variable variable Finalmente, la opci´n m´s adecuada cuando tenemos lenguajes que presenten ´mbitos anidados o a a (veremos en otro tema c´mo representar los ´mbitos en la tabla) es guardar la informaci´n sobre o a o c Universitat Jaume I 2006-2007
  • 14. 14 II26 Procesadores de lenguaje los objetos en otra estructura de datos a la que apunta tanto la tabla como los nodos del ´rbol: a Informaci´n objetos o {tipo:entero, dir:1000} asignaci´n o ... {tipo:entero, dir:1008} variable suma ... tipo: entero Tabla de s´ ımbolos variable variable a c ... 4.2. Comprobaciones de tipos Un aspecto importante del an´lisis sem´ntico es la comprobaci´n de los tipos de las expresiones. a a o Esta comprobaci´n se hace con un doble objetivo: o Detectar posibles errores. Averiguar el operador o funci´n correcto en casos de sobrecarga y polimorfismo. o Para representar los tipos en el compilador, se emplean lo que se denominan expresiones de tipo (ET). La definici´n de estas expresiones es generalmente recursiva. Un ejemplo ser´ o ıa: Los tipos b´sicos: real, entero,. . . , adem´s de los tipos especiales error de tipo y ausencia de a a tipo (void) son ETs. Si n1 y n2 son enteros, rango(n1 , n2 ) es una ET. Si T es una ET, tambi´n lo es puntero(T ). e Si T1 y T2 son ETs, tambi´n lo es T1 → T2 . e Si T1 y T2 son ETs, tambi´n lo es vector(T1 , T2 ). e Si T1 y T2 son ETs, tambi´n lo es T1 × T2 . e Si T1 ,. . . ,Tk son ETs y N1 ,. . . ,Nk son nombres de campos, registro((N1 , T1 ), . . . , (Nk , Tk )) es una ET. Ten en cuenta que seg´n los lenguajes, estas definiciones cambian. Es m´s, los lenguajes pueden u a restringir qu´ expresiones realmente resultan en tipos v´lidos para los programas (por ejemplo, en e a Pascal no se puede definir un vector de funciones). Algunos ejemplos de declaraciones y sus expresiones correspondientes ser´ ıan: Declaraci´n o Expresi´n de tipo o int v[10]; vector(rango(0, 9),entero) struct { registro((a,entero), (b,real)) int a; float b; } st; int *p; puntero(entero) int f(int, char); entero × car´cter → entero a void f2(float); real → void
  • 15. An´lisis sem´ntico a a 15 4.2.1. Equivalencia de tipos Comprobar si dos expresiones de tipo, T1 y T2 , son equivalentes es muy sencillo. Si ambas corresponden a tipos elementales, son equivalentes si son iguales. En caso de que correspondan a tipos estructurados, hay que comprobar si son el mismo tipo y si los componentes son equivalentes. En muchos lenguajes se permite dar nombre a los tipos. Esto introduce una sutileza a la hora de comprobar la equivalencia. La cuesti´n es, dada una declaraci´n como o o typedef int a; typedef int b; ¿son equivalentes a y b? La respuesta depende del lenguaje (o, en casos como el Pascal, de la implementaci´n). Existen dos criterios para la equivalencia: o Equivalencia de nombre: dos expresiones de tipo con nombre son equivalentes si y s´lo si tienen o el mismo nombre. Equivalencia estructural: dos expresiones de tipo son equivalentes si y s´lo si tienen la misma o estructura. Hay argumentos a favor de uno y otro criterio. En cualquier caso, hay que tener en cuenta que para comprobar la equivalencia estructural ya no sirve el m´todo trivial presentado antes. En este e caso, puede haber ciclos, lo que obliga a utilizar algoritmos m´s complicados. a 4.2.2. Comprobaci´n de tipos en expresiones o Para comprobar los tipos en las expresiones podemos utilizar un atributo que indique el tipo de la expresi´n. Este tipo se infiere a partir de los distintos componentes de la expresi´n y de las o o reglas de tipos del lenguaje. A la hora de calcular el tipo hay varias opciones: podemos calcularlo sobre la gram´tica, sobre a el AST o al construir el AST. En cualquier caso, puede ser util tener una funci´n similar a la de ´ o la secci´n 3.2 que refleje las peculiaridades de los tipos del lenguaje. Por ejemplo, si tenemos tipos o entero y real con las reglas habituales de promoci´n, esta funci´n devolver´ los resultados seg´n o o ıa u esta tabla: Primero Segundo Resultado entero entero entero entero real real real entero real real real real otros error tipo El c´lculo de atributos de tipo deber´ ser trivial sobre la gram´tica o sobre el AST. En cuanto a ıa a al c´lculo al construir el AST, se puede, por ejemplo, hacer que el constructor compruebe los tipos a de los componentes que recibe. En este caso, si es necesario, se puede a˜adir un nodo de promoci´n n o de tipos. Por ejemplo, si vamos a crear un nodo suma a partir de los ´rboles: a producto suma t: entero t: real y , variable num variable num t: entero t: entero t: real t: real nombre: a valor: 3 nombre: z valor: 3.14 podemos a˜adir un nodo intermedio para indicar la promoci´n: n o c Universitat Jaume I 2006-2007
  • 16. 16 II26 Procesadores de lenguaje suma t: real enteroAreal suma t: real producto t: entero variable num t: real t: real nombre: z valor: 3.14 variable num t: entero t: entero nombre: a valor: 3 Respecto a la propagaci´n de los errores de tipo, hay que intentar evitar un exceso de mensajes o de error. Para ello se pueden utilizar distintas estrategias: Se puede hacer que error tipo sea compatible con cualquier otro tipo. Se puede intentar inferir cu´l ser´ el tipo en caso de no haber error. Por ejemplo: si el a ıa operador en que se ha detectado el error es el y-l´gico, se puede devolver como tipo el tipo o l´gico. o 5. Interpretaci´n o La utilizaci´n del AST hace que la interpretaci´n sea bastante sencilla. Una vez lo hemos o o construido, tenemos dos aproximaciones para la interpretaci´n: o Podemos representar la entrada mediante una serie de instrucciones similares a un lenguaje m´quina con algunas caracter´ a ısticas de alto nivel. Este es el caso, por ejemplo, de Java y el Java bytecode. Esta aproximaci´n representa pr´cticamente una compilaci´n. o a o La otra alternativa es representar la entrada de manera similar al AST y recorrerlo para ejecutar el programa. Utilizaremos la segunda opci´n. Los nodos correspondientes a las expresiones tendr´n un m´- o a e todo al que llamaremos eval´a y que devolver´ el resultado de evaluar el sub´rbol correspondiente. u a a Por ejemplo, en el nodo de la suma tendr´ ıamos: Objeto NodoSuma: ... M´todo eval´a() e u devuelve i.eval´a() + d.eval´a(); // i y d son los hijos del nodo u u fin eval´a u ... fin NodoSuma Aquellos nodos que representen sentencias tendr´n el m´todo interpreta. Por ejemplo, para un a e nodo que represente un mientras, el m´todo correspondiente ser´ e ıa: Objeto NodoMientras: ... M´todo interpreta() e mientras condici´n.eval´a()=cierto: o u sentencias.interpreta() fin mientras fin interpreta ... fin NodoMientras
  • 17. An´lisis sem´ntico a a 17 Las variables se representar´ mediante entradas en una tabla (que, en casos sencillos, puede ıan ser la tabla de s´ ımbolos) que contenga el valor correspondiente en cada momento. Una asignaci´n o consiste simplemente en evaluar la expresi´n correspondiente y cambiar la entrada de la tabla. o La ejecuci´n del programa consistir´ en una llamada al m´todo interpreta del nodo ra´ del o a e ız programa. Alternativamente, si hemos guardado el programa como una lista de sentencias, la ejecuci´n consistir´ en un bucle que recorra la lista haciendo las llamadas correspondientes. o a L´gicamente, existen otras maneras de recorrer el ´rbol. En particular, si no lo hemos represen- o a tado con objetos, podemos recorrerlo mediante funciones recursivas o de manera iterativa. En este ultimo caso, podemos emplear una pila auxiliar o ir “enhebrando” el ´rbol durante su construcci´n. ´ a o Esta es probablemente la opci´n m´s r´pida con una implementaci´n cuidadosa, pero es tambi´n o a a o e la que m´s esfuerzo exige (especialmente si hay que recorrer el ´rbol en distintos ´rdenes). a a o 6. Introducci´n a metacomp o Mediante metacomp se pueden traducir esquemas de traducci´n a programas Python que im- o plementan los correspondientes analizadores descendentes recursivos. Un programa metacomp se divide en varias secciones. Cada secci´n se separa de la siguiente o por un car´cter “ %” que ocupa la primera posici´n de una l´ a o ınea. La primera secci´n contiene la o especificaci´n del analizador l´xico. Las secciones pares contienen c´digo de usuario. La tercera y o e o sucesivas secciones impares contienen el esquema de traducci´n. Es decir, un programa metacomp o se escribe en un fichero de texto siguiendo este formato: Especificaci´n l´xica o e % C´digo de usuario o % Esquema de traducci´no % C´digo de usuario o % Esquema de traducci´no . . . Las secciones de “c´digo de usuario” se utilizan para declarar estructuras de datos y variables, o definir funciones e importar m´dulos utiles para el procesador de lenguaje que estamos especifican- o ´ do. Estos elementos deber´n codificarse en Python. La herramienta metacomp copia literalmente a estos fragmentos de c´digo en el programa que produce como salida. o 6.1. Especificaci´n l´xica o e La especificaci´n l´xica consiste en una serie de l´ o e ıneas con tres partes cada una: Un nombre de categor´ o la palabra None si la categor´ se omite. ıa ıa Un nombre de funci´n de tratamiento o None si no es necesario ning´n tratamiento. o u Una expresi´n regular. o Los analizadores l´xicos generados por metacomp dividen la entrada siguiendo la estrategia e avariciosa y, en caso de conflicto, prefiriendo las categor´ que aparecen en primer lugar. Por ıas cada lexema encontrado en la entrada se llama a la correspondiente funci´n de tratamiento, que o normalmente se limita a calcular alg´n atributo adicional a los tres que tienen por defecto todos u los componentes: lexema, nlinea y cat, que representan, respectivamente, el lexema, el n´mero de u l´ ınea y la etiqueta de categor´ del componente. ıa c Universitat Jaume I 2006-2007
  • 18. 18 II26 Procesadores de lenguaje Un ejemplo de especificaci´n l´xica para una calculadora sencilla ser´ o e ıa: categor´ ıa expresi´n regular o acciones atributos + num [0–9] calcular valor valor emitir sum [-+] copiar lexema lexema emitir abre ( emitir cierra ) emitir espacio [ t]+ omitir nl n emitir En metacomp, lo podemos escribir as´ ı: num valor [0-9]+ sum None [-+] abre None ( cierra None ) None None [ t]+ nl None n Donde valor ser´ una funci´n como la siguiente: ıa o def valor(componente): componente.v= int(componente.lexema) No necesitamos ning´n tratamiento para sum ya que metacomp a˜ade por defecto el atributo lexema u n (que es el que hemos usado para calcular el valor de los enteros). 6.2. Esquema de traducci´n o Las secciones de esquema de traducci´n contienen las reglas de la GPDR utilizando una sintaxis o muy similar a la de la asignatura. Cada regla tiene una parte izquierda, un s´ımbolo ->, una parte derecha y un punto y coma. Los no terminales se escriben marcados mediante < y >. Los terminales tienen que coincidir con los declarados en la especificaci´n l´xica2 . Se pueden emplear los opera- o e dores regulares de concatenaci´n (impl´ o ıcita), disyunci´n (mediante |), opcionalidad (mediante ?), o clausura (mediante *) y clausura positiva (mediante +). Por ejemplo, la regla E → T (sum T )∗ se escribir´ en metacomp as´ ıa ı: <E> -> <T> ( sum <T> )*; Las acciones de las reglas se escriben encerradas entre arrobas (@) y sin fin de l´ ınea entre ellas. Para poder referirse a los terminales y no terminales, estos est´n numerados por su orden a de aparici´n en la parte derecha de la regla. El no terminal de la izquierda no tiene n´mero y, o u si no hay ambig¨edad, la primera aparici´n de cada s´ u o ımbolo no necesita n´mero. Si tenemos las u acciones de la regla anterior: E → T 1 {v:= T 1 .v} (sum T 2 {si sum.lexema= “+” entonces v:= v+ T 2 .v si no v:= v− T 2 .v fin si})∗ { E .v:= v} podemos escribirlas en metacomp as´ ı: 2 Existe tambi´n la posibilidad de utilizar una sintaxis especial para categor´ e ıas con un s´lo lexema, pero no lo o comentaremos.
  • 19. An´lisis sem´ntico a a 19 <E> -> <T> @v= T.v@ ( sum <T> @if sum.lexema=="+":@ @ v+= T2.v@ @else:@ @ v-= T2.v@ )* @E.v= v@; 7. Algunas aplicaciones Vamos a aprovechar metacomp para reescribir los ejemplos que vimos en el tema de an´lisis a l´xico: la suma de las columnas de un fichero con campos separados por tabuladores y la lectura e de ficheros de configuraci´n. o 7.1. Ficheros organizados por columnas La representaci´n en metacomp de la especificaci´n l´xica podr´ ser: o o e ıa 1 numero valor [0-9]+ 2 separacion None t 3 nl None n 4 None None [ ]+ La funci´n valor se define en la zona de auxiliares junto con dos funciones para tratamiento o de errores: 5 % 6 def valor(componente): 7 componente.v= int(componente.lexema) 8 9 def error(linea, mensaje): 10 sys.stderr.write("Error en l´nea %d: %sn" % (linea, mensaje)) ı 11 sys.exit(1) 12 13 def error_lexico(linea, cadena): 14 if len(cadena)> 1: 15 error(linea, "no he podido analizar la cadena %s." % repr(cadena)) 16 else: 17 error(linea, "no he podido analizar el car´cter %s." % repr(cadena)) a 18 19 ncol= 0 La funci´n error la utilizamos para escribir mensajes gen´ricos. La funci´n error_lexico es o e o llamada por metacomp cuando encuentra un error l´xico. Los dos par´metros que recibe son la e a l´ ınea donde se ha producido el error y el car´cter o caracteres que no se han podido analizar. a Podemos emplear el siguiente esquema de traducci´n:o Fichero → {suma:=0}( L´ ınea .v})∗ { Fichero .suma:= suma} ınea nl {suma:= suma+ L´ L´ ınea → numero1 {global ncol; col:=1; si col= ncol entonces L´ ınea .v:= numero.v fin si} (separaci´n n´mero2 o u {col:= col+1; si col= ncol entonces L´ ınea .v:= numero2 .v fin si} )∗ c Universitat Jaume I 2006-2007
  • 20. 20 II26 Procesadores de lenguaje Hemos supuesto que el s´ ımbolo inicial tiene un atributo sintetizado que leer´ el entorno. Ade- a m´s, la columna deseada est´ en la variable global ncol. Representamos esto en metacomp as´ a a ı: 20 % 21 <Fichero>-> @suma= 0@ 22 ( <Linea> @suma+= Linea.v@ nl )* 23 @Fichero.suma= suma@ ; 24 25 <Fichero>-> error 26 @error(mc_al.linea(), "l´nea mal formada")@ ı 27 ; 28 29 <Linea>-> numero 30 @col= 1@ 31 @if col== ncol:@ 32 @ Linea.v= numero.v@ 33 ( separacion numero 34 @col+= 1@ 35 @if col== ncol:@ 36 @ Linea.v= numero2.v@ 37 )* 38 @if col< ncol:@ 39 @ error(numero.nlinea, "no hay suficientes columnas")@ 40 ; 41 La regla <Fichero>-> error nos permite capturar los errores que se produzcan durante el an´lisis. a Hay varias maneras de tratar los errores, pero hemos optado por abortar el programa. Hemos utilizado mc_al, que contiene el analizador l´xico, para averiguar el n´mero de l´ e u ınea del error. Finalmente, hemos decidido que el programa tenga un par´metro que indique qu´ columna a e sumar. Si no se especifica ning´n par´metro adicional, se leer´ la entrada est´ndar. En caso de que u a a a haya varios par´metros, se sumar´ la columna deseada de cada uno de los ficheros especificados, a a escribi´ndose el total. Con esto, el programa principal es: e 42 % 43 def main(): 44 global ncol 45 if len(sys.argv)== 1: 46 sys.stderr.write("Error, necesito un n´mero de columna.n") u 47 sys.exit(1) 48 try: 49 ncol= int(sys.argv[1]) 50 except: 51 sys.stderr.write("Error, n´mero de columna mal formado.n") u 52 sys.exit(1) 53 54 if len(sys.argv)== 2: 55 A= AnalizadorSintactico(sys.stdin) 56 suma= A.Fichero.suma 57 else: 58 suma= 0 59 for arg in sys.argv[2:]: 60 try: 61 f= open(arg) 62 except:
  • 21. An´lisis sem´ntico a a 21 63 sys.stderr.write("Error, no he podido abrir el fichero %s.n" % arg) 64 sys.exit(1) 65 A= AnalizadorSintactico(f) 66 suma+= A.Fichero.suma 67 print "La suma es %d." % suma Por defecto, metacomp genera una funci´n main que analiza sys.stdin. Si queremos modificar ese o comportamiento, debemos crear nuestra propia funci´n. Como ves, la mayor parte del c´digo se o o dedica a analizar posibles errores en los par´metros. La construcci´n m´s interesante est´ en las a o a a dos l´ ıneas que crean el analizador. La asignaci´n o A= AnalizadorSintactico(f) hace que se cree un analizador que inmediatamente analizar´ el fichero f (que debe estar abierto). a Cuando termina el an´lisis, el objeto A tiene un atributo con el nombre del s´ a ımbolo inicial y que tiene sus atributos sintetizados. As´ es como transmitimos el resultado. ı 7.2. Ficheros de configuraci´n o Vamos ahora a programar el m´dulo de lectura de los ficheros de configuraci´n. Haremos que o o el resultado del an´lisis sea un diccionario en el que las claves sean las variables y los valores a asociados sean los le´ıdos del fichero. El analizador l´xico se escribir´ as´ e ıa ı: 1 variable None [a-zA-Z][a-zA-Z0-9]* 2 igual None = 3 valor None [0-9]+|"[^"n]*" 4 nl None (#[^n]*)?n 5 None None [ t]+ Los unicos auxiliares que necesitamos son las funciones de tratamiento de errores: ´ 6 % 7 def error(linea, mensaje): 8 sys.stderr.write("Error en l´nea %d: %sn" % (linea, mensaje)) ı 9 sys.exit(1) 10 11 def error_lexico(linea, cadena): 12 if len(cadena)> 1: 13 error(linea, "no he podido analizar la cadena %s." % repr(cadena)) 14 else: 15 error(linea, "no he podido analizar el car´cter %s." % repr(cadena)) a Nuestro esquema de traducci´n ser´: o a Fichero → {dic:= {}}(( L´ ınea {dic[ L´ ınea .dcha}|λ) nl)∗ { Fichero .dic:= dic} ınea .izda]:= L´ L´ ınea → variable { L´ ınea .izda:= variable.lexema } igual valor { L´ ınea .dcha:= valor.lexema} Pasado a metacomp: 16 % 17 <Fichero>-> @dic= {}@ 18 ( (<Linea> @dic[Linea.izda]= Linea.dcha@)? nl )* 19 @Fichero.dic= dic@ ; 20 21 <Fichero>-> error c Universitat Jaume I 2006-2007
  • 22. 22 II26 Procesadores de lenguaje 22 @error(mc_al.linea(), "l´nea mal formada")@ ı 23 ; 24 25 <Linea>-> variable 26 @Linea.izda= variable.lexema@ 27 igual valor 28 @Linea.dcha= valor.lexema@ 29 ; 30 Como ves, hemos utilizado el operador de opcionalidad para representar la disyunci´n ( Linea |λ). o 8. Resumen del tema El analizador sem´ntico tiene dos objetivos: a • Hacer comprobaciones que no se hagan durante el an´lisis l´xico o sint´ctico. a e a • Crear una representaci´n adecuada para fases posteriores. o Implementaremos el an´lisis sem´ntico en dos partes: a a • Mediante esquemas de traducci´n dirigidos por la sintaxis. o • Recorriendo el AST. Un esquema de traducci´n dirigido por la sintaxis a˜ade a las gram´ticas: o n a • Acciones intercaladas en las partes derechas de las reglas. • Atributos asociados a los no terminales. Dos tipos de atributos: heredados y sintetizados. Las acciones deben garantizar que se eval´an correctamente los atributos. u Se pueden implementar los esquemas de traducci´n sobre los analizadores sint´cticos inter- o a pretando los atributos como par´metros y a˜adiendo el c´digo de las acciones al c´digo del a n o o analizador. El c´lculo de algunos atributos y algunas comprobaciones sem´nticas son m´s f´ciles sobre a a a a el AST. La tabla de s´ ımbolos se puede implementar eficientemente mediante una tabla hash. Las comprobaciones de tipos se complican si se introducen nombres. Dos criterios: • Equivalencia de nombre. • Equivalencia estructural. La interpretaci´n se puede realizar mediante recorridos del AST. o metacomp permite implementar c´modamente esquemas de traducci´n dirigidos por la sin- o o taxis.