Você está na página 1de 22

4o Ingenier a Inform atica

II26 Procesadores de lenguaje


An alisis sem antico Esquema del tema
1. Introducci on 2. Esquemas de traducci on dirigidos por la sintaxis 3. El arbol de sintaxis abstracta 4. Comprobaciones sem anticas 5. Interpretaci on 6. Introducci on a metacomp 7. Algunas aplicaciones 8. Resumen del tema

1.

Introducci on

Hay determinadas caracter sticas de los lenguajes de programaci on que no pueden ser modeladas mediante gram aticas incontextuales y que es necesario comprobar en una fase posterior al an alisis sint actico. Por otro lado, las fases posteriores de la compilaci on o interpretaci on necesitan una representaci on de la entrada que les permita llevar a cabo sus funciones de manera adecuada. Estas dos vertientes detecci on de errores y representaci on de la informaci on est an muy relacionadas y se solapan en la pr actica. Supongamos, por ejemplo, que nuestro lenguaje permite asignaciones seg un la regla Asignaci on id:= Expresi on ; Es habitual que se impongan ciertas restricciones. En nuestro caso, estas podr an ser: El identicador de la parte izquierda debe estar declarado previamente. El tipo de la expresi on debe ser compatible con el del identicador. El analizador sem antico deber a comprobar que estas dos restricciones se cumplen antes de declarar que la sentencia de asignaci on est a bien formada. Pero sucede que la informaci on necesaria para comprobarlas es u til tambi en para generar c odigo. Esto quiere decir que si tuvi eramos una separaci on estricta entre las fases del compilador, para generar c odigo deber amos volver a mirar el identicador para saber a qu e objeto (variable, funci on, constante, etc.) corresponde y qu e tipo tiene y tambi en deber amos volver a comprobar los tipos de la expresi on para generar el c odigo adecuado. Por otro lado, aunque en teor a el a rbol de an alisis ser a suciente para fases posteriores de la compilaci on o interpretaci on, es una representaci on que contiene mucha informaci on redundante. As , el arbol correspondiente a a:= b+c; bien podr a tener el aspecto del arbol de la izquierda, cuando el de la derecha contiene esencialmente la misma informaci on y resulta m as c omodo para

II26 Procesadores de lenguaje

trabajar: Asignaci on

ida :=

Expresi on

; asignaci on

Expresi on

T ermino variablea suma variableb constantec

T ermino

Factor

Factor

idc

idb El segundo arbol se conoce como arbol de sintaxis abstracta (o AST, de las iniciales en ingl es). Como hemos comentado, durante el an alisis sem antico se recoge una serie de informaciones que resultan de utilidad para fases posteriores. Estas informaciones se pueden almacenar en el arbol, decor andolo: asignaci on

variable
nombre: a tipo: entero

suma
tipo: entero

variable
nombre: b tipo: entero

constante
nombre: c tipo: entero valor: 5

As el objetivo de la fase de an alisis sem antico ser a doble: por un lado detectaremos errores que no se han detectado en fases previas y por otro lado obtendremos el AST decorado de la entrada. Para ello utilizaremos esquemas de traducci on dirigidos por la sintaxis, que permitir an asociar acciones a las reglas de la gram atica. Estas acciones realizar an comprobaciones y construir an el AST que despu es se recorrer a para terminar las comprobaciones y ser a la base para la interpretaci on o la generaci on de c odigo.

2.

Esquemas de traducci on dirigidos por la sintaxis

Nuestro objetivo es especicar una serie de acciones que se realizar an durante el an alisis de la entrada y tener un mecanismo que nos permita obtener la informaci on necesaria para realizar estas acciones. Para esto a nadimos a las gram aticas dos elementos nuevos: Acciones intercaladas en las reglas. Atributos asociados a los no terminales de la gram atica.

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 alisis sem antico

de forma sencilla si aumentamos nuestra gram atica incluyendo la acci on en la parte derecha de la 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 es analiza la segunda parte.

2.2.

Atributos asociados a los no terminales

Si nuestras acciones se limitaran a escribir mensajes que indicaran la fase del an alisis en la que nos encontramos, no necesitar amos mucho m as. En general, querremos que las acciones respondan al contenido de los programas que escribimos. Para ello, vamos a a nadir a los no terminales una serie de atributos. Estos no son m as que una generalizaci on de los atributos que tienen los terminales. Vamos a ver un ejemplo. Supongamos que en un lenguaje de programaci on se exige que en las subrutinas aparezca un identicador en la cabecera que debe coincidir con el que aparece al nal. Tenemos el siguiente fragmento de la gram atica del lenguaje: Subrutina Cabecera Fin Cabecera Cuerpo Fin subrutina id( Par ametros ); n id

En la regla correspondiente a Fin querremos comprobar que el identicador es el mismo que en la cabecera. Vamos a denir un atributo del no terminal Fin que ser a el que nos diga el nombre de la funci on: Fin n id {si id.lexema = Fin .nombre entonces error n si}

El atributo nombre es lo que se conoce como un atributo heredado : su valor se calcular a en las reglas en las que Fin aparezca en la parte derecha y se podr a utilizar en las reglas en las que Fin aparezca en la izquierda; ser a informaci on que heredar a de su entorno. F jate en c omo hemos empleado un atributo de id para hacer comprobaciones. El analizador sem antico es el que emplea la informaci on contenida en los atributos de los componentes l exicos. Recuerda que el sint actico s olo ve las etiquetas de las categor as. En cuanto a error, suponemos que representa las acciones necesarias para tratar el error. Ahora tenemos que completar las acciones de modo que Fin tenga alg un valor para el nombre. Podemos a nadir una acci on a la regla de Subrutina para calcular el valor de Fin .nombre: Subrutina Cabecera Cuerpo { Fin .nombre:= Cabecera .nombre} Fin

Lo que hacemos es copiar el atributo nombre que nos devolver a Cabecera . No debes confundir 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 a en las reglas en las que Cabecera aparezca en la parte izquierda y se utilizar a en las reglas donde Cabecera aparezca en la parte derecha; es informaci on que Cabecera sintetiza para su entorno. Debemos calcular el valor de nombre en la regla de Cabecera : Cabecera subrutina id( Par ametros ); { Cabecera .nombre:= id.lexema}

2.3.

Otros elementos de las acciones

Como habr as comprobado, no hemos denido ning un lenguaje formal para escribir las acciones. Asumiremos que se emplean construcciones corrientes de los algoritmos tales como condicionales
c Universitat Jaume I 2006-2007

II26 Procesadores de lenguaje

o bucles. Tambi en haremos referencia en ellos a subrutinas y a variables. Las variables las interpretaremos 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 n si} Valor 1 { Valor .v:= signo* Valor 1 .v}

no hay ninguna interferencia entre las distintas variables signo que se puedan utilizar en un momento dado. F jate tambi en en c omo hemos distinguido mediante un sub ndice las dos apariciones del no terminal Valor . Un recurso muy u til son los atributos que contienen listas. Por ejemplo, para una declaraci on de variables del tipo: DeclVariables ListaIds podemos hacer lo siguiente: DeclVariables ListaIds ListaIds ListaIds : Tipo {para v en ListaIds .l hacer: declara_var(v, Tipo .t) n para} ListaIds : Tipo

id, ListaIds |id

id, ListaIds 1 { ListaIds .l:= concat( [id.lexema], ListaIds 1 .l)} id { ListaIds .l:= [id.lexema]}

2.4.

Recopilaci on

Con lo que hemos visto, podemos decir que un esquema de traducci on consta de: Una gram atica incontextual que le sirve de soporte. 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 on de nuestra gram atica. Exigiremos que: 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 on haga referencia a los atributos sintetizados de los no terminales situados a su derecha en la producci on. Las dos u ltimas reglas nos permitir an integrar las acciones sem anticas en los analizadores descendentes recursivos. Cuando se emplean, se dice que el esquema de traducci on est a basado en una gram atica L-atribuida. La L indica que el an alisis y evaluaci on de los atributos se pueden hacer de izquierda a derecha.

An alisis sem antico

Ejercicio 1

Las siguientes reglas representan parte de las sentencias estructuradas de un lenguaje de programaci on: Programa Sentencias Sentencia Sentencia Sentencia Sentencia Sentencias Sentencia Sentencias | Sentencia

mientras Expresi on hacer Sentencias nmientras si Expresi on entonces Sentencias sino Sentencias nsi interrumpir otros

A nade las reglas necesarias para comprobar que la instrucci on interrumpir aparece u nicamente dentro de un bucle.
Ejercicio 2

Sea G la siguiente gram atica: A B C B C |a

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

A nade a G las reglas sem anticas necesarias para que el atributo ia de A contenga el n umero de aes al inicio de la cadena generada. Por ejemplo, dadas las cadenas aaaaba, abaaaa y aaa, los valores de ia ser an 4, 1 y 3, respectivamente. Puedes utilizar los atributos adicionales que consideres necesarios, pero ninguna variable global. Adem as, los atributos que a nadas deben ser de tipo entero o l ogico.

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 onde proceden. La respuesta var a seg un los textos. Los que se oponen, deenden que el signicado del programa debe depender u nicamente de el. Los que est an a favor de los atributos heredados, sostienen que permiten formalizar la entrada de informaci on acerca de, por ejemplo, el entorno donde se ejecutar a el programa o las opciones de compilaci on. Una situaci on similar se da con los s mbolos terminales: hay autores que piensan que no deber an tener atributos en absoluto; otros deenden que s olo deben tener atributos sintetizados; y los hay que opinan que pueden tener tanto atributos heredados como sintetizados. Otro aspecto sobre el que hay diferencias de interpretaci on es el de los efectos laterales de las reglas. En algunos textos, las reglas de evaluaci on de los atributos no pueden tener efectos laterales. Otros autores deenden que esta distinci on no es m as que un problema de implementaci on ya que es posible a nadir un nuevo atributo que represente el entorno e ir actualiz andolo adecuadamente. Esto u nicamente supone una incomodidad, pero no un problema formal. Esta es la posici on que seguiremos nosotros, permitiendo que haya efectos laterales en el c alculo de atributos. Quiz a los ejemplos m as claros de existencia de efectos laterales sean el manejo de la tabla de s mbolos y el control de errores. Cuando se encuentra una declaraci on en un programa es necesario actualizar la tabla de s mbolos de modo que sea posible reconocer ocurrencias posteriores del identicador. 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 on de c odigo. Una manera sencilla de marcar esta circunstancia es tener una variable
c Universitat Jaume I 2006-2007

II26 Procesadores de lenguaje

global de tipo l ogico que indique si se ha encontrado alg un error. Nuevamente, ser a posible, pero no necesario, tener un atributo que transporte esa informaci on.

2.6.

Eliminaci on de recursividad por la izquierda y atributos

En el tema de an alisis sint actico vimos c omo se pueden modicar las producciones con recursividad por la izquierda para lograr que la gram atica sea LL(1). El problema con las transformaciones es que dejan una gram atica que tiene muy poca relaci on con la original, lo que hace que escribir los atributos y las reglas correspondientes sea dif cil sobre la gram atica transformada. Sin embargo, se pueden escribir los atributos sobre la gram atica original y despu es convertirlos de una manera bastante mec anica. Como las GPDRs no suelen necesitar recursividad por la izquierda, es f acil que no tengas que utilizar esta transformaci on. Pese a todo, vamos a ver c omo se hace la transformaci on sobre un ejemplo. Partimos del siguiente esquema de traducci on: E E T E 1 + T { E .v:= E 1 .v+ T .v}

T { E .v:= T .v} num { T .v:= num.v}

Comenzamos por transformar la gram atica: E E E T T E

+ T E num

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

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

Con esto, el atributo h contendr a siempre la suma, que es el valor que tenemos que devolver nalmente. Por eso hay que transmitir el nuevo valor al atributo v: E E T { E .h:= T .v } E { E .v:= E .v}

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

Qu e hacemos cuando E se reescribe como la cadena vac a? En este caso, basta con devolver como sintetizado el valor que se hereda. El esquema de traducci on completo queda: E E E T
Ejercicio 3

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

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

Escribe el arbol de an alisis de 3+4 con el esquema original y el transformado. Dec oralos.

An alisis sem antico

Ejercicio 4

Transforma el siguiente esquema de traducci on para eliminar la recursividad por la izquierda: E E T T F F E 1 + T { E .v:= E 1 .v+ T .v} T { E .v:= T .v} T 1 * F { T .v:= T 1 .v* F .v}

F { T .v:= F .v} ( E ) { F .v:= E .v} num { F .v:= num.v}

El siguiente ejercicio presenta la transformaci on de una manera m as general.


Ejercicio* 5

Si tenemos las siguientes reglas en un esquema de traducci on A A A 1 Y { A .a:= g( A 1 .a, Y .y)} X { A .a:= f( X .x)}

podemos transformarlas en A A A X { A .h:= f( X .x)} A { A .a:= A .s} Y { A 1 .h:= g( A .h, Y .y)} A 1 { A .s:= A 1 .s}

{ A .s:= A .h}

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

2.7.

Implementaci on de los esquemas de traducci on

La interpretaci on de las acciones como sentencias que se ejecutan al pasar el an alisis por ellas permite implementar los esquemas de traducci on de manera sencilla. Para ello se modica la implementaci on del analizador recursivo descendente correspondiente a la gram atica original de la siguiente manera: Los atributos heredados del no terminal A se interpretan como par ametros de entrada de la funci on Analiza_A. Los atributos sintetizados del no terminal A se interpretan como par ametros de salida de la funci on Analiza_A. Las acciones sem anticas, una vez traducidas al lenguaje de programaci on correspondiente, se insertan en la posici on correspondiente seg un su orden en la parte derecha donde aparecen. En la pr actica, es frecuente que, si el lenguaje de programaci on (como C) no permite devolver m as de un valor, los atributos sintetizados del no terminal se pasen por referencia. En Python existe una soluci on bastante c omoda. Comenzamos por denir una clase vac a: class Atributos: pass Antes de llamar a una funci on o m etodo de an alisis, creamos un objeto de esta clase y le a nadimos los atributos heredados del no terminal. La correspondiente funci on de an alisis crear a los atributos sintetizados. Este es el u nico par ametro que se pasa. As , la traducci on de la regla:
c Universitat Jaume I 2006-2007

II26 Procesadores de lenguaje

E es la siguiente:

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

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 ogicamente, tendr amos que haber a nadido el c odigo de control de errores.

2.8.

Atributos en GPDR

La interpretaci on que hemos hecho de los esquemas de traducci on se traslada de forma natural a las GPDR. Por ejemplo, la traducci on de: E 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 a que a nadir el correspondiente c odigo para controlar errores. T 1 { E .v:= T 1 .v}(+ T 2 { E .v:= E .v+ T 2 .v})

3.

El arbol de sintaxis abstracta

Como hemos comentado en la introducci on, una de las posibles representaciones sem anticas de la entrada es el arbol de sintaxis abstracta o AST. Aunque es similar a los arboles de an alisis sint actico, tiene algunas diferencias importantes: No aparecen todos los componentes l exicos del programa. Por ejemplo: No es necesario incluir los par entesis de las expresiones. No se necesitan los separadores o terminadores de las sentencias. ... Pueden aparecer otros componentes no estrictamente sint acticos, como acciones de coerci on de tipos.

An alisis sem antico

3.1.

Construcci on

Para construir los arboles, debemos comenzar por denir qu e elementos emplearemos para cada estructura: Estructura Representaci on si if E then LS end E LS mientras while C do LS end C LS repetir repeat LS until C ; LS ... ... C ... E 1+ E 2 E1 ... E2 id:= E ; id E suma begin S 1 . . . S n end S 1 ... Sn asignaci on Estructura Representaci on sentencias

F jate que el arbol resultante puede representar programas en diversos lenguajes de programaci on del estilo C, Pascal, etc. Ahora debemos utilizar los atributos para construir el arbol. Utilizando el atributo arb para devolver el arbol que construye cada no terminal, podemos hacer algo parecido a: Sentencia Sentencia Sentencia Sentencia if Expresi on then Sentencias end { Sentencia .arb:= NodoSi( Expresi on .arb, Sentencias .arb)} while Expresi on do Sentencias end { Sentencia .arb:= NodoMientras( Expresi on .arb, Sentencias .arb)} repeat Sentencias until Expresi on ; { Sentencia .arb:= NodoRepetir( Sentencias .arb, Expresi on .arb)} id:= Expresi on ; { Sentencia .arb:= NodoAsignaci on(id.lexema, Expresi on .arb)}

Para la implementaci on hay dos opciones principales: Utilizar funciones (NodoSi, NodoMientras, . . . ) que devuelvan una estructura de datos adecuada. Utilizar objetos (NodoSi, NodoMientras, . . . ) para cada uno de los nodos. Probablemente, la segunda opci on sea la m as c omoda para el trabajo posterior con el arbol. 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 on T ermino 1 {arb:= T ermino 1 .arb} ( + T ermino 2 {arb:=NodoSuma(arb, T ermino 2 .arb)}) { Expresi on .arb:= arb}
c Universitat Jaume I 2006-2007

10

II26 Procesadores de lenguaje

Las restas, productos, etc, se tratar an de forma similar. Puedes comprobar que de esta manera 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 arbol o utilizar recursividad por la derecha, que no da problemas para el an alisis LL(1). Utiliza ambas posibilidades para escribir sendos esquemas de traducci on que construyan los AST para expresiones formadas por sumas y potencias de identicadores siguiendo las reglas habituales de prioridad y asociatividad.

3.2.

Evaluaci on de atributos sobre el AST

Un aspecto interesante del AST es que se puede utilizar para evaluar los atributos sobre el, en lugar de sobre la gram atica inicial. Si reexionas sobre ello, es l ogico. Todo el proceso de evaluaci on de los atributos se puede ver como el etiquetado de un arbol (el arbol de an alisis), pero no hay nada que impida que el arbol sobre el que se realiza la evaluaci on sea el AST. Un ejemplo ser a el c alculo de tipos. Si tenemos la expresi on (2+3.5)*4, podemos calcular los tipos sobre el arbol de an alisis:

Expresi on
t: real

T ermino
t: real

Factor
t: real

Factor
t: entero

Expresi on
t: real

num
v: 4 t: entero

T ermino
t: entero

T ermino
t: real

Factor
t: entero

Factor
t: real

num
v: 2 t: entero

num
v: 3.5 t: real

An alisis sem antico

11

Para realizar esta evaluaci on, podr amos utilizar una gram atica similar a la siguiente1 : Expresi on ... T ermino ... Factor Factor ... num { Factor .t:= num.t} ( Expresi on ) { Factor .t:= Expresi on .t} Factor 1 {t:= Factor 1 .t}(* Factor 2 {t:= m asgeneral(t, Factor 2 .t)}) { T ermino .t:= t} T ermino 1 {t:= T ermino 1 .t}(+ T ermino 2 {t:= m asgeneral(t, T ermino 2 .t)}) { Expresi on .t:= t}

Tambi en podemos realizar el c alculo sobre el AST: producto


t: real

suma
t: real

num
v: 4 t: entero

num

num

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

Esto se har a junto con el resto de comprobaciones sem anticas. La elecci on acerca de si evaluar atributos en el AST o durante el an alisis descendente es b asicamente una cuesti on de simplicidad. Generalmente, podemos decir que los atributos sintetizados tienen una dicultad similar en ambos casos. Sin embargo, cuando la gram atica ha sufrido transformaciones o hay muchos atributos heredados, suele ser m as f acil evaluarlos sobre el AST. Otro aspecto interesante a la hora de decidir qu e atributos evaluar sobre el arbol de an alisis y cu ales sobre el AST est a en los propios nodos del arbol. Supongamos que queremos tener nodos diferentes para la suma entera y la suma real. En este caso, tendremos que comprobar los tipos directamente sobre el arbol de an alisis (o crear un AST y despu es modicarlo, pero esto puede ser forzarlo demasiado).

4.

Comprobaciones sem anticas

Los atributos nos permitir an llevar a cabo las comprobaciones sem anticas que necesite el lenguaje. En algunos casos, utilizaremos los atributos directamente (posiblemente evaluados sobre el AST), por ejemplo en la comprobaci on que hac amos de que el identicador al nal de la funci on 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 a la comprobaci on de que un identicador no se ha declarado dos veces. Si hemos utilizado un nodo similar a:
1 Utilizamos la funci on m asgeneral que devuelve el tipo m as general de los que se le pasan como par ametros (el tipo real se considera m as general que el entero).

c Universitat Jaume I 2006-2007

12

II26 Procesadores de lenguaje

declaraci on

Tipo el m etodo de comprobaci on ser a:

Listaids

Objeto NodoDeclaraci on: ... M etodo compsem anticas() ... para i listaids hacer si TablaS mbolos.existe(i) entonces error si no TablaS mbolos.inserta(i, tipo) n si n para ... n compsem anticas ... n NodoDeclaraci on Utilizando directamente el esquema de traducci on habr a que duplicar parte del c odigo: Declaraci on Listaids Tipo { Listaids .t:= Tipo .t } 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 on del AST, las comprobaciones sem anticas y, probablemente, durante la interpretaci on y la generaci on de c odigo necesitaremos obtener informaci on asociada a los distintos identicadores presentes en el programa. La estructura de datos que permite almacenar y recuperar esta informaci on es la tabla de s mbolos. En principio, la tabla debe ofrecer operaciones para: Insertar informaci on relativa a un identicador. Recuperar la informaci on a partir del identicador. La estructura que puede realizar estas operaciones de manera eciente es la tabla hash (que en Python est a disponible mediante los diccionarios). La implementaci on habitual es una tabla que asocia cada identicador a informaci on tal como su naturaleza (constante, variable, nombre de funci on, etc.), su tipo (entero, real, booleano, etc.), su valor (en el caso de constantes), su direcci on (en el caso de variables y funciones), etc. Es importante tener u nicamente una tabla; si tenemos varias, por ejemplo, una para constantes y otra para variables, el acceso se hace m as dif cil ya que para comprobar las propiedades de un identicador hay que hacer varias consultas en lugar de una.

An alisis sem antico

13

Una cuesti on importante es c omo se relacionan la tabla y el AST. Tenemos distintas posibilidades, que explicaremos sobre el siguiente arbol, correspondiente a la sentencia a= a+c: asignaci on

variable
nombre: a

suma
tipo: entero

variable
nombre: a

variable
nombre: c

La primera posibilidad es dejar el arbol tal cual y cada vez que necesitemos alguna informaci on, por ejemplo el valor de a, consultar la tabla. Esta opci on ser a la m as adecuada para situaciones en las que se vaya a recorrer el arbol pocas veces, por ejemplo en una calculadora donde se eval ue cada expresi on una sola vez. Otra posibilidad es ir decorando cada una de las hojas con toda la informaci on que se vaya recopilando. Por ejemplo, si durante el an alisis averiguamos el tipo y direcci on de las variables, pasar amos a almacenar la nueva informaci on: Tabla de s mbolos asignaci on a: {tipo:entero, dir:1000} ... variable
nombre: a tipo: entero dir: 1000

suma
tipo: entero

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

variable
nombre: a tipo: entero dir: 1000

variable
nombre: c tipo: entero dir: 1008

Esto tiene costes muy elevados en tiempo tendremos que actualizar repetidamente un n umero de nodos que puede ser elevado y espacio hay mucha informaci on que se almacena repetida. 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 on ... c: {tipo:entero, dir:1008} variable suma
tipo: entero

...

variable variable

Finalmente, la opci on m as adecuada cuando tenemos lenguajes que presenten ambitos anidados (veremos en otro tema c omo representar los ambitos en la tabla) es guardar la informaci on sobre
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 arbol: Informaci on objetos {tipo:entero, dir:1000} asignaci on ... {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 alisis sem antico es la comprobaci on de los tipos de las expresiones. Esta comprobaci on se hace con un doble objetivo: Detectar posibles errores. Averiguar el operador o funci on correcto en casos de sobrecarga y polimorsmo. Para representar los tipos en el compilador, se emplean lo que se denominan expresiones de tipo (ET). La denici on de estas expresiones es generalmente recursiva. Un ejemplo ser a: Los tipos b asicos: real, entero,. . . , adem as de los tipos especiales error de tipo y ausencia de tipo (void ) son ETs. Si n1 y n2 son enteros, rango(n1 , n2 ) es una ET. Si T es una ET, tambi en lo es puntero(T ). Si T1 y T2 son ETs, tambi en lo es T1 T2 . Si T1 y T2 son ETs, tambi en lo es vector(T1 , T2 ). Si T1 y T2 son ETs, tambi en lo es T1 T2 . 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 un los lenguajes, estas deniciones cambian. Es m as, los lenguajes pueden restringir qu e expresiones realmente resultan en tipos v alidos para los programas (por ejemplo, en Pascal no se puede denir un vector de funciones). Algunos ejemplos de declaraciones y sus expresiones correspondientes ser an: Declaraci on int v[10]; struct { int a; float b; } st; int *p; int f(int, char); void f2(float); Expresi on de tipo vector(rango(0, 9),entero) registro((a,entero), (b,real))

puntero(entero) entero car acter entero real void

An alisis sem antico

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 on es, dada una declaraci on como 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 on). Existen dos criterios para la equivalencia: Equivalencia de nombre: dos expresiones de tipo con nombre son equivalentes si y s olo si tienen el mismo nombre. Equivalencia estructural: dos expresiones de tipo son equivalentes si y s olo si tienen la misma 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 etodo trivial presentado antes. En este caso, puede haber ciclos, lo que obliga a utilizar algoritmos m as complicados. 4.2.2. Comprobaci on de tipos en expresiones

Para comprobar los tipos en las expresiones podemos utilizar un atributo que indique el tipo de la expresi on. Este tipo se inere a partir de los distintos componentes de la expresi on y de las reglas de tipos del lenguaje. A la hora de calcular el tipo hay varias opciones: podemos calcularlo sobre la gram atica, sobre el AST o al construir el AST. En cualquier caso, puede ser u til tener una funci on similar a la de la secci on 3.2 que reeje las peculiaridades de los tipos del lenguaje. Por ejemplo, si tenemos tipos entero y real con las reglas habituales de promoci on, esta funci on devolver a los resultados seg un esta tabla: Primero entero entero real real Segundo Resultado entero real real real error tipo

entero real entero real otros

El c alculo de atributos de tipo deber a ser trivial sobre la gram atica o sobre el AST. En cuanto al c alculo al construir el AST, se puede, por ejemplo, hacer que el constructor compruebe los tipos de los componentes que recibe. En este caso, si es necesario, se puede a nadir un nodo de promoci on de tipos. Por ejemplo, si vamos a crear un nodo suma a partir de los arboles: producto
t: entero

suma
t: real

y variable num variable num


t: entero t: entero nombre: a valor: 3 t: real t: real nombre: z valor: 3.14

podemos a nadir un nodo intermedio para indicar la promoci on:


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 on de los errores de tipo, hay que intentar evitar un exceso de mensajes 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 al ser a el tipo en caso de no haber error. Por ejemplo: si el operador en que se ha detectado el error es el y-l ogico, se puede devolver como tipo el tipo l ogico.

5.

Interpretaci on

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

An alisis sem antico

17

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

6.

Introducci on a metacomp

Mediante metacomp se pueden traducir esquemas de traducci on a programas Python que implementan los correspondientes analizadores descendentes recursivos. Un programa metacomp se divide en varias secciones. Cada secci on se separa de la siguiente por un car acter % que ocupa la primera posici on de una l nea. La primera secci on contiene la especicaci on del analizador l exico. Las secciones pares contienen c odigo de usuario. La tercera y sucesivas secciones impares contienen el esquema de traducci on. Es decir, un programa metacomp se escribe en un chero de texto siguiendo este formato: Especicaci on l exica % C odigo de usuario % Esquema de traducci on % C odigo de usuario % Esquema de traducci on . . . Las secciones de c odigo de usuario se utilizan para declarar estructuras de datos y variables, denir funciones e importar m odulos u tiles para el procesador de lenguaje que estamos especicando. Estos elementos deber an codicarse en Python. La herramienta metacomp copia literalmente estos fragmentos de c odigo en el programa que produce como salida.

6.1.

Especicaci on l exica

La especicaci on l exica consiste en una serie de l neas con tres partes cada una: Un nombre de categor a o la palabra None si la categor a se omite. Un nombre de funci on de tratamiento o None si no es necesario ning un tratamiento. Una expresi on regular. Los analizadores l exicos generados por metacomp dividen la entrada siguiendo la estrategia avariciosa y, en caso de conicto, preriendo las categor as que aparecen en primer lugar. Por cada lexema encontrado en la entrada se llama a la correspondiente funci on de tratamiento, que normalmente se limita a calcular alg un atributo adicional a los tres que tienen por defecto todos los componentes: lexema, nlinea y cat, que representan, respectivamente, el lexema, el n umero de l nea y la etiqueta de categor a del componente.
c Universitat Jaume I 2006-2007

18

II26 Procesadores de lenguaje

Un ejemplo de especicaci on l exica para una calculadora sencilla ser a: categor a num sum abre cierra espacio nl expresi on regular [09] [-+] \( \) [ \t]+ \n
+

acciones calcular valor emitir copiar lexema emitir emitir emitir omitir emitir

atributos valor lexema

En metacomp, lo podemos escribir as : num sum abre cierra None nl valor [0-9]+ None [-+] None \( None \) None [ \t]+ None \n

Donde valor ser a una funci on como la siguiente: def valor(componente): componente.v= int(componente.lexema) No necesitamos ning un tratamiento para sum ya que metacomp a nade por defecto el atributo lexema (que es el que hemos usado para calcular el valor de los enteros).

6.2.

Esquema de traducci on

Las secciones de esquema de traducci on contienen las reglas de la GPDR utilizando una sintaxis 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 especicaci on l exica2 . Se pueden emplear los operadores regulares de concatenaci on (impl cita), disyunci on (mediante |), opcionalidad (mediante ?), clausura (mediante *) y clausura positiva (mediante +). Por ejemplo, la regla E se escribir a en metacomp as : <E> -> <T> ( sum <T> )*; Las acciones de las reglas se escriben encerradas entre arrobas (@) y sin n de l nea entre ellas. Para poder referirse a los terminales y no terminales, estos est an numerados por su orden de aparici on en la parte derecha de la regla. El no terminal de la izquierda no tiene n umero y, si no hay ambig uedad, la primera aparici on de cada s mbolo no necesita n umero. Si tenemos las 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 n si}) { E .v:= v} podemos escribirlas en metacomp as :
2 Existe tambi en la posibilidad de utilizar una sintaxis especial para categor as con un s olo lexema, pero no lo comentaremos.

T (sum T )

An alisis sem antico

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 alisis l exico: la suma de las columnas de un chero con campos separados por tabuladores y la lectura de cheros de conguraci on.

7.1.

Ficheros organizados por columnas

La representaci on en metacomp de la especicaci on l exica podr a ser:


1 2 3 4

numero separacion nl None

valor None None None

[0-9]+ \t \n [ ]+

La funci on valor se dene en la zona de auxiliares junto con dos funciones para tratamiento de errores:
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

% def valor(componente): componente.v= int(componente.lexema) def error(linea, mensaje): sys.stderr.write("Error en l nea %d: %s\n" % (linea, mensaje)) sys.exit(1) def error_lexico(linea, cadena): if len(cadena)> 1: error(linea, "no he podido analizar la cadena %s." % repr(cadena)) else: error(linea, "no he podido analizar el car acter %s." % repr(cadena)) ncol= 0

La funci on error la utilizamos para escribir mensajes gen ericos. La funci on error_lexico es llamada por metacomp cuando encuentra un error l exico. Los dos par ametros que recibe son la l nea donde se ha producido el error y el car acter o caracteres que no se han podido analizar. Podemos emplear el siguiente esquema de traducci on: Fichero L nea {suma:=0}( L nea nl {suma:= suma+ L nea .v}) { Fichero .suma:= suma}

numero1 {global ncol; col:=1; si col= ncol entonces L nea .v:= numero.v n si} (separaci on n umero2 {col:= col+1; si col= ncol entonces L nea .v:= numero2 .v n 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 a el entorno. Adem as, la columna deseada est a en la variable global ncol. Representamos esto en metacomp as :
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

% <Fichero>->

@suma= 0@ ( <Linea> @suma+= Linea.v@ nl )* @Fichero.suma= suma@ ;

<Fichero>-> error @error(mc_al.linea(), "l nea mal formada")@ ; <Linea>-> numero @col= 1@ @if col== ncol:@ @ Linea.v= numero.v@ ( separacion numero @col+= 1@ @if col== ncol:@ @ Linea.v= numero2.v@ )* @if col< ncol:@ @ error(numero.nlinea, "no hay suficientes columnas")@ ;

La regla <Fichero>-> error nos permite capturar los errores que se produzcan durante el an alisis. Hay varias maneras de tratar los errores, pero hemos optado por abortar el programa. Hemos utilizado mc_al, que contiene el analizador l exico, para averiguar el n umero de l nea del error. Finalmente, hemos decidido que el programa tenga un par ametro que indique qu e columna sumar. Si no se especica ning un par ametro adicional, se leer a la entrada est andar. En caso de que haya varios par ametros, se sumar a la columna deseada de cada uno de los cheros especicados, escribi endose el total. Con esto, el programa principal es:
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62

% def main(): global ncol if len(sys.argv)== 1: sys.stderr.write("Error, necesito un n umero de columna.\n") sys.exit(1) try: ncol= int(sys.argv[1]) except: sys.stderr.write("Error, n umero de columna mal formado.\n") sys.exit(1) if len(sys.argv)== 2: A= AnalizadorSintactico(sys.stdin) suma= A.Fichero.suma else: suma= 0 for arg in sys.argv[2:]: try: f= open(arg) except:

An alisis sem antico

21

63 64 65 66 67

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

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

7.2.

Ficheros de conguraci on

Vamos ahora a programar el m odulo de lectura de los cheros de conguraci on. Haremos que el resultado del an alisis sea un diccionario en el que las claves sean las variables y los valores asociados sean los le dos del chero. El analizador l exico se escribir a as :
1 2 3 4 5

variable igual valor nl None

None None None None None

[a-zA-Z][a-zA-Z0-9]* = [0-9]+|\"[^"\n]*\" (#[^\n]*)?\n [ \t]+

Los u nicos auxiliares que necesitamos son las funciones de tratamiento de errores:
6 7 8 9 10 11 12 13 14 15

% def error(linea, mensaje): sys.stderr.write("Error en l nea %d: %s\n" % (linea, mensaje)) sys.exit(1) def error_lexico(linea, cadena): if len(cadena)> 1: error(linea, "no he podido analizar la cadena %s." % repr(cadena)) else: error(linea, "no he podido analizar el car acter %s." % repr(cadena)) Nuestro esquema de traducci on ser a: Fichero L nea {dic:= {}}(( L nea {dic[ L nea .izda]:= L nea .dcha}|) nl) { Fichero .dic:= dic}

variable { L nea .izda:= variable.lexema } igual valor { L nea .dcha:= valor.lexema}

Pasado a metacomp:
16 17 18 19 20 21

% <Fichero>->

@dic= {}@ ( (<Linea> @dic[Linea.izda]= Linea.dcha@)? nl )* @Fichero.dic= dic@ ;

<Fichero>-> error
c Universitat Jaume I 2006-2007

22

II26 Procesadores de lenguaje

22 23 24 25 26 27 28 29 30

@error(mc_al.linea(), "l nea mal formada")@ ; <Linea>-> variable @Linea.izda= variable.lexema@ igual valor @Linea.dcha= valor.lexema@ ; Como ves, hemos utilizado el operador de opcionalidad para representar la disyunci on ( Linea |).

8.

Resumen del tema


El analizador sem antico tiene dos objetivos: Hacer comprobaciones que no se hagan durante el an alisis l exico o sint actico. Crear una representaci on adecuada para fases posteriores. Implementaremos el an alisis sem antico en dos partes: Mediante esquemas de traducci on dirigidos por la sintaxis. Recorriendo el AST. Un esquema de traducci on dirigido por la sintaxis a nade a las gram aticas: 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 uan correctamente los atributos. Se pueden implementar los esquemas de traducci on sobre los analizadores sint acticos interpretando los atributos como par ametros y a nadiendo el c odigo de las acciones al c odigo del analizador. El c alculo de algunos atributos y algunas comprobaciones sem anticas son m as f aciles sobre el AST. La tabla de s mbolos se puede implementar ecientemente mediante una tabla hash. Las comprobaciones de tipos se complican si se introducen nombres. Dos criterios: Equivalencia de nombre. Equivalencia estructural. La interpretaci on se puede realizar mediante recorridos del AST. metacomp permite implementar c omodamente esquemas de traducci on dirigidos por la sintaxis.

Você também pode gostar