Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Analisissemantico 110519213337 Phpapp01
Analisissemantico 110519213337 Phpapp01
Semntico en
Procesadores de
Lenguaje
Cuaderno N 38
Ingeniera Informtica
Cuaderno N 38
ANLISIS SEMNTICO EN PROCESADORES DE
LENGUAJE
Autorers:
Francisco Ortn Soler
Juan Manuel Cueva Lovelle
Maria Cndida Luengo Dez
Aquilino Adolfo Juan Fuente
Jos Emilio Labra Gayo
Ral Izquierdo Castanedo
Universidad de Oviedo - Espaa
Editorial:
SERVITEC
ISBN: 84-688-6208-8
Deposito Legal: AS-1358-04
PRLOGO
El objetivo de este libro es introducir los conceptos necesarios sobre la fase de anlisis semntico en procesadores de lenguaje, para un curso universitario de traductores,
compiladores e intrpretes: procesadores de lenguajes de programacin.
Est principalmente dirigido a alumnos de cuarto curso de Ingeniera Informtica,
aunque cualquier persona con conocimientos bsicos de teora de lenguajes y gramticas,
as como el conocimiento de algn lenguaje de programacin orientado a objetos como
Java o C++ est capacitado para seguir su contenido.
Para facilitar la labor docente del mismo, los conceptos introducidos han sido ilustrados con un conjunto importante de ejemplos. Asimismo, al final del libro se ha aadido
un captulo de cuestiones de revisin y otro de ejemplos propuestos. El objetivo principal
de estos dos puntos es fijar los conocimientos adquiridos y enfatizar los puntos ms importantes.
El libro se compone de los siguientes puntos:
Inicialmente se definen los conceptos bsicos a emplear a lo largo de todo el
texto.
El primer punto es una introduccin somera a la especificacin de la semntica
de lenguajes de programacin. Aunque la principal tarea de este texto es centrarnos en el anlisis semntico de lenguajes y no en su semntica, introduciremos este concepto por la relacin que posee con las gramticas atribuidas.
El segundo captulo es el que muestra el contexto del anlisis semntico dentro
del marco de los procesadores de lenguaje. Detalla los objetivos principales de
ste, as como la interaccin de esta fase con el resto.
El captulo 3 introduce el mecanismo ms empleado a la hora de definir analizadores semnticos de procesadores de lenguajes: las gramticas atribuidas (definiciones dirigida por sintaxis).
El siguiente captulo profundiza en las caractersticas ms importantes de las
gramticas atribuidas, empleadas para la implementacin de un evaluador.
El punto cuarto de este libro muestra cmo pueden evaluarse las gramticas
atribuidas. Se basa en los conceptos y clasificaciones expuestas en el captulo
anterior, ahondando en cmo, en funcin del tipo de gramtica atribuida, podremos implementar sta, empleando distintas tcnicas.
El captulo 6 detalla la parte principal de prcticamente la mayora de los analizadores semnticos: la comprobacin de tipos. Define los conceptos necesarios
e indica los objetivos y problemas que deber solventar un procesador de lenguaje.
Una vez concluidos los captulos, cuestiones y ejercicios propuestos, se presenta
un conjunto de apndices en los que se detalla el cdigo fuente empleado en los
CONTENIDO
Anlisis Semntico en Procesadores de Lenguaje .................................................................................. 1
1 Especificacin Semntica de Lenguajes de Programacin ..............................................................5
1.1. Especificacin Formal de Semntica ............................................................................................5
3 Gramticas Atribuidas...................................................................................................................... 21
3.1. Atributos....................................................................................................................................... 21
3.2. Reglas Semnticas....................................................................................................................... 22
3.3. Gramticas Atribuidas................................................................................................................. 23
3.4. Gramticas Atribuidas en Anlisis Semntico............................................................................ 28
6 Comprobacin de Tipos................................................................................................................... 75
6.1. Beneficios del Empleo de Tipos ................................................................................................. 76
Cuestiones de Revisin.........................................................................................................................117
Ejercicios Propuestos ...........................................................................................................................119
A Evaluacin de un AST.................................................................................................................... 125
A.1
Implementacin del AST ..................................................................................................... 125
ast.h ...................................................................................................................................................................... 125
A.2
Visitas del AST ..................................................................................................................... 129
visitor.h ................................................................................................................................................................ 129
visitorsemantico.h............................................................................................................................................... 133
visitorsemantico.cpp .......................................................................................................................................... 134
visitorgc.h ............................................................................................................................................................ 134
visitorgc.cpp ........................................................................................................................................................ 134
visitorcalculo.h .................................................................................................................................................... 135
visitorcalculo.cpp................................................................................................................................................ 135
visitormostrar.h................................................................................................................................................... 136
visitormostrar.cpp............................................................................................................................................... 136
A.3
Especificacin Lxica y Sintctica del Lenguaje ................................................................ 136
sintac.y.................................................................................................................................................................. 136
lexico.l .................................................................................................................................................................. 140
La sintaxis del lenguaje C indica que las expresiones se pueden formar con un conjunto de
operadores y un conjunto de elementos bsicos. Entre los operadores, con sintaxis binaria
infija, se encuentran la asignacin, el producto y la divisin. Entre los elementos bsicos de
una expresin existen los identificadores y las constantes enteras sin signo (entre otros).
Su semntica identifica que en el registro asociado al identificador superficie se le va a asociar el valor resultante del producto de los valores asociados a base y altura, divididos
por dos (la superficie de un tringulo).
Finalmente, el anlisis semntico del procesador de lenguaje, tras haber analizado correctamente que la sintaxis es vlida, deber comprobar que se satisfacen las siguientes condiciones:
Que todos los identificadores que aparecen en la expresin hayan sido declarados en el mbito actual, o en alguno de sus mbitos (bloques2) previos.
Que la subexpresin de la izquierda sea semnticamente vlida, es decir, que sea
un lvalue3.
Que a los tipos de los identificadores base y altura se les pueda aplicar el
operador de multiplicacin. Un registro en C, por ejemplo, no sera vlido.
todas las comprobaciones necesarias no llevadas a cabo por el analizador sintctico para
asegurarse de que el programa pertenece al lenguaje. Otra fase del compilador donde se
hace uso parcial de la semntica del lenguaje es en la optimizacin de cdigo, en la que analizando el significado de los programas previamente a su ejecucin, se pueden llevar a cabo
transformaciones en los mismos para ganar en eficiencia.
Existen dos formas de describir la semntica de un lenguaje de programacin: mediante especificacin informal o natural y formal.
La descripcin informal de un lenguaje de programacin es llevada a cabo mediante
el lenguaje natural. Esto hace que la especificacin sea inteligible (en principio) para cualquier persona. La experiencia nos dice que es una tarea muy compleja, si no imposible, el
describir todas las caractersticas de un lenguaje de programacin de un modo preciso.
Como caso particular, vase la especificacin del lenguaje ISO/ANSI C++ [ANSIC++].
La descripcin formal de la semntica de lenguajes de programacin es la descripcin rigurosa del significado o comportamiento de programas, lenguajes de programacin,
mquinas abstractas o incluso cualquier dispositivo hardware. La necesidad de hacer especificaciones formales de semntica surge para [Nielson92, Watt96, Labra03]:
Revelar posibles ambigedades existentes implementaciones de procesadores de
lenguajes o en documentos descriptivos de lenguajes de programacin.
Ser utilizados como base para la implementacin de procesadores de lenguaje.
Verificar propiedades de programas en relacin con pruebas de correccin o informacin relacionada con su ejecucin.
Disear nuevos lenguajes de programacin, permitiendo registrar decisiones
sobre construcciones particulares del lenguaje, as como permitir descubrir posibles irregularidades u omisiones.
Facilitar la comprensin de los lenguajes por parte del programador y como
mecanismo de comunicacin entre diseador del lenguaje, implementador y
programador. La especificacin semntica de un lenguaje, como documento de
referencia, aclara el comportamiento del lenguaje y sus diversas construcciones.
Estandarizar lenguajes mediante la publicacin de su semntica de un modo no
ambiguo. Los programas deben poder procesarse en otra implementacin de
procesador del mismo lenguaje exhibiendo el mismo comportamiento.
1.1. Especificacin Formal de Semntica
Si bien la especificacin formal de la sintaxis de un lenguaje se suele llevar a cabo
mediante la descripcin estndar de su gramtica en notacin BNF (Backus-Naur Form), en
el caso de la especificacin semntica la situacin no est tan clara; no hay ningn mtodo
estndar globalmente extendido.
El comportamiento de las distintas construcciones de un lenguaje de programacin,
puede ser descrito desde distintos puntos de vista. Una clasificacin de los principales m-
todos formales de descripcin semntica, as como una descripcin muy breve de las ideas
en las que se fundamentan, es [Nielson92, Labra01]:
Semntica operacional4: El significado de cada construccin sintctica es especificado mediante la computacin que se lleva a cabo en su ejecucin sobre
una mquina abstracta. Lo que realmente se especifica es cmo se lleva a cabo dicha ejecucin. Los significados del programa son descritos en trminos de operaciones, utilizando un lenguaje basado en reglas de inferencia lgicas en las que
se describen formalmente las secuencias de ejecucin de las diferentes instrucciones sobre una mquina abstracta [Nielson92]. Es muy cercano a la implementacin y se puede emplear para construir prototipos de procesadores de
lenguajes como la descripcin de PL/I en VDL [Lucas69].
Ejemplo 2. Lo siguiente es la especificacin formal de la semntica de una asignacin en un
lenguaje de programacin:
(e ) v
( x := e) {x a v}
Lo que se encuentra en la parte superior es una premisa y en la parte inferior una conclusin. La premisa indica que el resultado de evaluar una expresin e en un determinado
almacn (estado de una mquina abstracta) produce un valor v. La conclusin indica que,
dado un estado , la asignacin de una expresin e a un identificador x produce un nuevo
estado resultado de aadir a la asociacin del valor de v al identificador x.
Semntica denotacional5. La representacin del comportamiento de cada sentencia o frase del lenguaje se lleva a cabo mediante entidades matemticas (denotacin) que representan el efecto de haber ejecutado las sentencia o frase asociada [Watt96]. Por tanto, se hace ms hincapi en el efecto de la computacin
que en cmo se lleva a cabo. Se utiliza mayoritariamente en diseo de lenguajes
de programacin y se ha empleado para especificar la semntica completa de
lenguajes como Ada, Algol-60 y Pascal [Bjorner82]
Ejemplo 3. La especificacin de una asignacin en semntica denotacional es:
Operational semantics.
Denotational semantics, inicialmente denominada mathematical semantics.
6
La S viene de statement (sentencia).
7
Axiomatic semantics.
5
{2 = 2}x := 2{x = 2}
{n + 1 = 2}x := n + 1{x = 2}
{y 2 + 1 > 10}x = y * 2 + 1{x > 10}
Las especificaciones axiomticas tambin se denominan tripletas de Hoare en honor a su
creador. Como se muestra en el ejemplo, la derivacin de estas tripletas es llevada a cabo de
la postcondicin hacia la precondicin siguiendo un razonamiento hacia atrs.
Semntica algebraica8. Se basa en la especificacin de tipos de datos abstractos mediante una coleccin de operaciones (incluyendo alguna constante). Puesto que un conjunto de valores al que se le aaden una coleccin de operaciones
constituye un lgebra, este mtodo de descripcin formal de semntica se denomina semntica algebraica [Meinke92]. Este mtodo est pues enfocado a especificar la semntica de los tipos y sus operaciones. La semntica algebraica
constituye tambin la base de la semntica de acciones, empleada para especificar la semntica de lenguajes de programacin al completo.
Ejemplo 5. La especificacin del tipo lgico (booleano) en un lenguaje de programacin puede
llevarse a cabo del siguiente modo, siguiendo la semntica algebraica:
specification Truth-Values
sort Truth-Value
operations
true
: Truth-Value
false : Truth-Value
not_
: Truth-Value Truth-Value
__
: Truth-Value, Truth-Value Truth-Value
__
: Truth-Value, Truth-Value Truth-Value
variables t, u: Truth-Value
equtations
not true = false
not false = true
t true = t
t false = false
t u
= u t
t true = true
t false = t
t u
= u t
end specification
Algegraic semantics.
Action semantics.
Attribute grammar. Posiblemente, una mejor traduccin podra ser gramticas con atributos, pero la
amplia extensin del trmino gramtica atribuida hace que utilicemos la segunda traduccin.
10
Como comentbamos al comienzo de este libro, el anlisis semntico11 de un procesador de lenguaje es la fase encargada de detectar la validez semntica de las sentencias
aceptadas por el analizador sintctico. Tambin comentbamos cmo, de un modo convencional y menos formal, se suele afirmar que la sintaxis de un lenguaje de programacin
es aquella parte del lenguaje que puede ser descrita mediante una gramtica libre de contexto, mientras que el anlisis semntico es la parte de su especificacin que no puede ser descrita por la sintaxis [Scott00].
En el diagrama de fases de un compilador [Aho90] podemos destacar, a raz de las
dos definiciones previas, una mayor interconexin entre la fase de anlisis semntico y las
siguientes fases de un compilador:
Cdigo fuente
Analizador
Lxico
Tokens
Analizador
Sintctico
Tabla de
Smbolos
AST
Insercin y
Bsqueda de
Smbolos
Analizador
Semntico
Errores
Semnticos
Manejador
de Errores
AST decorado
Generador
de Cdigo
Intermedio
Cdigo Intermedio
Optimizador
de Cdigo
Cdigo Optimizado
Generador
de Cdigo
Cdigo objeto
11
12
Un compilador no suele generar el cdigo destino directamente a una plataforma especfica, sino que
utiliza un mecanismo intermedio de representacin de cdigo [Cueva98].
10
Para que la asignacin previa fuese correcta, los tres identificadores deberan de estar declarados. Puede que estn declarados en el mbito (bloque) actual o en uno menos
anidado que el actual, en cuyo caso el analizador sintctico tendra que aplicar reglas de
mbito como la ocultacin de identificadores.
Ejemplo 6. Si enmarcamos la sentencia anterior en el siguiente programa C:
#include <stdio.h>
int main() {
double base=2.5, altura=10;
{
double superficie, altura = 1;
superficie = base * altura / 2;
printf("%lf", superficie);
}
printf("%lf", superficie);
return 0;
}
El primer printf es correcto, pero no el segundo. En el primer caso, todas los identificadores de la asignacin estn declarados: superficie y altura (con valor 1) estn declarados en el mbito actual (altura oculta al identificador con valor 10, puesto que est ms
anidado) y el valor mostrado es 1.25. Sin embargo, el segundo printf no es correcto
puesto que la superficie del tringulo no ha sido declarada.
Comprobaciones de unicidad
Existen multitud de elementos en lenguajes de programacin cuyas entidades han
de existir de un modo nico, es decir, no se permite que estn duplicadas. Ejemplos tpicos
son:
Constantes de cada case en Pascal, C o Java. Cada uno de los elementos existentes en los condicionales mltiples de los lenguajes de programacin mencionados, ha de ser nico. En otro caso, el analizador semntico deber generar un
error de compilacin.
Los valores de un tipo enumerado de Pascal o C han de ser nicos.
Las etiquetas de un lenguaje de programacin, como un ensamblador, no pueden estar repetidas, puesto que los saltos a las mismas seran ambiguos.
La declaracin de un identificador en un mbito ha de ser nica en multitud de
lenguajes de programacin.
Comprobaciones de enlace13
En ocasiones, el empleo de un elemento de un lenguaje ha de estar ligado a una utilizacin previa del mismo:
En un ensamblador, un salto a una etiqueta requiere que sta haya sido referida
como una posicin de memoria.
En el lenguaje de programacin ANSI C [Kernighan91] y en ISO/ANSI C++
[ANSIC++] la invocacin a una funcin o mtodo requiere que stos hayan sido declarados previamente.
13
Binding.
11
La especificacin de mbitos para determinadas estructuras de control, en determinados lenguajes como BASIC, requiere la utilizacin pareja de palabras reservadas como IF / END IF, FOR ID / NEXT ID o SUB / END SUB.
Comprobaciones pospuestas por el analizador sintctico
A la hora de implementar un procesador de un lenguaje de programacin, es comn
encontrarse con situaciones en las que una gramtica libre de contexto puede representar
sintcticamente propiedades del lenguaje; sin embargo, la gramtica resultante es compleja y
difcil de procesar en la fase de anlisis sintctico. En estos casos es comn ver cmo el
desarrollador del compilador escribe una gramtica ms sencilla que no representa detalles
del lenguaje, aceptndolos como vlidos cuando realmente no pertenecen al lenguaje. En la
posterior fase de anlisis semntico ser donde se comprueben aquellas propiedades del
lenguaje que, por sencillez, no fueron verificadas por el analizador sintctico.
Hay multitud de escenarios de ejemplo y estn en funcin de la implementacin de
cada procesador. Sin embargo, los siguientes suelen ser comunes:
Es posible escribir una gramtica libre de contexto capaz de representar que toda implementacin de una funcin en C tenga al menos una sentencia return.
No obstante, si escribimos la gramtica de cualquier funcin como una repeticin de sentencias, siendo return es un tipo de sentencia, la gramtica es ms
sencilla y fcil de procesar. El analizador semntico deber comprobar, pues,
dicha restriccin.
Cuando un lenguaje posee el operador de asignacin como una expresin y no
como una sentencia (C y Java frente a Pascal), hay que comprobar que la expresin de la parte izquierda de la asignacin posee una direccin de memoria en la
que se pueda escribir (lvalue). Esta restriccin puede ser comprobada por el analizador semntico, permitiendo sintcticamente que cualquier expresin se encuentre en la parte izquierda del operador de asignacin.
Las sentencias break y continue de Java y C slo pueden utilizarse en determinadas estructuras de control del lenguaje. ste es otro escenario para que
el analizador sintctico posponga la comprobacin hasta la fase anlisis semntico.
Comprobaciones dinmicas
Todas las comprobaciones semnticas descritas en este punto suelen llevarse a cabo
en fase de compilacin y por ello reciben el nombre de estticas. Existen comprobaciones que, en su caso ms general, slo pueden ser llevadas a cabo en tiempo de ejecucin y
por ello se llaman dinmicas. stas suelen ser comprobadas por un intrprete o por cdigo de comprobacin generado por el compilador tambin puede darse el caso de que no
se comprueben. Diversos ejemplos pueden ser acceso a un vector fuera de rango, utilizacin de un puntero nulo o divisin por cero.
Analizaremos ms en detalle este tipo de comprobaciones en 6.5.
Comprobaciones de tipo
Sin duda, este tipo de comprobaciones es el ms exhaustivo y amplio en fase de
anlisis semntico. Ya bien sea de un modo esttico (en tiempo de compilacin), dinmico
(en tiempo de ejecucin) o en ambos, las comprobaciones de tipo son necesarias en todo
lenguaje de alto nivel. De un modo somero, el analizador semntico deber llevar a cabo las
dos siguientes tareas relacionadas con los tipos:
12
1. Comprobar las operaciones que se pueden aplicar a cada construccin del lenguaje. Dado un elemento del lenguaje, su tipo identifica las operaciones que sobre l se pueden aplicar. Por ejemplo, en el lenguaje Java el operador de producto no es aplicable a una referencia a un objeto. De un modo contrario, el operador punto s es vlido.
2. Inferir el tipo de cada construccin del lenguaje. Para poder implementar la
comprobacin anterior, es necesario conocer el tipo de toda construccin sintcticamente vlida del lenguaje. As, el analizador semntico deber aplicar las
distintas reglas de inferencia de tipos descritas en la especificacin del lenguaje
de programacin, para conocer el tipo de cada construccin del lenguaje.
La tarea de comprobar todas las restricciones de cada tipo y la inferencia de stos
ser ampliamente descrita en 0.
2.2. Anlisis Semntico como Decoracin del AST
Un procesador de lenguaje en el que todas las fases ocurren en un nico recorrido
del cdigo fuente se denomina de una pasada14. En este tipo de procesadores, el anlisis
semntico y la generacin de cdigo estn intercaladas con el anlisis sintctico y por tanto
con el anlisis lxico. Este tipo de compiladores es ms eficiente y emplea menos memoria
que los que requieren ms de una pasada. Sin embargo, el cdigo que genera acostumbra a
ser menos eficiente que los compiladores que emplean ms de una pasada. Pascal y C son
ejemplos de lenguajes que pueden ser compilados con una sola pasada por ello, es siempre
necesario declarar una funcin antes de utilizarla (forward en Pascal).
Existen lenguajes como Modula-2 o Java cuyas estructuras requieren ms de una
pasada para ser procesados se puede invocar a mtodos o funciones definidas posteriormente en el archivo. Asimismo, la mayora de los compiladores que optimizan el cdigo
generado (en su representacin intermedia o final) son de ms de una pasada, para poder
analizar y optimizar el cdigo15. Es importante resaltar que una pasada de un compilador es
un concepto distinto al de una fase. En una pasada se pueden llevar a cabo todas las fases
(si ste es de una pasada), o para una fase se pueden dar varias pasadas (como la optimizacin intensiva de cdigo). Las configuraciones intermedias son tambin comunes.
El anlisis semntico de un programa es ms sencillo de implementar si se emplean
para las fases de anlisis sintctico y semntico dos o ms pasadas16. En este caso, la fase de
anlisis sintctico crear un rbol sintctico abstracto para que sea procesado por el analizador semntico.
Si el procesador es de una pasada, el analizador sintctico ir llamando al analizador
semntico de un modo recursivo y, si bien ningn rbol es creado de forma explcita, los
mbitos de las invocaciones recursivas (o los niveles de la pila del reconocedor) formarn
implcitamente el rbol sintctico.
rbol de sintaxis abstracta
Como sabemos, un rbol sintctico17es una representacin de la estructura de una
consecucin de componentes lxicos (tokens), en la que stos aparecen como nodos hoja y
14
One-pass compier.
Existen compiladores que optimizan el cdigo de un modo intensivo, llegando a dar hasta 7 pasadas a
la representacin del programa [Louden97].
16
En ocasiones, como en el caso de Java o Modula2, es de hecho en nico modo de hacerlo.
17
Parse tree.
15
13
los nodos internos representan los pasos en las derivaciones de la gramtica asociada. Los
rboles sintcticos poseen mucha ms informacin de la necesaria para el resto de las fases
de un compilador, una vez finalizada la fase de anlisis sintctico.
Una simplificacin de rbol sintctico que represente toda la informacin necesaria
para el resto del procesamiento del programa de un modo ms eficiente que el rbol sintctico original, recibe el nombre de rbol de sintaxis abstracta (AST, Abstract Syntax Tree).
As, la salida generada por un analizador sintctico de varias pasadas, ser el AST representativo del programa de entrada.
Un AST puede ser visto como el rbol sintctico de una gramtica denominada abstracta, al igual que un rbol sintctico es la representacin de una gramtica (en ocasiones
denominada concreta). Por tanto, es comn ver una gramtica que representa una simplificacin de un lenguaje de programacin denominada gramtica abstracta del lenguaje.
Ejemplo 7. Lo siguiente es una gramtica (concreta) de una expresin, descrita en la notacin
propia de yacc/bison [Mason92]:
expresion: expresion '+' termino
| expresion '-' termino
| termino
;
termino: termino '*' factor
| termino '/' factor
| factor
;
factor: '-' factor
| '(' expresion ')'
| CTE_ENTERA
;
factor
*
(
expresin
expresin
trmino
factor
trmino
factor
CTE_ENTERA
(21)
factor
CTE_ENTERA
(32)
En el rbol anterior hay informacin (como los parntesis o los nodos intermedios factor y
trmino) que no es necesaria para el resto de fases del compilador. La utilizacin de un
AST simplificara el rbol anterior. Una posible implementacin sera siguiendo el siguiente
diagrama de clases:
14
Expresion
1
ExpresionUnaria
operador : char
operando : Expresion
ExpresionUnaria()
ConstanteEntera
valor : int
ConstanteEntera()
ExpresionBinaria
operador : char
operando1 : Expresion
operando2 : Expresion
ExpresionBinaria()
Su implementacin en C++ puede consultarse en el apndice A.1. Todos los nodos son
instancias derivadas de la clase abstracta expresin. De este modo, se podr trabajar con
cualquier expresin, independientemente de su aridad, mediante el uso de esta clase. Todas
las expresiones binarias sern instancias de la clase ExpresionBinaria, teniendo un
atributo que denote su operador. Del mismo modo, las expresiones con un operador unario (en nuestro ejemplo slo existe el menos unario) son instancias de ExpresionUnaria. Finalmente, las constantes enteras son modeladas con la clase ConstanteEntera.
Una implementacin yacc/bison que cree el AST a partir de un programa de entrada es:
%{
#include "ast.h"
int yyparse();
int yylex();
%}
%union {
int entero;
Expresion *expresion;
}
%token <entero> CTE_ENTERA
%type <expresion> expresion termino factor
%%
expresion: expresion '+' termino {$$=new ExpresionBinaria('+',$1,$3); }
| expresion '-' termino {$$=new ExpresionBinaria('-',$1,$3); }
| termino {$$=$1;}
;
termino: termino '*' factor {$$=new ExpresionBinaria('*',$1,$3); }
| termino '/' factor {$$=new ExpresionBinaria('/',$1,$3); }
| factor { $$=$1; }
;
factor: '-' factor { $$=new ExpresionUnaria('-',$2); }
| '(' expresion ')' { $$=$2; }
| CTE_ENTERA { $$=new ConstanteEntera($1); }
;
%%
As, ante la misma sentencia de entrada 3*(21+-32), el AST generado tendr la siguiente
estructura:
15
Expresin Binaria
(*)
Expresin Binaria
(+)
Constante Entera
(3)
Constante Entera
(21)
Expresin Unaria
(-)
Constante Entera
(32)
Ntese cmo ya no son necesarios los nodos de factor y trmino, puesto que el propio
rbol ya se crear con una estructura que refleje la informacin relativa a la precedencia de
operadores. De la misma forma, el procesamiento del AST en las siguientes fases es notablemente ms sencillo que el del rbol sintctico original.
La gramtica abstracta de nuestro ejemplo es:
expresion: expresionBinaria
| expresionUnaria
| constanteEntera
;
expresioBinaria: expresion ('*'|'/'|'+'|'-') expresion
;
expresionUnaria: '-' expresion
;
constanteEntera: CTE_ENTERA
;
Ejemplo 8. Considrese la siguiente gramtica libre de contexto, que representa una simplificacin de la sintaxis de una sentencia condicional en un lenguaje de programacin imperativo
los smbolos terminales se diferencian de los no terminales porque los primeros han sido
escritos en negrita:
sentencia
sentenciaIf
else
lectura
escritura
expresion
16
|
|
|
|
|
|
|
|
|
|
|
sentenciaIf
lectura
escritura
expresion
if ( expresion ) sentencia else
else sentencia
read expresion
write expresion
true
false
cte_entera
id
expresion + expresion
expresion expresion
expresion * expresion
expresion / expresion
expresion = expresion
La gramtica anterior acepta ms sentencias que las pertenecientes al lenguaje, como suele
suceder en la mayor parte de los casos. El analizador semntico debera restringir la validez
semntica de las sentencias teniendo en cuenta que la expresin de la estructura condicional
debe ser lgica (o entera en el caso de C), y que tanto la expresin a la izquierda del operador de asignacin como la expresin de la sentencia de lectura sean lvalues.
La siguiente sentencia es sintcticamente vlida:
if (true)
read a
else
write a=2+a*b
if
true read write
=
id
(a)
id
(a)
cte_entera *
(2)
id
(a)
id
(b)
Una vez que el analizador semntico obtenga el AST del analizador sintctico, ste deber utilizar el AST para llevar a cabo todas las
comprobaciones necesarias para verificar la validez semntica del programa de entrada.
Este proceso es realizado mediante lo que se conoce como decoracin o anotacin del
AST: asignacin de informacin adicional a los nodos del AST representando propiedades
de las construcciones sintcticas del lenguaje, tales como el tipo de una expresin. Un AST
decorado o anotado18 es una ampliacin del AST, en el que a cada nodo del mismo se le
aaden atributos indicando las propiedades necesarias de la construccin sintctica que
representan.
Ejemplo 9. Dada la gramtica libre de contexto:
S
declaracion
expresion
18
|
|
|
|
|
|
|
S declaracion ;
S expresion ;
int id
float id
id
cte_entera
cte_real
( expresion )
expresion + expresion
expresion expresion
17
|
|
|
expresion * expresion
expresion / expresion
expresion = expresion
Para implementar un analizador semntico deberemos decorar el AST con la siguiente informacin:
Las expresiones tendrn asociadas un atributo tipo que indique si son reales o enteras.
Esto es necesario porque se podr asignar un valor entero a una expresin real, pero no
al revs.
Las expresiones debern tener un atributo lgico que indique si son o no lvalues. De
este modo, se podr comprobar si lo que est a la izquierda de la asignacin es o no
semnticamente correcto.
En una declaracin se deber insertar el identificador en una tabla de smbolos con su
tipo declarado, para poder conocer posteriormente el tipo de cualquier identificador en
una expresin. Es, por tanto, necesario asignar un atributo nombre (cadena de caracteres) a un identificador.
Finalmente aunque ms enfocado a la fase de generacin de cdigo o interpretacin
que al anlisis semntico se le asigna un valor entero o real a las constantes del lenguaje.
Para el siguiente programa, un posible AST decorado es el mostrado en la Figura 6.
int a;
float b;
b=(a+1)*(b-8.3);
S
id
=
id
(nombre=a, (nombre=b, (tipo=real,
tipo=entero)
tipo=real) lvalue=true)
id
(nombre=b,
tipo=real,
lvalue=true)
+
(tipo=entero,
lvalue=false)
*
(tipo=real,
lvalue=false)
(tipo=real,
lvalue=false)
id
cte_entera
id
(nombre=b,
(valor=1,
(nombre=a,
tipo=real,
tipo=entero, tipo=entero,
lvalue=true)
lvalue=true) lvalue=false)
cte_real
(valor=8.3,
tipo=real,
lvalue=false)
Figura 6: AST decorado para llevar a cabo el anlisis semntico del programa analizado.
Ntese cmo el analizador semntico ser el encargado de decorar el AST como se muestra
en la Figura 6, adems de comprobar las restricciones mencionadas con anterioridad por
las que, precisamente, se ha decorado el rbol.
18
De este modo es ms sencillo llevar a cabo la tarea de anlisis semntico puesto que
toda la informacin sintctica est explcita en el AST, y el anlisis semntico tiene que limitarse a decorar el rbol y comprobar las reglas semnticas del lenguaje de programacin. En
el ejemplo anterior, utilizando la estructura del rbol se va infiriendo el tipo de cada una de
las expresiones. La informacin anotada a cada nodo del rbol ser empleada tambin por
las siguientes fases del procesador de lenguajes como la generacin de cdigo. Siguiendo
con el ejemplo, el conocer los tipos de cada una de las construcciones del AST facilita al
generador de cdigo saber el nmero de bytes que tiene que reservar, la instruccin de bajo
nivel que tiene que emplear, o incluso si es necesario o no convertir los operandos antes de
realizar la operacin.
El principal formalismo que existe para decorar rboles sintcticos es el concepto
de gramtica atribuida. Mediante gramticas atribuidas se implementan analizadores semnticos a partir del AST o del rbol sintctico. Tambin, partiendo de una gramtica atribuida,
existen mtodos de traduccin de stas a cdigo. Estos conceptos y tcnicas sern lo que
estudiaremos en los siguientes puntos.
19
Gramticas Atribuidas
Las gramticas libres de contexto son las elegidas comnmente para representar la
sintaxis de los lenguajes de programacin. stas representan cmo debe ser la estructura de
cualquier programa perteneciente al lenguaje que describen. Sin embargo, un procesador de
lenguaje necesita conocimiento adicional del significado de las construcciones para llevar a
cabo acciones en funcin de la fase en la que se encuentre.
A modo de ejemplo, consideremos la gramtica inicial de expresiones mostrada en
el Ejemplo 7. La gramtica representa la estructura de un tipo de expresiones aritmticas de
constantes enteras, teniendo en cuenta la precedencia comn de los operadores empleados.
En la fase de anlisis sintctico el reconocimiento de dicha estructura es suficiente, pero en
las fases posteriores hay que llevar a cabo el clculo de distintas parte de su semntica:
En la fase de anlisis semntico, para una ampliacin del ejemplo con diversos
tipos, habr que comprobar que la expresin satisface las reglas propias de los
tipos del lenguaje, calculando los tipos de cada subexpresin y analizando si las
operaciones aplicadas son vlidas para los tipos inferidos.
En la generacin de cdigo habr que ir traduciendo el programa de entrada a
otro programa con igual semntica, pero expresado en otro lenguaje de salida.
Si nos encontramos desarrollando un intrprete, en la fase de ejecucin necesitaremos conocer el valor de cada una de las expresiones. Esto mismo puede
darse si se est implementando un compilador y nos encontramos en la fase de
optimizacin de cdigo, en la que podemos reemplazar el clculo de una expresin con todos sus operandos constantes por su valor esta optimizacin recibe
el nombre de calculo previo de constantes19.
Para llevar a cabo las tareas previas, es comn emplear en el diseo e implementacin de un procesador de lenguaje gramticas atribuidas. Las gramticas atribuidas (o con
atributos) son ampliaciones de las gramticas libres de contexto que permiten especificar
semntica dirigida por sintaxis: la semntica de una sentencia de un lenguaje de programacin est directamente relacionada con su estructura sintctica y, por tanto, se suele representar mediante anotaciones de su rbol sintctico [Louden97]. Puesto que el uso de las
gramticas atribuidas es posterior a la fase de anlisis sintctico, es comn ver stas como
ampliacin de gramticas que especifiquen sintaxis abstracta y no concreta [Saraiva99] (
2.2).
3.1. Atributos
Un atributo es una propiedad de una construccin sintctica de un lenguaje. Los
atributos varan considerablemente en funcin de la informacin que contienen, de la fase
de procesamiento en la que se hallen, e incluso en si estn siendo calculados en fase de traduccin (empleados por los traductores y compiladores) o de ejecucin (empleados por los
19
Constant folding.
21
intrpretes). Tpicos ejemplos de atributos de una gramtica son: el tipo de una variable, el
valor de una expresin, la direccin de memoria de una variable o el cdigo objeto (destino) de una funcin.
El tiempo en el que los atributos mencionados son calculados es variable en funcin del lenguaje y procesador empleado. Pueden estticos si son calculados de un modo
previo a la ejecucin de la aplicacin, o dinmicos si su valor se obtiene en tiempo de ejecucin del programa. En el primer ejemplo mencionado el clculo del tipo de una variable, para lenguajes como C y Pascal, la inferencia de tipos es resuelta de un modo esttico
por el analizador semntico; en el caso de Lisp, la comprobacin relativa a los tipos es
siempre calculada en tiempo de ejecucin.
Si a es un atributo de un smbolo gramatical X, escribiremos X.a para referirnos al
valor del atributo a asociado al smbolo gramatical X. Para cada atributo, se debe especificar
su dominio: el conjunto de posibles valores que puede tomar20. Si en una produccin de la
gramtica libre de contexto aparece ms de una vez un smbolo gramatical X, entonces se
debe aadir un subndice a cada una de las apariciones para podernos referir a los valores
de los atributos de un modo no ambiguo: X 1 .a, X 2 .a, X 3 .a...
Ejemplo 10. Dado el smbolo no terminal expresion, podremos tener en un compilador los
siguientes atributos definidos sobre l:
Atributo
Descripcin
expresion.tipo
Entero, carcter, real, booleano, El tipo inferido para la exprearray, registro, puntero o fun- sin (anlisis semntico).
cin.
expresion.valor
El concepto de dominio viene de la especificacin semntica de lenguajes, en especial de la denotacional. En programacin, el concepto de dominio es traducido al concepto de tipo.
21
Attribute equation o semantic rule.
22
Gramticas Atribuidas
Una regla semntica asociada a la produccin p es una funcin matemtica que especifica las relaciones entre los valores de los atributos del siguiente modo [Aho90]:
a := f (a1 , a 2 , a3 ...a k )
Donde ai ,1 i k son atributos de los smbolos gramaticales de la produccin
( X j VT VN ,0 j n ) y a es:
pP
atributos, siendo R(p) el conjunto de reglas semnticas asociadas a p P y P el
conjunto de producciones de la gramtica libre de contexto G.
Como hemos definido en el punto 3.2, las reglas semnticas establecen relaciones
entre los valores de los atributos de una gramtica atribuida, expresadas mediante una funcin matemtica. Desde el punto de vista ms estricto de definicin de gramtica atribuida,
las reglas semnticas nicamente pueden recibir como parmetros otros atributos de la
gramtica (no estn permitidas las constantes, variables o llamadas a funciones que generen
efectos colaterales23) y devolver, sin generar un efecto colateral, un nico valor que ser
asignado a otro atributo de la produccin actual.
22
En los textos escritos en ingls, este conjunto es AI(X) ya que el atributo es inherited.
Una funcin que no posea efectos colaterales es aqulla que devuelve y genera siembre el mismo resultado al ser llamada con el mismo conjunto de argumentos. Una funcin que incremente una variable
global, por ejemplo, genera efectos laterales. Las funciones que no tienen efectos colaterales son meras
expresiones de clculo computacional sobre los argumentos pasados.
23
23
expresion1
termino1
factor1
|
|
|
|
|
|
expresion2 + termino
expresion2 - termino
termino
termino2 * factor
termino2 / factor
factor
- factor2
( expresion )
CTE_ENTERA
Ntese como, en caso de existir una repeticin de un smbolo gramatical en una misma
produccin, se ha roto la posible ambigedad con la adicin de subndices. Una gramtica
atribuida que sea capaz de evaluar el valor de las expresiones del lenguaje, necesaria en un
intrprete o para optimizar cdigo (vase Ejemplo 10), estar formada por GA={G, A, R},
donde A ser:
Atributo
Dominio
expresion.valor
Nmeros enteros
termino.valor
Nmeros enteros
factor.valor
Nmeros enteros
CTE_ENTERA.valor
Nmeros enteros
Finalmente, R ser:
P
(1)
(2)
(3)
(4)
(5)
(6)
(7)
24
R
expresion1.valor = expresion2.valor + termino.valor
expresion1.valor = expresion2.valor - termino.valor
expresion.valor = termino.valor
termino1.valor = termino2.valor * factor.valor
termino1.valor = termino2.valor / factor.valor
termino.valor = factor.valor
factor1.valor = factor2.valor
De hecho, desde la primera definicin de gramtica atribuida dada por Knuth [Knuth68] existe mltiple
bibliografa que define y emplea gramticas atribuidas variando estos dos parmetros en funcin del
objetivo buscado.
25
Syntax-directed definition.
26
Las producciones de las gramticas han sido numeradas para poder hacer referencia a ellas de un modo
ms sencillo.
24
Gramticas Atribuidas
P R
(8) factor.valor = expresion.valor
(9) factor.valor = CTE_ENTERA.valor
Hemos utilizado como metalenguaje el lenguaje de programacin C. En todos los casos los
atributos son sintetizados, puesto que el atributo que se calcula en toda regla semntica est
en la parte izquierda de su produccin asociada el atributo CTE_ENTERA.valor es sintetizado por el analizador lxico.
La gramtica atribuida se ha construido para que la evaluacin de los cuatro operadores sea
asociativa a izquierdas, de modo que la sentencia 1-1-2 poseer el valor -2 (y no 2).
Del mismo modo que las gramticas libres de contexto no especifican cmo deben
ser procesadas por el analizador sintctico (ascendente o descendentemente, en sus mltiples alternativas), una gramtica atribuida no especifica el orden en el que los atributos tienen que ser calculados. Las gramticas atribuidas son, pues, declarativas y no imperativas
[Wilhelm95]. Veremos cmo, al igual que existen clasificaciones de gramticas libres de
contexto en funcin de los algoritmos que las procesan (LL, SLL, LL(K), LR, SLR o
LALR) tambin existen gramticas atribuidas en funcin del orden de evaluacin de sus
atributos (L-atribuidas, S-atribuidas y bien definidas).
Las principales clasificaciones de gramticas atribuidas tienen en cuenta si los atributos calculados en las producciones son heredados o sintetizados. Es importante comprender la nocin de cada uno de ellos y cundo y cmo es necesario emplear uno u otro.
Supongamos una produccin p P de una gramtica libre de contexto:
X 0 X 1 X 2 ... X n , X 0 VN , X i VT VN ,1 i n
Los atributos sintetizados se calculan ascendentemente en el rbol sintctico: en
funcin de los atributos de los nodos hijos y asignndole un valor a un atributo sintetizado
del nodo padre (Figura 7). Por este motivo, se dice que en esa produccin el atributo se
sintetiza (podr ser empleado en producciones en las que el smbolo gramatical X 0 se encuentre en la parte derecha de la produccin).
A
Atributo
Sintetizado
Clculo del
atributo
Produccin
donde se
utilizar el
valor del
atributo
sintetizado
Produccin
donde se
calcula el
atributo
sintetizado
X0
Atributo
Heredado
Clculo del
atributo
Produccin
donde se
calcula el
atributo
heredado
Produccin
donde se
utilizar el
valor del
atributo
heredado
25
parte derecha de la produccin. Es por ello por lo que stos se consideran sintetizados por
el analizador lxico.
Ejemplo 12: Dada la siguiente gramtica libre de contexto:
(1)
(2)
(3)
(4)
(5)
declaracion
tipo
variables1
tipo variables ;
int
float
id , variables2
id
Supngase que tenemos un objeto ts (tabla de smbolos) que nos ofrece el mtodo
insertar para aadir a una estructura de datos el valor de un tipo asociado a una cadena. El objetivo es, mediante una definicin dirigida por sintaxis, insertar los identificadores
de la gramtica en la tabla de smbolos junto a su tipo apropiado. En un procesador de lenguaje real, esta informacin se necesitar para conocer el tipo de un identificador cuando
forme parte de una expresin.
Ntese cmo las producciones en las que realmente conocemos el identificador a insertar
en la tabla de smbolos son la cuarta y quinta. Sin embargo, el tipo que puedan tener asociado es conocido en las reglas 2 y 3. De este modo, una solucin es transmitir la informacin relativa al tipo hasta las reglas 4 y 5 para que stas inserten el identificador en la tabla
de smbolos:
P
(1)
(2)
(3)
(4)
(5)
R
variables.tipo = tipo.tipo
tipo.tipo = I
tipo.tipo = F
ts.insertar(id.valor,variables1.tipo)
variables2.tipo=variables1.tipo
ts.insertar(id.valor,variables.tipo)
El valor del tipo declarado se sintetiza en las reglas 2 o 3 y es pasado como heredado, por
medio de la regla 1, al no terminal variables. ste lo emplear para, junto al valor del
identificador (su nombre), insertarlo en la tabla de smbolos.
Es importante darse cuenta de que el valor que variables ha heredado en la regla 4,
deber asignrselo a la segunda aparicin del mismo no terminal, para que este proceso sea
llevado a cabo de un modo recursivo heredando siempre su valor. En la Figura 8 se muestra el rbol propio del siguiente programa de entrada: int a,b;
declaracion
tipo
variables
Hereda el
(tipo=I) valor de tipo (tipo=I)
Sintetiza el
valor de tipo
(regla 2)
(regla 1)
int
id
,
(valor=a,
ts.insertar(a, I) )
float
Hereda el
valor de tipo
(regla 4)
variables
(tipo=I)
id
(valor=b,
ts.insertar(b, I) )
26
Gramticas Atribuidas
int a,b; .
Si el valor del atributo tipo no es asignado al nodo variables situado en la parte inferior derecha por medio de la asignacin de la produccin cuarta, este atributo no tendra el
valor I. El resultado sera que no se insertara correctamente el identificador b en la tabla
de smbolos mediante la ltima regla semntica.
Si se trata de resolver el problema propuesto nicamente con atributos sintetizados, la solucin sera mucho ms compleja. Tendramos que ir guardando en un atributo sintetizado de
variables la lista de todos los identificadores declarados. En la primera produccin se
tendra que poner un bucle en el que cada uno de los identificadores de la lista de variables
sera insertado en la tabla de smbolos con el tipo sintetizado.
Si en lugar de una definicin dirigida por sintaxis tuvisemos que escribir una gramtica
atribuida, no se podra utilizar una estructura de datos auxiliar por los efectos colaterales
que se producen al insertar elementos. Debera definirse un atributo que fuese una lista de
los smbolos e ir pasando ste a lo largo de todos los no terminales del programa. En el
desarrollo real de un procesador de lenguaje esto producira un elevado acoplamiento y, por
lo tanto, se suele abordar con una definicin dirigida por sintaxis.
Ejemplo 13: Dada la siguiente gramtica libre de contexto:
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
expresion
masTerminos1
termino
masFactores1
factor
|
|
|
|
termino masTerminos
+ termino masTerminos2
- termino masTerminos2
factor masFactores
* factor masFactores2
/ factor masFactores2
CTE_ENTERA
Valor de expresion.valor
1-3-5
1+2*3
16/4/4
-7
7
1
La complejidad del problema radica en que los dos operandos de todas las expresiones
binarias se encuentran en producciones distintas. En el caso de la suma, el primer operando
aparece en la primera produccin, pero el segundo se encuentra en la produccin 2. El clculo del valor de la subexpresin ha de llevarse a cabo en la segunda produccin, como
suma del trmino de la primera produccin y el trmino de sta. Pero, cmo podemos
acceder en la segunda produccin al valor del primer operando? Mediante el empleo de un
atributo heredado:
(1)
masTerminos.operando1 = termino.valor
27
masTerminos.operando1 = termino.valor
expresion.valor = masTerminos.valor
masFactores.operando1 = factor.valor
termino.valor = masFactores.valor
factor.valor = CTE_ENTERA.valor
La primera regla semntica calcula la subexpresin con sus dos trminos y la convierte en el
primer operando de la siguiente subexpresin. Una vez que la siguiente subexpresin haya
evaluado su valor, el valor que retornamos (atributo masTerminos1.valor) es el valor
de la siguiente subexpresin (masTerminos2.valor). En el caso de que una subexpresin no posea segundo operando (y por tanto produzca el vaco), su valor es el valor de su
primer operando:
(4) masTerminos1.valor = masTerminos1.operando1
Gramticas Atribuidas
pP
B(p) la conjuncin de predicados que debern satisfacer los atributos de p
P, y P el conjunto de producciones de la gramtica libre de contexto G.
As, para que una sentencia pertenezca al lenguaje definido por la gramtica atribuida deber ser sintcticamente correcta (reconocida por la gramtica libre de contexto), y los
valores de los atributos debern, tras crearse el rbol y evaluarse (decorarse) ste, satisfacer
todas las restricciones especificadas en B.
Ejemplo 14: Considrese la siguiente gramtica que reconoce nmeros en base octal y decimal,
posponiendo para ello el carcter o o d a cada nmero, respectivamente27.
numero
base
digitos
digito
digitos base
o | d
digitos digito
digito
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Para calcular el valor real del nmero, debemos aadir a los no terminales base, digitos
y digito un atributo base que pueda tener los valores 8 y 10. El atributo valor poseer
el valor del no terminal asociado, en base decimal. Aadimos, pues, las reglas semnticas
para calcular este atributo:
P
(1) numero
(2) base
(3)
(4) digitos1
(5) digitos
(6) digito
(7)
(8)
(9)
(10)
(11)
(12)
(13)
(14)
(15)
R
digitos base numero.valor = digitos.valor
digitos.base = base.base
base.base = 8
o
| d
base.base = 10
digitos1.valor = digito.valor +
digitos2
digito
digitos2.valor * digitos1.base
digitos2.base = digitos1.base
digito.base = digitos1.base
digitos.valor = digito.valor
digito
digito.base = digitos.base
digito.valor = 0
0
| 1
digito.valor = 1
| 2
digito.valor = 2
| 3
digito.valor = 3
| 4
digito.valor = 4
| 5
digito.valor = 5
| 6
digito.valor = 6
| 7
digito.valor = 7
| 8
digito.valor = 8
| 9
digito.valor = 9
Con las reglas semnticas previas, la gramtica atribuida consigue que el atributo
numero.valor posea el valor del nmero en base decimal. Sin embargo, existen senten27
Podra tratarse de una gramtica atribuida para la fase de anlisis lxico. Existen herramientas que
utilizan estos tipos de gramticas para identificar los componentes lxicos del lenguaje a procesar
[ANTLR, JavaCC].
29
cias de este lenguaje que, aunque sean sintcticamente correctas, no lo son semnticamente.
Esta condicin se produce cuando un nmero octal posea algn dgito 8 o 9. Esta restriccin hace que las sentencias 185o y 109o no deban ser semnticamente correctas.
As, ampliaremos la gramtica atribuida a una cudrupla que posea, adems de gramtica,
atributos y reglas, un conjunto de predicados:
P
(14) digito
(15) digito
8
9
digito.base > 8
digito.base >= 10
Ntese cmo las nicas producciones que poseen restricciones semnticas son las dos ltimas, ya que es donde aparecen los dos dgitos no permitidos en los nmeros octales.
Adems se ha escrito el conjunto de rutinas semnticas de modo que el no terminal
digito posea el atributo heredado base, precisamente para poder comprobar que sta
sea superior a ocho.
Ejemplo 15: En el Ejemplo 9 se especificaba mediante una gramtica libre de contexto (G) el
siguiente lenguaje:
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
(10)
(11)
(12)
(13)
(14)
(15)
S
sentencias1
sentencias1
sentencias
declaracion
declaracion
expresion
expresion
expresion
expresion1
expresion1
expresion1
expresion1
expresion1
expresion1
sentencias
declaracion ; sentencias2
expresion ; sentencias2
int id
float id
id
cte_entera
cte_real
( expresion2 )
expresion2 + expresion3
expresion2 expresion3
expresion2 * expresion3
expresion2 / expresion3
expresion2 = expresion3
Del terminal id, el atributo id.valor es la cadena de caracteres que representa el identificador.
El atributo tipo es un carcter indicando el tipo del no terminal asociado: I
para el tipo entero y F para el real.
El atributo declaracion.id es un par (producto cartesiano) en el que el
primer valor es una cadena de caracteres (el valor del identificador) y el segundo
ser un carcter (el tipo asociado a dicho identificador).
30
Gramticas Atribuidas
(12)
(13)
(14)
(15)
R
sentencias.ids = nil
declaracion.ids = sentencias1.ids
sentencias2.ids = sentencias1.ids+declaracion.id
expresion.ids = sentencias1.ids
sentencias2.ids = sentencias1.ids
declaracion.id = (id.valor, I)
declaracion.id = (id.valor, F)
expresion.lvalue = true
expresion.tipo = expresion.ids[id.valor]
expresion.lvalue = false
expresion.tipo = I
expresion.lvalue = false
expresion.tipo = F
expresion2.ids = expresion1.ids
expresion1.lvalue = expresion2.lvalue
expresion1.tipo = expresion2.tipo
expresion2.ids = expresion1.ids
expresion3.ids = expresion1.ids
expresion1.lvalue = false
expresion1.tipo=mayorTipo(expresion2.tipo,expresion3.tipo)
expresion2.ids = expresion1.ids
expresion3.ids = expresion1.ids
expresion1.lvalue = false
expresion1.tipo=mayorTipo(expresion2.tipo,expresion3.tipo)
expresion2.ids = expresion1.ids
expresion3.ids = expresion1.ids
expresion1.lvalue = false
expresion1.tipo=mayorTipo(expresion2.tipo,expresion3.tipo)
expresion2.ids = expresion1.ids
expresion3.ids = expresion1.ids
expresion1.lvalue = false
expresion1.tipo=mayorTipo(expresion2.tipo,expresion3.tipo)
expresion2.ids = expresion1.ids
expresion3.ids = expresion1.ids
expresion1.lvalue = expresion2.lvalue
expresion1.tipo=expresion2.tipo
char mayorTipo(char t1,char t2) {
if ( t1 == F || t2 == F ) return F;
return I;
}
31
Una vez especificado en modo en el que se deben calcular cada uno de los atributos, limitaremos la sintaxis del lenguaje mediante el ltimo elemento de la gramtica atribuida: un
conjunto de predicados asociados a las producciones de la gramtica (B), que establece las
restricciones semnticas del lenguaje.
P
B
(5)
(6)
(7)
(15)
declaracion.ids[id.valor] == nil
declaracion.ids[id.valor] == nil
expresion.ids[id.valor] != nil
expresion2.lvalue
expresion2.tipo!=I || expresion3.tipo!=F
En este ejemplo se aprecia la principal diferencia entre gramtica atribuida y definicin dirigidas por sintaxis. Las primeras, al no poder generar efectos colaterales, han de representar
la tabla de smbolos como un atributo (ids) de varios smbolos no terminales. En las definiciones dirigidas por sintaxis, la tabla de smbolos es una estructura de datos global a la
que se accede desde las reglas semnticas. Adicionalmente, el mecanismo de deteccin de
errores se desarrolla con cdigo adicional dentro de las reglas semnticas, en lugar de especificarlo aparte en un conjunto de predicados (B). Las definiciones dirigidas por sintaxis son
menos formales, pero ms pragmticas.
32
Como hemos visto, las gramticas atribuidas ofrecen un modo de decorar o anotar
un rbol sintctico (concreto o abstracto) de un modo declarativo, sin identificar explcitamente el modo en el que deban ejecutarse las reglas semnticas en el caso de que realmente se puedan ejecutar. En funcin de las propiedades que cumpla una gramtica atribuida,
podremos decir si se puede evaluar cualquier rbol asociado a un programa de entrada e
incluso podremos determinar un orden especfico de ejecucin de las reglas semnticas.
Existen multitud de trabajos, bibliografa e investigacin relativa a gramticas atribuidas. De este modo, los distintos tipos de gramticas aqu presentados son lo ms representativos, existiendo otros tipos que no mencionamos.
4.1. Atributos Calculados en una Produccin
Los atributos calculados en una produccin p de la gramtica libre de contexto asociada a una gramtica atribuida son los que cumplan la siguiente condicin:
X VN (G ), AS ( X ) I AH ( X ) =
X VN (G ), AS ( X ) U AH ( X ) = A( X )
p P, p : X , AS ( X ) AC ( p)
p P, p : Y X , AH ( X ) AC ( p)
33
El significado real de que una gramtica atribuida sea completa es que haya sido escrita de un modo correcto. Pongamos un ejemplo con el caso de gramticas libres de contexto. Este tipo de gramticas se utiliza para describir lenguajes sintcticamente. Sin embargo, podemos escribir una gramtica sucia, en la que existan smbolos no terminales que no
tengan una produccin asociada (smbolos muertos) o producciones cuyo smbolo no terminal de la parte izquierda nunca se emplee en otra produccin (smbolos inaccesibles).
As, aunque una gramtica libre de contexto es un mecanismo para expresar la sintaxis de
un lenguaje de programacin, si escribimos una gramtica sucia no estaremos describiendo
ningn lenguaje.
La misma problemtica puede surgir cuando escribimos gramticas atribuidas. Podra darse el caso de que, siguiendo una notacin especfica, tratsemos de describir atributos de un programa mediante una gramtica atribuida. Si dicha gramtica no es completa,
no estaremos describiendo realmente evaluaciones de los atributos, puesto que cu clculo
no se podra llevar a cabo ante todos los programas de entrada. Por ello, la comprobacin
de que una gramtica atribuida sea completa es una verificacin de que se est expresando
correctamente la asignacin de valores a los atributos.
Ejemplo 16: Dada la gramtica atribuida del Ejemplo 13, en la que G era:
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
expresion
masTerminos1
termino
masFactores1
factor
|
|
|
|
termino masTerminos
+ termino masTerminos2
- termino masTerminos2
factor masFactores
* factor masFactores2
/ factor masFactores2
CTE_ENTERA
Los atributos calculados y, para cada caso, si son heredados o sintetizados en una produccin se muestra en la siguiente tabla:
Produccin
(1)
34
Atributos Calculados
masTerminos.operando1
expresion.valor
Produccin
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
Atributos Calculados
masTerminos.operando1
masTerminos.valor
masTerminos.operando1
masTerminos.valor
masTerminos.valor
masFactores.operando1
termino.valor
masFactores.operando1
masFactores.valor
masFactores.operando1
masFactores.valor
masFactores.valor
factor.valor
expresion
masTerminos
masTerminos
masTerminos
termino
masFactores
masFactores
masFactores
factor
Atributos Sintetizados
expresion.valor
masTerminos.valor
masTerminos.valor
masTerminos.valor
termino.valor
masFactores.valor
masFactores.valor
masFactores.valor
factor.valor
Calculados?
S
S
S
S
S
S
S
S
S
Finalmente, la ltima restriccin indica que, para todas las producciones, los atributos heredados de los smbolos gramaticales de la parte derecha debern ser calculados:
Produccin No Terminal Derecha
(1)
(2)
(3)
termino
masTerminos
termino
masTerminos
termino
masTerminos
Atributos Heredados
Calculados?
masTerminos.operando1
masTerminos.operando1
masTerminos.operando1
35
factor
masFactores
factor
masFactores
factor
masFactores
Atributos Heredados
Calculados?
masFactores.operando1
masFactores.operando1
masFactores.operando1
(8)
(9)
La gramtica anterior no es completa, ya que en la produccin 4 el no terminal de la izquierda posee un atributo sintetizado (masTerminos.valor) que no se calcula. No se
cumple, por tanto, la tercera condicin de gramtica completa.
Ntese cmo el resultado de que la gramtica no sea completa implica que ante determinados programas de entrada, no se puedan calcular los atributos del rbol. As, por ejemplo,
el programa de entrada 33, generara el siguiente rbol anotado:
36
expresion
(valor=?)
Regla 1.2
termino
(valor=33)
Regla 1.1
Regla 5.2
masTerminos
(operando1=33
valor=?)
Regla 5.1
factor
(valor=33)
masFactores
(operando1=33
valor=33)
Regla 9
Regla 8
CTE_ENTERA
(valor=33)
El hecho de que un atributo sea sintetizado, hace que su valor se calcule cuando el no terminal se encuentra en su parte izquierda. Al no haberse calculado en la cuarta produccin,
hace que este valor no est disponible en la segunda regla de la primera produccin. As, el
valor que obtendra la expresin sera desconocido. Para evitar estos errores surge el concepto de gramtica completa, obligando, entre otras cosas, a que todo atributo sintetizado
sea calculado cuando su no terminal aparezca a la izquierda de una produccin.
Ejemplo 18: Dada la siguiente gramtica atribuida:
(1)
(2)
S
A
A G
F G
(3)
(4)
(5)
(6)
TOKEN_A
TOKEN_B
TOKEN_C
G2 TOKEN_D
G1
28
37
coracin o anotacin del rbol sintctico ante un programa de entrada se denomina evaluacin de la gramtica [Aho90].
Para que una gramtica sea bien definida (no circular), deber ser posible encontrar
un modo de calcular la gramtica ante cualquier programa de entrada. Esto implica saber en
qu orden se han de ejecutar las reglas semnticas ante cualquier programa, para poder evaluar la gramtica29. As, una gramtica es completa si asigna correctamente valores a sus
atributos y ser bien definida si, adems, es posible encontrar un orden de ejecucin de sus
reglas semnticas ante cualquier programa de entrada. Por lo tanto, toda gramtica atribuida
bien definida es completa.
Existe un algoritmo para calcular si una gramtica est bien definida [Jazayeri75]. Su
principal inconveniente es que posee una complejidad computacional exponencial.
Ejemplo 19: La siguiente gramtica atribuida:
<S>
<A> <B>
<A>
<B>
A
B
A.a = B.b
B.b = A.a + 1
es una gramtica atribuida completa, puesto que los dos nicos atributos A.a y B.b son
heredados y en todas las producciones en las que A y B aparecen en la parte derecha, stos
son calculados. Sin embargo no est bien definida puesto que, dado el nico programa de
entrada vlido AB, es imposible evaluar la gramtica calcular el valor de los dos atributos.
X 0 X 1 X 2 ... X n , X 0 VN , X i VT VN ,1 i n
todos los atributos heredados de Xj, 1 jn, son calculados en funcin de:
Estudiaremos en mayor profundidad el clculo de orden de evaluacin de las reglas semnticas de una
gramtica atribuida en 1.
38
que la condicin se satisfaga para una definicin dirigida por sintaxis, se dice que sta es
con atributos por la izquierda [Aho90].
Si nos fijamos en la definicin de gramtica L-atribuida, nos daremos cuenta que
especifica una restriccin de los atributos heredados de la gramtica. Si una gramtica es Satribuida, no posee atributo heredado alguno y, por tanto, es tambin L-atribuida: toda
gramtica S-atribuida es tambin L-atribuida. Vemos pues cmo la expresividad de las segundas es superior a las de las primeras ya que se trata de un subconjunto.
Ejemplo 20: La gramtica atribuida completa presentada en el Ejemplo 13 y en el Ejemplo 16, es
una gramtica L-atribuida puesto que sus atributos heredados satisfacen la restriccin indicada:
P
(1) masTerminos.operando1
(2) masTerminos2.operando1
(3) masTerminos2.operando1
(5) masFactores.operando1
(6) masFactores.operando1
(7) masFactores.operando1
termino.valor
masTerminos1.operando1
termino.valor
masTerminos1.operando1
termino.valor
Depende de
Restriccin
1
2
1
2
1
factor.valor
masFactores1.operando1
factor.valor
masFactores1.operando1
factor.valor
1
2
1
2
1
En la tabla anterior se indican los atributos heredados calculados en cada una de las producciones. La tercera columna indica los valores empleados para realizar dicho clculo. Por
ltimo, la cuarta columna muestra si el atributo empleado para el clculo (el de la tercera
columna) es un heredado del no terminal de la izquierda (1 restriccin) o un atributo de un
smbolo gramatical de la parte derecha, situado a la izquierda del atributo calculado (2
restriccin).
Obviamente, al tener la gramtica atributos heredados, no es S-atribuida.
Ejemplo 21: La gramtica atribuida del Ejemplo 14:
P
(1) numero
(2) base
(3)
(4) digitos1
(5) digitos
(6) digito
(7)
(8)
(9)
(10)
(11)
R
digitos base numero.valor = digitos.valor
digitos.base = base.base
base.base = 8
o
| d
base.base = 10
digitos
digitos1.valor = digito.valor +
2
digito
digitos2.valor * digitos1.base
digitos2.base = digitos1.base
digito.base = digitos1.base
digito
digitos.valor
= digito.valor
digito.base = digitos.base
digito.valor = 0
0
| 1
digito.valor = 1
| 2
digito.valor = 2
| 3
digito.valor = 3
| 4
digito.valor = 4
| 5
digito.valor = 5
39
P
(12)
(13)
(14)
(15)
R
|
|
|
|
6
7
8
9
digito.valor
digito.valor
digito.valor
digito.valor
=
=
=
=
6
7
8
9
R
variables.tipo = tipo.tipo
tipo.tipo = I
tipo.tipo = F
ts.insertar(id.valor,variables1.tipo)
variables2.tipo = variables1.tipo
ts.insertar(id.valor,variables.tipo)
Esta definicin dirigida por sintaxis se puede traducir a otra equivalente (que reconozca el
mismo lenguaje e inserte la misma informacin en la tabla de smbolos) tan solo con el
empleo de atributos sintetizados:
P
declaracion variables
id ;
variables1
variables
2
id ,
| tipo
tipo
int
| float
R
ts.insertar(id.valor,variables.tipo)
variables1.tipo = variables2.tipo
ts.insertar(id.valor,variables2.tipo)
variables1.tipo = tipo.tipo
tipo.tipo = I
tipo.tipo = F
40
dar si, por ejemplo, no tuvisemos un evaluador de gramticas L-atribuidas, sino uno que
nicamente reconociese gramticas S-atribuidas como es el caso de yacc/bison [Mason92].
En ocasiones sucede que la estructura y evaluacin de una gramtica atribuida parece demasiado compleja. Es comn que se est dando el caso que no se haya escrito la gramtica de un modo adecuado, haciendo demasiado uso de atributos sintetizados. La reescritura de la misma empleando ms atributos heredados suele reducir la complejidad de la
gramtica, as como su evaluacin.
41
30
43
do en base al rbol sintctico y, por tanto, se suele representar superpuesto al subrbol sintctico correspondiente a la produccin.
Ejemplo 23. En el Ejemplo 15 presentamos una gramtica atribuida que comprobaba la validez
semntica de las expresiones de un pequeo lenguaje de programacin. Para la ltima produccin tenamos las siguientes reglas semnticas asociadas:
P
R
expresion2.ids = expresion1.ids
expresion1
expresion2 = expresion3 expresion3.ids = expresion1.ids
expresion1.lvalue = expresion2.lvalue
expresion1.tipo=expresion2.tipo
lvalue
tipo
expresion
ids
tipo
expresion ids
lvalue
tipo
expresion
ids
Figura 10: Grafo de dependencias locales para la produccin que define las asignaciones.
Ntese como las dependencias de los atributos, explicitadas en las reglas semnticas, estn
representadas mediante aristas dirigidas. Adems, cada atributo de un smbolo gramatical
est ubicado al lado de ste, superponiendo el grafo de dependencias al subrbol sintctico
(en tono gris) correspondiente a la produccin asociada.
Dado un programa de entrada perteneciente al lenguaje descrito por la gramtica
atribuida, su grafo de dependencias es la unin de los grafos de dependencias locales de
las producciones empleadas para reconocer el programa, representando cada nodo del rbol sintctico creado [Louden97].
Ejemplo 24. Empleando la misma gramtica atribuida del Ejemplo 15, el siguiente programa:
int i;
float r;
r=i*0.5;
dencias:
44
S
ids sentencias
;
ids declaracion id
ids sentencias
;
int id valor
float
ids declaracion id
id valor
ids sentencias
ids sentencias
id valor
id
valor
cte_real
Figura 11: Grafo de dependencias para el clculo de atributos, ante el programa especificado.
Sobre el rbol sintctico se ha ubicado todo atributo asociado a los smbolos gramaticales.
En cada una de las derivaciones del rbol (agrupacin de un nodo y sus hijos) generada por
la correspondiente produccin de la gramtica, se representa el grafo de dependencias locales. El grafo resultante es el grafo de dependencias obtenido para el programa de entrada.
A modo de ejemplo, centrmonos en la produccin sptima (expresion id) y sus
reglas semnticas asociadas. Esta produccin tiene, en el programa de ejemplo, dos derivaciones y, por tanto, dos subrboles asociados los dos en los que aparece id como nodo
hoja y expresion como su nodo padre. Las dos reglas semnticas asociadas a esta produccin son:
expresion.lvalue = true
expresion.tipo = expresion.ids[id.valor]
La primera no denota ninguna dependencia, puesto que puede ejecutarse sin requerir la
ejecucin de ninguna otra regla. Sin embargo, la segunda revela una dependencia del atributo expresion.tipo respecto a los dos atributos expresion.ids e id.valor. Por
este motivo, se aprecia en el grafo de dependencia para los dos subrboles que representan las dos derivaciones una arista dirigida que representa las dependencias indicadas.
Si nos encontramos describiendo una definicin dirigida por sintaxis, puede darse el
caso de que una regla semntica sea la invocacin a un procedimiento o a algn mtodo
que no devuelva ningn valor y que, por tanto, dicha regla no sea empleada para asignar un
valor a un atributo. En este caso, para calcular el grafo de dependencias debe inventarse un
45
atributo ficticio para el no terminal del lado izquierdo de la produccin, haciendo que dependa de los parmetros pasados al procedimiento.
Ejemplo 25. Recordemos la definicin dirigida por sintaxis presentada en el Ejemplo 12:
P
declaracion tipo
variables ;
tipo
int
tipo
float
variables1
id ,
variables2
variables
id
R
variables.tipo = tipo.tipo
tipo.tipo = I
tipo.tipo = F
ts.insertar(id.valor,variables1.tipo)
variables2.tipo = variables1.tipo
ts.insertar(id.valor,variables.tipo)
La problemtica comentada surge en las reglas semnticas en las que se inserta un identificador en la tabla de smbolos. Al permitir los efectos colaterales de insercin de datos en
una estructura global en lugar de representar sta mediante atributos de la gramtica, nos
encontramos con la problemtica de que, para ejecutar la rutina de la ltima produccin:
ts.insertar(id.valor,variables.tipo)
tipo tipo
float
tipo variables
valor id
tipo variables
valor id
Figura 12: Grafo de dependencias para la entrada float a,b;.
46
cular un nodo (atributo) del grafo de dependencias antes de evaluar los atributos dependientes de ste. Una ordenacin de los nodos de un grafo de dependencias que cumpla
dicha restriccin se denomina ordenamiento topolgico del grafo.
Un ordenamiento topolgico de un grafo de dependencias es todo ordenamiento
m1 , m2 ,..., mk de los nodos del grafo tal que las aristas vayan desde los nodos que aparecen
primero en el orden a los que parecen ms tarde; es decir, si mi m j es una arista desde
mi a m j , entonces mi aparece antes que m j en el orden topolgico [Aho90]. Todo ordenamiento topolgico de un grafo de dependencias establece un orden vlido en el que se
pueden evaluar las reglas semnticas asociadas a los nodos del rbol sintctico.
La condicin necesaria y suficiente para que exista al menos un ordenamiento topolgico para un grafo de dependencias es que el grafo sea acclico. Este tipo de grafos recibe
el nombre de grafos acclicos dirigidos (DAG, Directed Acyclic Graphs). Una gramtica atribuida
no circular (bien definida) es aquella para la que cualquier posible grafo de dependencias es
acclico de ah la importancia de crear gramticas atribuidas no circulares, para que pueda
evaluarse sta ante cualquier programa de entrada.
Ejemplo 26. A raz del grafo de dependencias del Ejemplo 25, podemos establecer un ordenamiento topolgico de los nodos del mismo. Para ello, numeramos los atributos conforme a
la dependencia que puedan tener entre s:
declaracion
tipo 3 tipo
float
4 tipo variables 5
2 valor id
6tipo variables7
1 valor id
Figura 13: Numeracin de los nodos del grafo de dependencias, estableciendo un ordenamiento topolgico sobre el mismo.
Los nmeros iniciales son asignados a los nodos que no poseen ninguna dependencia de
otro nodo. Una vez hecho esto, se numeran los nodos consecutivamente conforme dependan de otros nodos que posean un valor menor. Pueden existir distintas numeraciones.
Un ordenamiento topolgico de los nodos del grafo establece un orden vlido en el que se
pueden evaluar las reglas semnticas asociadas a los nodos del rbol. De este modo, el orden de ejecucin de las reglas semnticas ser el indicado por los nodos del grafo. Si un
nodo del grafo corresponde a un smbolo terminal, no habr que calcular su atributo puesto que ste ya fue sintetizado por el analizador lxico.
Nmero Atributo
de nodo
1
id.valor
Produccin
Lxico
47
Nmero Atributo
de nodo
Produccin
id.valor
Analizador Lxico
Lxico
tipo.tipo
tipo.tipo = F
variables.tipo
variables.tipo = tipo.tipo
ts.insertar(id.valor,
variables1.tipo)
varirables.tipo
variables2.tipo =
variables1.tipo
ts.insertar(id.valor,
variables.tipo)
Puede, pues, convertirse el programa declarativo especificado con la definicin dirigida por
sintaxis en un programa imperativo que, empleando el rbol sintctico como principal estructura de datos, ejecute la siguiente secuencia de sentencias31:
tipo.tipo3 = F
variables.tipo4 = tipo.tipo3
ts.insertar(id.valor2,variables.tipo4)
variables.tipo6 = variables.tipo4
ts.insertar(id.valor1,variables.tipo6)
Ejemplo 27. En el Ejemplo 14 se present la siguiente gramtica atribuida que evaluaba el valor
y la correccin semntica de nmeros decimales y octales:
P
(1) numero
(2) base
(3)
(4) digitos1
(5) digitos
(6) digito
(7)
(8)
(9)
(10)
(11)
(12)
(13)
(14)
(15)
R
digitos base numero.valor = digitos.valor
digitos.base = base.base
base.base = 8
o
| d
base.base = 10
digitos
digitos1.valor = digito.valor +
2
digito
digitos2.valor * digitos1.base
digitos2.base = digitos1.base
digito.base = digitos1.base
digito
digitos.valor
= digito.valor
digito.base = digitos.base
digito.valor = 0
0
| 1
digito.valor = 1
| 2
digito.valor = 2
| 3
digito.valor = 3
| 4
digito.valor = 4
| 5
digito.valor = 5
| 6
digito.valor = 6
| 7
digito.valor = 7
| 8
digito.valor = 8
| 9
digito.valor = 9
Cabra preguntarnos cul sera un orden de evaluacin de los atributos ante la entrada de la
sentencia 71o. Su grafo de dependencias con un ordenamiento topolgico vlido del mismo es:
31
A los atributos de los nodos se les ha aadido el subndice de su ordenacin topolgica, para evitar
ambigedades entres las distintas ocurrencias (instancias) de cada atributo.
48
10valor numero
3base base
Tras el ordenamiento identificado con la numeracin de los nodos del grafo, podemos
concluir que una secuencia vlida de ejecucin de las reglas semnticas es la siguiente:
digito.valor1 = 7
digito.valor2 = 1
base.base3 = 8
base.base4 = base.base3
digito.base5 = base.base4
digitos.base6 = base.base4
digito.base7 = digitos.base6
digitos.valor8 = digito.valor1
digitos.valor9 = digito.valor2+digitos.valor8*digitos.base6
numero.valor10 = digitos.valor9
Ntese cmo, tras la ejecucin de las sentencias previas, el valor del atributo
digito.valor es igual a 57 valor de 71 en octal.
Evaluacin de una gramtica atribuida
Los pasos necesarios para evaluar una gramtica atribuida se pueden precisar como
sigue. Se utiliza la gramtica libre de contexto subyacente para construir, a partir del programa de entrada, su rbol sintctico (o su AST en funcin de si la gramtica es concreta o
abstracta). Se construye un grafo de dependencias para el programa de entrada. Se establece
un orden topolgico de evaluacin de las reglas semnticas de la gramtica atribuida. Se
ejecutan las reglas semnticas siguiendo el ordenamiento calculado, traduciendo as la gramtica atribuida a un programa imperativo que trabaja sobre una estructura de datos: el
rbol sintctico. Recordemos que para que esto pueda efectuarse ante cualquier programa
de entrada, la condicin necesaria y suficiente es que la gramtica sea no circular y, por
tanto, todo grafo de dependencias sea acclico.
Para llevar a cabo el proceso de evaluacin de una gramtica atribuida existen principalmente dos mtodos [Louden97, Aho90]:
32
49
buida, cuya ejecucin es de complejidad exponencial [Jazayeri75]33. Adicionalmente, la creacin del grafo de dependencias y la obtencin de un ordenamiento topolgico cada vez que se procesa un programa, supone una complejidad
adicional.
Existen herramientas que implementan este mtodo generando, tras escribir la
gramtica con una sintaxis determinada, un evaluador de la misma si sta no es
circular; ejemplos de estas herramientas son FNC-2 [FNC2], Ox [Bischoff92],
Elegant [Jansen93] o lrc [LRC].
Puesto que sta es una condicin de la gramtica atribuida, la computacin slo debera llevarse a cabo
una nica vez tras la escritura de la gramtica atribuida y no cada vez que se procese un programa de
entrada.
34
Rule-based methods.
35
La L (left) indica que el programa de entrada es analizado de izquierda a derecha. Dada una sentencia
de entrada, ste es el orden en el que el analizador lxico le pasa los tokens al analizador sintctico.
50
una restriccin en la evaluacin, puesto que la utilizacin de los atributos de los elementos
terminales de la gramtica debe seguir este mismo orden.
Evaluacin ascendente de gramticas S-atribuidas
Toda gramtica S-atribuida se puede evaluar ascendentemente, calculando los atributos todos ellos sintetizados conforme el programa de entrada es analizado [Aho90]. El
analizador sintctico deber almacenar en su pila los valores de los atributos sintetizados
asociados a cada uno de los smbolos gramaticales. En el momento en el que se lleve a cabo
una reduccin, se calcularn los valores de los nuevos atributos sintetizados (del no terminal de la izquierda de la produccin) en funcin de los atributos que estn en la pila (de los
no terminales de la parte derecha, entre otros). Este mecanismo es el llevado a cabo por la
herramienta yacc/bison.
El poder clasificar una gramtica atribuida como S-atribuida ofrece, por tanto, dos
beneficios frente al concepto general de gramtica atribuida:
1. Sin necesidad de calcular el grafo de dependencias, se conoce a priori el orden
de ejecucin de las reglas semnticas. No es necesario ordenar topolgicamente
el grafo (ni siquiera crearlo) para conocer su orden de evaluacin.
2. Slo es necesario visitar una vez cada nodo del rbol sintctico creado ante la
entrada de un programa36. Una vez ejecutada la regla semntica asociada al nodo del rbol, no necesitar volver a procesar ste. Esta propiedad conlleva una
clara mejora de eficiencia en tiempo de compilacin.
Evaluacin descendente de gramticas L-atribuidas
Dada una gramtica atribuida que cumpla las condiciones de gramtica Latribuida37, podr ser evaluada mediante un analizador sintctico descendente recursivo:
36
Aunque en los procesadores de una pasada el rbol no se crea explcitamente, ste s existe, representndose cada uno de sus nodos como un contexto en la pila del reconocedor.
37
O una definicin dirigida por sintaxis con atributos por la izquierda.
38
La gramtica deber ser descendente y acorde con el algoritmo de anlisis sintctico, es decir, gramtica LL1 si el algoritmo tiene un lookahead 1 o LL(k) si el algoritmo permite un k determinado.
51
P
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
expresion
masTerminos1
termino
masFactores1
factor
|
|
|
|
termino masTerminos
+ termino masTerminos2
- termino masTerminos2
factor masFactores
* factor masFactores2
/ factor masFactores2
CTE_ENTERA
R
(1) masTerminos.operando1 = termino.valor
expresion.valor = masTerminos.valor
(2) masTerminos2.operando1=masTerminos1.operando1+termino.valor
masTerminos1.valor = masTerminos2.valor
(3) masTerminos2.operando1=masTerminos1.operando1-termino.valor
masTerminos1.valor = masTerminos2.valor
(4) masTerminos1.valor = masTerminos1.operando1
(5) masFactores.operando1 = factor.valor
termino.valor = masFactores.valor
(6) masFactores2.operando1=masFactores1.operando1*factor.valor
masFactores1.valor = masFactores2.valor
(7) masFactores2.operando1=masFactores1.operando1/factor.valor
masFactores1.valor = masFactores2.valor
(8) masFactores1.valor = masFactores1.operando1
(9) factor.valor = CTE_ENTERA.valor
Es una gramtica LL1, por lo que implementar un analizador sintctico descendente recursivo predictivo con un lookahead de 1 es relativamente sencillo: traducimos cada no terminal
en un mtodo recursivo de una clase Sintactico y cada terminal a una comprobacin de
que el token esperado es el actual denominada comnmente match [Watt00].
Al mismo tiempo que se reconoce la estructura sintctica del programa de entrada, es posible evaluar los atributos de la gramtica atribuida. Los mtodos que representan los no
terminales de la gramtica recibirn tantos parmetros como atributos heredados posean, y
devolvern los valores de sus atributos sintetizados39. En el cuerpo de los mtodos se traducirn las reglas semnticas a cdigo, calculando los atributos heredados de un no terminal previamente a su invocacin para, en su llamada, pasrselos como parmetros. Los atributos sintetizados de los smbolos de la parte derecha se obtendrn como retorno de su
invocacin. Finalmente, el mtodo deber devolver el clculo de los atributos sintetizados
del no terminal de la parte izquierda de la produccin.
Siguiendo este esquema de traduccin, podremos codificar en Java la primera produccin y
sus reglas semnticas del siguiente modo:
/** Mtodo que reconoce sintcticamente el
* no terminal "expresion".<br/>
* Produccin: expresion -> terminos masTerminos<br/>
* Adems evala los atributos calculados en las reglas semnticas
* de esta produccin. Recibe los atributos heredados (ninguno) y
* devuelve los sintetizados (expresion.valor)<br/>
*/
public int expresion() {
39
El modo de devolver mltiples valores por un mtodo vara en funcin del leguaje de programacin
elegido: con registros, direcciones de memoria, vectores de elementos o incluso empleando herencia.
52
53
Cuando un mtodo es invocado, recibir como parmetros los valores de sus atributos heredados, deber invocar a los no terminales de la parte derecha, calcular sus atributos sintetizados y, finalmente, los retornar. Este proceso se hace de un modo recursivo.
Fjese como el orden de invocacin de los no terminales de la parte derecha tiene que ser
de izquierda a derecha, puesto que sta la restriccin impuesta en la definicin de gramtica
L-atribuida ( 4):
Si un atributo heredado de un smbolo de la parte derecha depende de un atributo heredado del no terminal de la izquierda, este valor ya ha sido calculado
puesto que se ha recibido como parmetro en el mtodo que se est ejecutando.
Si un atributo heredado de un smbolo de la parte derecha depende de un atributo de otro smbolo de la parte derecha, el segundo ha de estar la izquierda del
primero (si no, no sera L-atribuida). Puesto que la invocacin de mtodos se
realiza siguiendo un orden de izquierda a derecha, los atributos necesarios estarn siempre disponibles ya que sus mtodos asociados fueron invocados previamente.
Esta tcnica de evaluacin descendente de gramticas atribuidas es la llevada a cabo
por las herramientas descendentes AntLR [ANTLR] y JavaCC [JavaCC].
Evaluacin ascendente de atributos heredados
Hemos comentado cmo una gramtica S-atribuida puede ser fcilmente evaluada
por un analizador sintctico ascendente. Para ello, la pila del analizador deber aumentarse
para albergar los valores de los atributos de los smbolos gramaticales. En cada reduccin
se evaluarn los atributos sintetizados del no terminal de la izquierda, empleando pare ello
los valores de los atributos de la parte derecha de la produccin.
En la clasificacin de gramticas presentadas en 4 veamos cmo las gramticas Satribuidas constituan un subconjunto de las L-atribuidas y, por tanto, posean una expresividad menor. As, el poder calcular atributos heredados mediante un analizador sintctico
ascendente siempre representar una caracterstica positiva del mismo.
Dada la siguiente gramtica atribuida:
<S>
<A>
<B>
<A> <B>
a
b
B.e = f(A.s)
A.s = a.s
B.s = g(B.e)
Existe la necesidad de un atributo heredado B.e. Este caso es el mismo que se daba
en el Ejemplo 12. El atributo heredado, en un esquema de evaluacin ascendente, no se
poda pasar a la ltima produccin para que pueda calcularse B.s. Sin embargo, la gramtica anterior se puede rescribir aadiendo un no terminal vaco justo antes del smbolo que
necesita el atributo heredado.
(1)
(2)
(3)
(4)
<S>
<A>
<B>
<C>
A.s = a.s
B.s = g(topepila-1.e)
C.e = f(topepila.s)
54
En nuestro ejemplo depende nicamente de A.s y podr acceder a l puesto que siempre
se encontrar en el tope de la pila ya que est justo a su izquierda en la primera produccin40.
Una vez calculado el valor de C.e, ste se posicionar en el tope de la pila al haberse reducido la cuarta produccin. Puesto que B es el siguiente smbolo de C, la siguiente
reduccin ser aqulla en la que B aparezca en la parte izquierda produccin 3. Cmo
podr la regla semntica de la tercera produccin acceder al atributo B.e? Como su parte
derecha est en el tope de la pila, y B justo debajo, deber restar al tope de la pila tantos
elementos como smbolos aparezcan en su parte derecha. En nuestro caso slo est el smbolo terminal b, por lo que el acceso a B.e ser acceder al tope de la pila menos uno.
Hemos visto cmo es posible, siempre que se tenga acceso a los valores de la pila
de un reconocedor ascendente, simular atributos heredados en un analizador sintctico
ascendente. Esta traduccin tiene un conjunto de limitaciones que estn en funcin del
modo en el que se ha escrito la gramtica. Dichas limitaciones pueden ser consultadas en
[Scott00] y [Aho90].
El generador de analizadores sintcticos ascendentes yacc/bison ofrece, de un modo implcito, esta traduccin de definiciones dirigidas por sintaxis. Esta herramienta permite ubicar la asignacin de atributos heredados como cdigo C dentro de una produccin.
La siguiente especificacin yacc/bison, produce el mismo resultado que nuestra gramtica
atribuida:
S: A {$<e>$=f($<s>1);} B
;
A: a {$<s>$=$<s>1;}
;
B: b {$<s>$=g($<e>-1);}
;
$$1 B
{$<s>$=$<s>1;}
{$<s>$=g($<e>-1);}
{$<e>$=f($<s>0);}
Este nuevo cdigo posee precisamente la traduccin hecha previamente por nosotros, empleando la sintaxis adecuada de la herramienta yacc/bison.
Hemos visto cmo las gramticas S y L-atribuidas pueden evaluarse en una nica
pasada al mismo tiempo que se lleva a cabo el anlisis sintctico. Adems, para ambas se
puede identificar un modo de evaluarlas a priori, sin necesidad de establecer un grafo de
dependencias y un ordenamiento topolgico. Las gramticas S-atribuidas son un subconjunto de las L-atribuidas y se pueden evaluar ascendente y descendentemente, respectiva40
Si dependiese de ms smbolos ubicados a su izquierda (situados todos en la parte derecha de la produccin) podra obtener sus valores mediante el acceso a los registros anteriores al tope de la pila.
55
mente. Surge la irona de que, de un modo opuesto, las gramticas ascendentes (LR) poseen
ms expresividad que las descendentes (LL) [Scott00]. As, las gramticas ascendentes permiten representar sintcticamente un mayor nmero de lenguajes, pero stas poseen una
menor capacidad a la hora de representar un clculo de propiedades (atributos) para las
distintas construcciones del lenguaje. Por este motivo surgen dos soluciones prcticas:
Las herramientas ascendentes permiten acceder directamente a la pila del reconocedor, ofreciendo al desarrollador del compilador la posibilidad de simular
atributos heredados mediante traducciones (explcitas o implcitas) de la gramtica.
Las herramientas descendentes amplan su capacidad de reconocimiento sintctico mediante tcnicas como el backtracking selectivo, modificacin del lookahead,
notacin extendidas (EBNF) o el empleo de predicados semnticos.
5.3. Evaluacin de Atributos en Varias Pasadas
En el punto anterior ( 5.2) hemos analizado cmo pueden evaluarse las gramticas
atribuidas en el desarrollo de procesadores de una sola pasada, empleando para ello mtodos basados en reglas ( 5.1): establecimiento esttico de un orden de ejecucin de las reglas semnticas, en funcin de las caractersticas de la gramtica atribuida.
Los procesadores de lenguaje de una pasada poseen los beneficios de ser ms eficientes (menor tiempo de compilacin) y requerir menos memoria. Sin embargo, es ms
sencillo realizar el anlisis semntico como un recorrido del AST puesto que refleja de un
modo sencillo la estructura del programa de entrada, se puede elegir el orden de evaluacin
en funcin de la propiedad analizada y, finalmente, se pueden dar tantas pasadas al AST
como sea necesario. A modo de ejemplo, David Watt [Watt00] identifica que las dos tareas
principales a llevar a cabo por un lenguaje de programacin tpico (con tipos y mbitos
estticos) son la comprobacin de mbitos y tipos41. Para ello, el analizador semntico puede desarrollarse en dos pasadas:
Identificacin: Aplicando las reglas de mbitos que define el lenguaje, cada utilizacin de un identificador en una expresin ser resuelta conociendo su entidad
concreta dentro del programa de entrada. El resultado de este recorrido del
AST es que cada nodo de tipo identificador tendr asociado una referencia a su
smbolo y tipo apropiados.
Comprobacin de tipos: Posteriormente a la pasada de identificacin, se va recorriendo en AST para, aplicando las reglas semnticas de inferencia de tipos
del lenguaje, poder asignar un tipo a las distintas construcciones del programa.
Conforme se van infiriendo los tipos, se realizarn las comprobaciones de que
las operaciones llevadas a cabo con los mismos sean las correctas42.
El nmero de pasadas al AST que son necesarias para llevar a cabo el anlisis semntico de un lenguaje est funcin de sus reglas semnticas.
41
La comprobacin de mbitos (scope rules) valida la correcta utilizacin de los identificadores en cada
mbito, y aplica las reglas de ocultacin en mbitos anidados. La comprobacin de tipos (type rules)
conlleva aplicar las reglas semnticas para inferir los tipos y restringir las operaciones que se pueden
aplicar a cada expresin, en funcin de su tipo.
42
Por ejemplo, al resultado de la invocacin de una funcin (o mtodo), slo se le podr aplicar el operador [] si el tipo devuelto es un vector si acepta la operacin [].
56
43
En la fase de optimizacin de cdigo, es comn traducir este estructura de datos en un grafo dirigido
acclico [Muchnick97].
44
Existen herramientas de desarrollo de compiladores como SableCC [SableCC] que hacen uso intensivo
de este patrn de diseo.
57
Visitor
visitar(e : ElementoConcretoA)
visitar(e : ElementoConcretoB)
Cliente
VisitorConc retoA
visitar(e : ElementoConcretoA)
visitar(e : ElementoConcretoB)
Estructura
VisitorConc retoB
visitar(e : ElementoConcretoA)
visitar(e : ElementoConcretoB)
Elemento
n aceptar(v : Visitor)
v.visitar(this)
v.visitar(this)
ElementoConcretoA
aceptar(v : Visitor)
operacinA()
ElementoConcretoB
aceptar(v : Visitor)
operacinB()
45
Realmente, esta coleccin de nodos por parte de cada nodo es diseada mediante otro patrn de diseo
denominado Composite.
58
expresion '+'
expresion '-'
expresion '*'
expresion '/'
expresion '='
'-' expresion
'(' expresion
CTE_ENTERA
CTE_REAL
expresion
expresion
expresion
expresion
expresion
')'
Se puede crear un AST en la fase de anlisis sintctico, al igual que se hizo en el Ejemplo 7,
con la siguiente estructura:
Expresion
valorCalculado : double
tipo : char
aceptar(v : Visitor)
ExpresionUnaria
operador : String
operando : Expresion
ConstanteReal
valor : double
aceptar(v : Visitor)
aceptar(v : Visitor)
ExpresionBinaria
operador : String
operando1 : Expresion
operando2 : Expresion
aceptar(v : Visitor)
ConstanteEntera
valor : int
aceptar(v : Visitor)
Figura 16: Estructura esttica de los AST empleados para las expresiones del lenguaje presentado.
Para implementar un compilador e intrprete del lenguaje, identificamos las siguientes pasadas al AST:
La pasada de anlisis semntico que infiere el tipo de toda expresin y comprueba que
se cumplen todas las reglas semnticas. El nico caso semnticamente incorrecto del
59
Una pasada sera la generacin de cdigo. Aunque se podra aadir un atributo al nodo
raz del AST para albergar el cdigo de cada construccin sintctica del lenguaje, esto
requerira una memoria muy superior a si se utiliza un atributo de los objetos Visitor
que modele el flujo de salida donde va a generar el cdigo.
Otra pasada sera el clculo del valor de la expresin. Este escenario podra darse tanto
en la optimizacin de cdigo de un compilador, como en la implementacin de un intrprete. Para ello, aadimos un atributo valorCalculado que alberge el valor de la expresin en nuestro ejemplo, al tratarse de expresiones formadas por literales, este valor siempre se podr calcular.
Finalmente, para poder hacer trazas del procesador de lenguaje en tiempo de desarrollo,
identificaremos una pasada que muestre el contenido del AST.
A modo de ejemplo, la siguiente sera la implementacin en Java de los mtodos del Visitor
que calcula el valor de la expresin. Para dar una mayor versatilidad patrn, aadiremos a a
sus mtodos de visita, un parmetro general y un valor de retorno:
package optimizacioncodigo;
import ast.*;
import visitor.Visitor;
/**
* Visitor de clculo del valor de una expresin.<br/>
* Clase utilizada para optimizar las expresiones, calculando el valor
* de las expresiones constantes.<br/>
* 13-oct-2005<br/>
* Procesadores de Lenguaje<br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo<br/>
* @author Francisco Ortin
*/
public class VisitorCalculo extends Visitor {
public Object visitar(ExpresionBinaria exp, Object param) {
exp.operando1.aceptar(this,null);
exp.operando2.aceptar(this,null);
switch(exp.operador.charAt(0)) {
case '+': exp.valorCalculado=exp.operando1.valorCalculado+
exp.operando2.valorCalculado; break;
case '-': exp.valorCalculado=exp.operando1.valorCalculadoexp.operando2.valorCalculado; break;
case '*': exp.valorCalculado=exp.operando1.valorCalculado*
exp.operando2.valorCalculado; break;
case '/': exp.valorCalculado=exp.operando1.valorCalculado/
exp.operando2.valorCalculado; break;
case '%': exp.valorCalculado=exp.operando1.valorCalculado%
exp.operando2.valorCalculado; break;
case '=': exp.valorCalculado=exp.operando1.valorCalculado;
break;
default: assert false;
}
if (exp.tipo=='I')
exp.valorCalculado=(int)exp.valorCalculado;
return null;
}
public Object visitar(ExpresionUnaria exp, Object param) {
assert exp.operador.equals("-");
46
Realmente, todas las asignaciones son incorrectas porque tratamos nicamente con constantes. Sin
embargo, lo especificaremos as para, en un futuro, poder aadir identificadores a las expresiones.
60
El clculo de un nodo que sea una expresin unaria requiere primero el clculo de su nico
operando. Para ello, invoca recursivamente al mtodo aceptar de la expresin almacenada como nico operando. Una vez calculada sta, asigna al atributo del nodo
ExpresionUnaria el valor calculado, cambiado de signo.
Respecto al clculo de las expresiones binarias el proceso es similar. Primero se calcula las
dos subexpresiones mediante visitas recursivas, para posteriormente asignar el valor adecuado en funcin del operador. Finalmente, la visita de las dos constantes slo requieren la
asignacin de sus valores constantes, puesto que los nodos del AST que modelan estos
terminales ya poseen dicho valor se le pasa en su construccin.
La generacin de cdigo es lleva a cabo por el Visitor VisitorGC. Una implementacin en
Java es:
package generacioncodigo;
import java.io.Writer;
import java.io.IOException;
import ast.*;
/**
* Visitor de generacin de cdigo de una expresin.<br/>
* 13-oct-2005<br/>
* Procesadores de Lenguaje<br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo<br/>
* @author Francisco Ortin
*/
public class VisitorGeneradorCodigo extends Visitor {
/**
* Dnde vamos a generar el cdigo
*/
private Writer out;
/**
* @param out Flujo de salida donde se genera el cdigo
*/
public VisitorGeneradorCodigo(Writer out) {
this.out = out;
}
public Object visitar(ExpresionBinaria exp, Object param) {
try {
exp.operando1.aceptar(this,null);
if (exp.operando1.tipo=='I' && exp.tipo=='F' )
out.write("\tITOF\n"); // * Integer to Float
exp.operando2.aceptar(this,null);
// * Tipo del operador
out.write("\t"+exp.tipo); // I o F
61
// * Operador
switch (exp.operador.charAt(0)){
case '+': out.write("ADD\n"); break;
case '-': out.write("SUB\n"); break;
case '*': out.write("MUL\n"); break;
case '/': out.write("DIV\n"); break;
case '=': out.write("STORE\n"); break;
default: assert false;
}
}
catch(IOException t) {
System.err.println("Error generando cdigo.");
System.exit(-1);
}
return null;
}
public Object visitar(ExpresionUnaria exp, Object param) {
try {
exp.operando.aceptar(this,null);
out.write("\t"+exp.tipo+"NEG\n");
}
catch(IOException t) {
System.err.println("Error generando cdigo.");
System.exit(-1);
}
return null;
}
public Object visitar(ConstanteReal cr, Object param) {
try {
out.write("\tPUSHF\t"+cr.valor+"\n");
}
catch(IOException t) {
System.err.println("Error generando cdigo.");
System.exit(-1);
}
return null;
}
public Object visitar(ConstanteEntera ce, Object param) {
try {
out.write("\tPUSHI\t"+ce.valor+"\n");
}
catch(IOException t) {
System.err.println("Error generando cdigo.");
System.exit(-1);
}
return null;
}
}
Como comentamos anteriormente, es el Visitor, en este caso, el que posee el flujo de salida
en lugar de asignar a cada nodo un buffer de memoria con su cdigo destino de pila asociado. Sin embargo, la utilizacin de un atributo con el cdigo generado para cada no terminal, es un mecanismo tpico cuando se utilizan gramticas atribuidas en la generacin de
cdigo [Louden97].
La visita de una constante implica su generacin de cdigo. La traduccin es utilizar la instruccin de bajo nivel PUSH anteponiendo el tipo de la constante (I o F) y el valor a apilar.
Ntese cmo se emplea el atributo tipo del nodo para anteponer el tipo a la sentencia.
Esto es posible porque previamente el Visitor de anlisis semntico infiri el tipo de cada
subexpresin.
Para las expresiones unarias, se genera el cdigo de su operando (que apilar el valor de la
subexpresin) y posteriormente se genera una instruccin NEG con su tipo adecuado. En
las expresiones binarias se genera el cdigo de las dos subexpresiones y despus el de la
62
operacin. Posteriormente a la generacin de cada subexpresin, como el anlisis semntico calcul previamente el tipo de ambas subexpresiones, se generan instrucciones de coercin (promocin) a bajo nivel. La instruccin ITOF convierte el valor entero del tope de la
pila a un valor real; sta es generada cuando el operando es entero y la expresin binaria
real.
Podemos ver el cdigo principal, dejando el anlisis sintctico y la construccin del AST a
la herramienta byaccJ:
import
import
import
import
import
import
import
sintactico.Parser;
lexico.Lexico;
semantico.VisitorSemantico;
optimizacioncodigo.*;
generacioncodigo.*;
visitor.VisitorDebug;
java.io.*;
/**
* Prueba del compilador.<br/>
* 13-oct-2005 <br/>
* Procesadores de Lenguaje <br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo <br/>
*
* @author Francisco Ortin
*/
public class Main {
public static void main(String args[]) throws IOException {
if (args.length<2) {
System.err.println("Necesito el archivo de entrada y salida.");
return;
}
FileReader fr=null;
try {
fr=new FileReader(args[0]);
} catch(IOException io) {
System.err.println("El archivo "+args[0]+
" no se ha podido abrir.");
return;
}
// * Creamos lxico y sintctico
Lexico lexico = new Lexico(fr);
Parser parser = new Parser(lexico,args[1]);
// * "Parseamos"
parser.parse();
// * Visitamos
parser.ast.aceptar(new VisitorSemantico(),null);
if (parser.ast.tipo!='E') {
System.out.println("Anlisis semntico finalizado
satisfactoriamente.");
parser.ast.aceptar(new VisitorCalculo(),null);
System.out.println("Valor de la expresin: "+
parser.ast.valorCalculado);
FileWriter fw=new FileWriter(args[1]);
parser.ast.aceptar(new VisitorGeneradorCodigo(fw),null);
fw.close();
System.out.println("Cdigo generado en el fichero: "+args[1]);
}
else
System.err.println("El programa no es semnticamente vlido.");
parser.ast.aceptar(new VisitorDebug(System.out),new Integer(0));
}
}
63
3
2
1
3.2
Para consultar la implementacin completa del AST, los cuatro Visitor implementados y el
resto del procesador consltese el apndice A.
Un ejemplo ms amplio de una implementacin completa de un procesador de lenguaje en Java, empleando distintos recorridos del AST mediante el patrn de diseo Visitor,
puede consultarse en [Watt00].
Evaluacin de gramticas S-atribuidas
Como en el caso de la evaluacin de atributos en una sola pasada ( 5.2), este tipo
de gramtica posibilita su evaluacin mediante una nica visita de cada uno de los nodos
del rbol, con el consecuente beneficio de eficiencia.
En el caso de las gramticas S-atribuidas, la evaluacin de la gramtica se obtiene
mediante un recorrido del rbol en profundidad postorden. Si se emplea el patrn de diseo Visitor para recorrer un rbol sintctico a partir de una gramtica S-atribuida, su recorrido postorden en profundidad implicar la visita de todos los hijos de cada nodo, para posteriormente computar su regla semntica asociada. Dependiendo de la fase en la que nos
encontremos, las visitas de los nodos producirn una determinada accin: identificacin de
smbolos, inferencia y comprobacin de tipos, generacin y optimizacin de cdigo, o incluso interpretacin.
Ejemplo 30. En el Ejemplo 29, la pasada de clculo del valor de la expresin representada por el
AST supone una gramtica S-atribuida. La gramtica abstracta (sobre el AST) es:
(1)
(2)
(3)
(4)
64
Expresion
Binaria:
Expresion
Unaria:
Constante
Entera:
Constante
Real:
Expresion1
Expresion1
Operador Expresion2
Expresion
CTE_ENTERA
Expresion
CTE_REAL
La notacin empleada para representar la gramtica del AST es la identificada por Michael
L. Scott [Scott00]. La sintaxis A:B en la parte izquierda de una produccin de una gramtica abstracta significa que A es un nodo del AST que se crea al reconocer la produccin asociada al no terminal B. Adems, A es un tipo de B y, por tanto, A puede aparecer siempre
que B aparezca en la parte derecha de una produccin. De este modo, la relacin entre A y
B puede modelarse en orientacin a objetos como una relacin de generalizacin (herencia): A es una clase derivada (ms especfica) que B (clase abstracta del AST).
Para la gramtica abstracta anterior, la fase de clculo de la expresin se puede representar
con las siguientes reglas semnticas:
(1)
(2)
(3)
(4)
switch(opearador) {
case +: Expresion1.valor = Expresion2.valor +
Expresion3.valor; break;
case -: Expresion1.valor = Expresion2.valor Expresion3.valor; break;
case *: Expresion1.valor = Expresion2.valor *
Expresion3.valor; break;
case /: Expresion1.valor = Expresion2.valor /
Expresion3.valor; break;
case =: Expresion1.valor = Expresion3.valor; break;
}
Expresion1.valor = - Expresion2.valor;
Expresion.valor = CTE_ENTERA.valor
Expresion.valor = CTE_REAL.valor
Ntese cmo la gramtica es S-atribuida puesto que todos los atributos son sintetizados. La
evaluacin del rbol puede hacerse mediante un recorrido en profundidad postorden, evaluando el atributo valor de los nodos hijos y posteriormente calculando el valor de la subexpresin padre.
En los dos ejemplos anteriores se puede ver la traduccin llevada a cabo entre una
gramtica atribuida (o definicin dirigida por sintaxis) sobre una sintaxis abstracta de un
lenguaje, y su representacin mediante un AST. Los nodos concretos y abstractos de cada
nodo, as como las relaciones de herencia, se obtienen a partir de los no terminales de la
gramtica y las etiquetas aadidas a cada produccin. Los atributos de cada smbolo gramatical se traducen a atributos de cada una de las clases que modelan el rbol vase el
Ejemplo 29 y Ejemplo 30.
Adicionalmente a la traduccin previa, vlida para cualquier tipo de gramtica, una
gramtica S-atribuida indicar un modo de codificar el mtodo visitar asociado a cada
uno de los nodos, pudindose as realizar una traduccin al patrn Visitor. Cada mtodo
visitar representa la ejecucin de la regla semntica asociada a la produccin, cuya parte
izquierda es el nodo pasado como parmetro. Puesto que, para calcular (sintetizar) los atributos del nodo que se est visitando es necesario calcular previamente los atributos (sintetizados) de sus hijos, se invocar inicialmente a los mtodos aceptar (y por tanto al mtodo visitar) para todos sus hijos y, posteriormente, se ejecutar la regla semntica asociada a la produccin. Siguiendo este esquema de traduccin de reglas semnticas a mtodos
de visita, una gramtica atribuida podr ser evaluada mediante un recorrido en profundidad
postorden de un rbol sintctico.
Evaluacin de gramticas L-atribuidas
Las gramticas L-atribuidas, al igual que las S-atribuidas, pueden evaluarse en una
nica visita de todos sus nodos, con un criterio de recorrido establecido a priori. Tras
65
haberse creado una estructura de rbol sintctico a partir de una gramtica concreta o abstracta, el problema que nos queda por abordar es cmo codificar los mtodos de visita, es
decir, en qu orden se ha de evaluar la gramtica.
Recordemos que las gramticas S-atribuidas son un subconjunto de las L-atribuidas.
El modo de calcular los atributos sintetizados de ambas gramticas fue analizado en el punto anterior. Para estos atributos debemos seguir el esquema de evaluacin mediante un
recorrido postorden de rbol. El estudio que ahora nos atae es relativo a los atributos
heredados de la gramtica. Aqu, si recordamos la definicin de gramtica L-atribuida ( 4),
comentbamos cmo los atributos heredados podan calcularse en funcin de:
Los atributos heredados del nodo padre. En este caso, el atributo del nodo padre (nodo actual, sobre el que se est ejecutando el mtodo visitar) debe utilizarse para calcular un atributo heredado, antes de invocar al mtodo aceptar
con el nodo poseedor de dicho atributo. Por tanto, este recorrido en profundidad emplea una evaluacin preorden.
Los atributos de la parte derecha, situados a la izquierda del smbolo gramatical.
Para poder asignar a un nodo A un atributo de un nodo B, situado a su izquierda, se visitar B (pasndole el mensaje aceptar) y posteriormente se evaluar
el atributo de A. ste es un recorrido inorden.
Vemos, pues, cmo el recorrido de una gramtica L-atribuida se lleva a cabo con
una nica visita de cada nodo mediante un recorrido en profundidad. Se evaluarn los atributos sintetizados con un recorrido postorden, y los heredados combinando inorden y preorden, en funcin de sus dependencias con otros atributos.
Ejemplo 31: Dada la sintaxis dirigida por sintaxis del Ejemplo 12:
declaracion tipo
variables ;
tipo
int
| float
variables1
id ,
variables2
| id
variables.tipo = tipo.tipo
tipo.tipo = I
tipo.tipo = F
ts.insertar(id.valor,variables.tipo)
variables2.tipo=variables1.tipo
ts.insertar(id.valor,variables.tipo)
Se ve cmo la ejecucin de la regla (asignacin de un atributo heredado a partir de un atributo de un nodo hermano a su izquierda) es evaluada mediante un recorrido inorden. El
otro clculo del atributo heredado tipo asociado al no terminal Variables se codifica
con el siguiente mtodo de visita:
void Visitor::visitar(Variables *e) {
// * Se ejecuta la regla que depende del heredado del padre
ts.insertar(e->getID(),e->tipo);
if (e->getVariables()) {
// * Reglas semntica que depende de un hermano de la izquierda
66
e->getVariables()->tipo=e->tipo;
// * Se visita al no terminal variables de la parte derecha
e->getVariables()->aceptar(this);
}
}
En este caso, el recorrido es preorden: primero se ejecuta la insercin en la tabla de smbolos y la asignacin de tipos, para posteriormente llevarse a cabo la visita. Aunque no se
muestre, la evaluacin del atributo tipo para segunda y tercera produccin se realiza postorden ya que tipo es un atributo sintetizado.
Otras evaluaciones con una nica visita
En la evaluacin de gramticas con una nica pasada, indicbamos cmo un inconveniente era que, al realizarse todas las fases en la misma pasada, la obtencin de componentes lxicos (tokens) segua siempre una ordenacin de izquierda a derecha respecto al
archivo fuente ( 5.2). sta no es una restriccin en el caso de realizar un procesador de
lenguaje en varias pasadas, puesto que el rbol sintctico ya posee en su estructura todos los
componentes lxicos, pudiendo acceder a stos sin necesidad de seguir un orden preestablecido.
Por la diferencia existente entre los compiladores de una y varias pasadas, descrita
en el prrafo anterior, es posible que una gramtica, sin ser ni S ni L-atribuida, pueda ser
evaluada con una nica visita de sus nodos ante cualquier programa de entrada. Adems, el
orden de evaluacin podr ser descrito de un modo esttico siguiendo un mtodo basado
en reglas definido en 5.1. La principal consideracin para evaluar los atributos durante el
recorrido del rbol es que los atributos heredados en un nodo se calculen antes de que el
nodo sea visitado, y que los atributos sintetizados se calculen antes de abandonar el nodo
[Aho90].
Un primer ejemplo de este tipo de gramticas es aqul que, sin ser L-atribuida,
cumple que la evaluacin de todos sus atributos heredados puede llevarse a cabo con un
orden predeterminado, mediante una nica visita de los no terminales de la parte derecha.
Ejemplo 32. Sea la siguiente gramtica atribuida:
(1) A
L M
(2)
Q R
TOKEN_L
TOKEN_M
TOKEN_Q
TOKEN_R
(3)
(4)
(5)
(6)
L
M
Q
R
L.h
M.h
A.s
R.h
Q.h
A.s
L.s
M.s
Q.s
R.s
=
=
=
=
=
=
=
=
=
=
1
h(L.s)
i(M.s)
2
k(R.s)
l(Q.s)
f1(TOKEN_L.s,L.h)
f2(TOKEN_M.s,M.h)
f3(TOKEN_Q.s,R.h)
f4(TOKEN_R.s,Q.h)
67
S
expresion1
expresion
expresion
expresion
expresion2 / expresion3
CTE_ENTERA
CTE_REAL
Se quiere ampliar con una gramtica atribuida para que el no terminal inicial (S) posea un
atributo S.valor de tipo real que posea el valor de la expresin. La expresin puede ser
entera o real47. En el caso de que la expresin sea real, todas las divisiones de la misma deben ser reales (ninguna ser entera, aunque ambos operandos sean enteros). Como ejemplo,
la sentencia 5/2/2.0 tendr deber evaluarse con valor 1.2548. En el caso de que la ex-
47
48
68
presin sea entera, todas sus divisiones se realizarn despreciando la parte decimal del resultado. A modo de ejemplo, 5/2/2 se evaluar con el valor de 1.
Del problema anterior se deduce que habr que calcular primero el tipo de la expresin y
despus llevar a cabo su procesamiento. El atributo que se asignar a toda subexpresin
para conocer su tipo ser tiposub, con los posibles valores entero y real. Una vez se
conozca el atributo de todas las subexpresiones de una expresin, se podr conocer el tipo
global de la expresin: este valor se guardar en el atributo tipoexp.
Empleando los atributos definidos, la siguiente gramtica atribuida resuelve el problema:
(1)
(2)
(3)
(4)
expresion.tipoexp = expresion.tiposub
S.valor = expresion.valor
if (expresion2.tiposub==real || expresion3.tiposub==real)
expresion1.tiposub = real
else expresion1.tiposub = entero
expresion2.tipoexp = expresion1.tipoexp
expresion3.tipoexp = expresion1.tipoexp
if (expresion1.tipoexp==entero)
expresion1.valor = (int)(expresion2.valor) /
(int)(expresion3.valor)
else expresion1.valor = expresion2.valor/expresion3.valor
expresion.tiposub = entero
expresion.valor = CTE_ENTERA.valor
expresion.tiposub = real
expresion.valor = CTE_REAL.valor
Demand-driven algorithm.
69
te en reemplazar cada atributo de los nodos del rbol con un mtodo que ejecute la parte
de la regla semntica asociada a su evaluacin, devolviendo su valor. Para evaluar un atributo, su mtodo asociado deber ser invocado y ste, a su vez, llamar a todos los mtodos
asociados a los atributos de los que dependa. El algoritmo recursivo finaliza si la gramtica
atribuida no es circular.
La tcnica de evaluacin bajo demanda no lleva a cabo una ordenacin topolgica
del grafo de dependencias ( 5.1), sino que hace uso de que las rutinas semnticas definen
dicho grafo de forma implcita. El mayor inconveniente de este algoritmo es su baja eficiencia, ya que la misma regla semntica es ejecutada en mltiples ocasiones. En el peor de
los casos, la complejidad computacional es exponencial en relacin con el nmero de atributos de la gramtica [Engelfriet84].
5.4. Rutinas Semnticas y Esquemas de Traduccin
Al igual que existen herramientas que construyen analizadores sintcticos a partir de
gramticas libres de contexto, tambin existen herramientas automticas que generan evaluadores de gramticas atribuidas o, en la mayor parte de los casos, definiciones dirigidas
por sintaxis. En muchas ocasiones, las herramientas de desarrollo de procesadores de lenguaje ofrecen la posibilidad de especificar, de un modo imperativo en lugar de declarativo,
las reglas semnticas de las definiciones dirigidas por sintaxis (gramticas atribuidas). Esta
notacin es la que se conoce como esquema de traduccin (dirigida por sintaxis): una
gramtica libre de contexto en la que se asocian atributos con los smbolos gramaticales y se
insertan rutinas semnticas dentro de las partes derecha de las producciones [Aho90]. Las
rutinas semnticas son, a su vez, fragmentos de cdigo que el desarrollador del compilador escribe normalmente entre llaves {} dejando explcito el momento en el que la
herramienta ha de ejecutar la misma, durante su proceso de anlisis.
La principal diferencia entre las herramientas que emplean gramticas atribuidas y
aqullas que ofrecen esquemas de traduccin es que en las segundas el desarrollador especifica el momento en el que se ha de ejecutar el cdigo. Sin embargo, en las gramticas atribuidas y definiciones dirigidas por sintaxis, el proceso de evaluacin de los atributos debe
ser resuelto por la propia herramienta. La evaluacin de una gramtica atribuida, conlleva
procesos como la creacin y ordenamiento topolgico de un grafo de dependencias, o la
limitacin a priori de las caractersticas de la gramtica vase 1.
La mayora de las herramientas que generan analizadores sintcticos ofrecen la posibilidad de aadir rutinas semnticas, definiendo as un esquema de traduccin. Herramientas como yacc/bison [Johnson75], ANTLR [ANTLR] o JavaCC [JavaCC] permiten
entremezclar rutinas semnticas con las producciones de las gramticas libres de contexto.
Puesto que los esquemas de traduccin ejecutan las rutinas semnticas de un modo
imperativo, el modo en el que se deriven las distintas producciones de la gramtica variar
el orden de ejecucin de las rutinas. De este modo, el diferenciar si un esquema de traduccin emplea un anlisis descendente o ascendente es fundamental para la ubicacin de sus
rutinas semnticas.
En los generadores de analizadores sintcticos descendentes que incorporan esquemas de traduccin (por ejemplo JavaCC o ANTLR), las rutinas semnticas pueden aparecer en cualquier parte de la parte derecha de la produccin. Una rutina semntica al comienzo de la parte derecha de una produccin ser ejecutada cuando el analizador tome la
decisin de derivar por dicha produccin. Una rutina situada en el medio de la parte derecha de una produccin se ejecutar una vez haya derivado todos los smbolos de la parte
derecha, ubicados a su izquierda.
70
Como se aprecia en el prrafo anterior, las limitaciones de los esquemas de traduccin basados en analizadores descendentes son los mismos que los identificados para la
evaluacin descendente de gramticas L-atribuidas en una nica pasada ( 5.2). Adicionalmente a estas limitaciones, hay que aadir que la ubicacin de las rutinas semnticas, dentro
de la parte derecha de cada produccin, sea en los sitios oportunos. Las restricciones para
ubicar las rutinas son [Aho90]:
masTerminos1
|
termino
termino
{masTerminos.operando1 = termino.valor}
masTerminos
{expresion.valor = masTerminos.valor}
+ termino
{masTerminos2.operando1 =
masTerminos1.operando1+termino.valor}
masTerminos2
{masTerminos1.valor = masTerminos2.valor}
- termino
{masTerminos2.operando1 =
masTerminos1.operando1-termino.valor}
masTerminos2
{masTerminos1.valor = masTerminos2.valor}
{masTerminos1.valor=masTerminos1.operando1}
factor
{masFactores.operando1 = factor.valor}
masFactores
{termino.valor = masFactores.valor}
71
masFactores1
|
factor
* factor
{ masFactores2.operando1 =
masFactores1.operando1*factor.valor}
masFactores2
{ masFactores1.valor = masFactores2.valor
}
/ factor
{ masFactores2.operando1 =
masFactores1.operando1/factor.valor}
masFactores2
{masFactores1.valor = masFactores2.valor}
{masFactores1.valor=masFactores1.operando1}
CTE_ENTERA
{factor.valor = CTE_ENTERA.valor}
La ejecucin de las reglas comienza por producir el no terminal expresion. ste producir el smbolo termino, obteniendo su atributo sintetizado termino.valor y asignndoselo al heredado masTerminos.operando1 antes de su produccin. Tras haber calculado el nico atributo heredado de masTerminos, se podr producir dicho no terminal. El
esquema general es asignar los heredados antes de producir y calcular los sintetizados en las
rutinas ubicadas en una produccin.
El momento en el que el esquema de traduccin ejecuta una rutina semntica es exactamente el mismo en el que se derivara por un no terminal que estuviese en lugar de la rutina.
En el caso de los analizadores ascendentes, no es posible ubicar rutinas semnticas
en cualquier zona de la parte derecha de una produccin, puesto que el reconocedor no
sabe en todo momento en que produccin se halla reduce nicamente cuando comprueba
que el tope de la pila es igual a la parte derecha de una produccin. Si se trabaja con gramticas S-atribuidas, los esquemas de traduccin asociados debern poseer sus rutinas semnticas al final de todas las producciones. stas se ejecutarn cuando se reduzca su produccin asociada.
Si la herramienta ascendente que nos ofrece un esquema de traduccin permite simular atributos heredados como es el caso de yacc/bison ser necesario conocer las traducciones empleadas para simular dichos atributos, tales como la ubicacin de rutinas semnticas en el medio de una produccin, o el acceso a cualquier elemento de la pila (vistos
en el punto 5.2, dentro del epgrafe Evaluacin ascendente de atributos heredados).
Otra caracterstica aadida a los esquemas de traduccin ms avanzados es la posibilidad de procesar tanto gramticas de anlisis sintctico (gramticas concretas), como
gramticas del resto de fases de un procesador de lenguaje (gramticas abstractas). Son capaces de validar la estructura sintctica, no slo de componentes lxicos (tokens), sino tambin de nodos de un AST. Empleando los mismos esquemas de traduccin se puede implementar cualquier fase de un compilador. Estos tipos de esquemas de traduccin, en los
que los elementos terminales de su gramtica son nodos de un AST en lugar de tokens, se
denominan tree walkers implementados por ANTLR y JavaCC, entre otros.
Ejemplo 35. El siguiente esquema de traduccin, empleando la sintaxis de AST de la herramienta
ANTLR [ANTLR], reconoce rboles de expresiones sencillas de un lenguaje de programacin:
expresion
72
|
|
|
|
|
;
Para comprender el ejemplo sin conocer la herramienta, se han indicado los elementos de la
gramtica en negrita. La sintaxis (#Padre Hijo1 Hijo2 Hijo3) representa un nodo
en el que Padre es el nodo raz y sus descendientes son Hijo1, Hijo2 e Hijo3. As, el
esquema de traduccin previo reconoce el AST de una expresin, al mismo tiempo que
evala su valor ste es el objetivo del cdigo Java incluido como rutinas semnticas.
73
Comprobacin de Tipos
Las variables i y j han de estar declaradas, tener definas en su tipo la operacin suma,
y calcular (inferir) el tipo de dicha operacin.
Respecto al tipo calculado, deber ser entero o convertible implcitamente (promocionable) a entero, puesto que en C los ndices de un array han de ser enteros.
La variable v deber estar declarada y poseer como tipo un array o puntero (en C el
operador [] se puede aplicar a punteros) de punteros a otro tipo T. De este modo, se
podrn aplicar los dos operadores [] y * a dicho identificador.
Finalmente, la funcin (o procedimiento) f tendr que haber sido declarada previamente y poseer un nico parmetro de tipo T o promocionable a un tipo T.
Todas estas tareas debern ser llevadas a cabo por una parte del analizador semntico: el
comprobador de tipos.
Tambin es comn que los compiladores necesiten el resultado de la inferencia o
clculo de tipos llevada a cabo por el analizador semntico, para la fase de generacin de
cdigo. A modo de ejemplo, el operador + del lenguaje de programacin Java, genera distinto cdigo en el caso de que los dos operandos sean nmeros enteros, nmero reales o,
incluso, cadenas de caracteres. En Java, el operador + posee una semntica de suma para
50
51
Type checker.
Type clash.
75
Si el mtodo es virtual, es necesario una indireccin mediante una tabla de mtodos virtuales.
76
Comprobacin de Tipos
dinmicamente en un tiempo constante, muy inferior al necesitado por Smalltalk. Posteriores optimizaciones del lenguaje Smalltalk-80 emplearon sistemas de tipos para mejorar su
rendimiento en tiempo de ejecucin [Atkinson86].
53
Built-in type.
77
78
expresion
expresion
expresion
expresion
expresion
expresion1
expresion1
expresion1
expresion1
expresion1
expresion1
expresion1
expresion1
expresion1
expresion1
expresion1
false
true
cte_caracter
cte_entera
cte_real
expresion2 AND expresion3
expresion2 OR expresion3
NOT expresion2
( expresion2 )
expresion2 + expresion3
expresion2 expresion3
expresion2 * expresion3
expresion2 / expresion3
expresion2 div expresion3
expresion2 > expresion3
expresion2 < expresion3
Comprobacin de Tipos
(17)
expresion1
expresion2 = expresion3
A partir de la siguiente gramtica libre de contexto, debemos definir una gramtica atribuida que nos diga si la expresin posee errores de tipo.
P
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
(10)
(11)
(12)
(13)
(14)
(15)
(16)
(17)
R
expresion.et = boolean
expresion.et = boolean
expresion.et = char
expresion.et = integer
expresion.et = real
expresion1.et = boolean
expresion1.et = boolean
expresion1.et = boolean
expresion1.et = expresion2.et
expresion1.et = MayorTipo(expresion2.et,expresion3.et)
expresion1.et = MayorTipo(expresion2.et,expresion3.et)
expresion1.et = MayorTipo(expresion2.et,expresion3.et)
expresion1.et = MayorTipo(expresion2.et,expresion3.et)
expresion1.et = integer
expresion1.et = boolean
expresion1.et = boolean
expresion1.et = boolean
ExpTipo mayorTipo(ExpTipo t1,ExpTipo t2) {
if ( t1==real || t2==real ) return real;
return integer;
}
Una vez calculado el atributo sintetizado expresion.et que posee la expresin de tipo
en el caso de que la operacin sea correcta, podemos identificar los casos de error como un
conjunto de predicados:
P
(6)
(7)
(8)
(10)
(11)
(12)
(13)
(14)
(15)
(16)
(17)
B
expresion2.et==boolean && expresion3.et==boolean
expresion2.et==boolean && expresion3.et==boolean
expresion2.et==boolean
realOentero(expresion2.et,expresion3.et)
realOentero(expresion2.et,expresion3.et)
realOentero(expresion2.et,expresion3.et)
realOentero(expresion2.et,expresion3.et)
expresion2.et==integer && expresion3.et==integer
realOentero(expresion2.et,expresion3.et) ||
(expresion2.et==char && expresion3.et==char)
realOentero(expresion2.et,expresion3.et) ||
(expresion2.et==char && expresion3.et==char)
realOentero(expresion2.et,expresion3.et) ||
(expresion2.et==char && expresion3.et==char)
boolean realOentero(ExpTipo t1,ExpTtipo t2) {
return (t1==integer || t2==real) &&
(t1==integer || t2==real );
}
Otro modo de obtener los posibles errores es definiendo una nueva expresin de tipo,
error, que denote un error de tipo al tratar de aplicar una operacin no definida sobre un
79
tipo determinado. En este caso, la expresin deber tener un tipo distinto a error para ser
semnticamente correcta.
Existen lenguajes en los que es posible crear nuevos tipos simples. Ejemplos tpicos
son los tipos enumerados y subrango de lenguajes como Pascal o Ada. Estos tipos no son
construidos, puesto que no utilizan expresiones de tipo para construir los nuevos tipos.
Ejemplo 40. En el lenguaje Pascal, un tipo subrango de valores enteros comprendidos entre 0 y 9 puede definirse del siguiente modo:
type Digito = 0..9;
Del mismo modo, un tipo enumerado de tres enteros distintos puede definirse, en el lenguaje C, del siguiente modo:
typedef enum { rojo, verde, azul } Color;
Tanto los valores enumerados como los subrangos son un subconjunto del tipo entero.
Expresiones de tipo de los dos ejemplos pueden ser 0..9 y [rojo,verde,azul]. Ntese cmo los valores de los que estn compuestos las expresiones tipo no son a su vez
expresiones de tipo. Por este hecho, los tipos enumerados y subrango no son compuestos,
sino simples.
Dado un conjunto de tipos simples, los lenguajes de programacin permiten al
usuario crear nuevos tipos, empleando constructores de tipos tales como struct, array,
union o class. Estos constructores pueden ser vistos como funciones que, recibiendo
un conjunto de tipos como parmetros, devuelven un nuevo tipo compuesto, cuya estructura depende del constructor empleado. El conjunto de valores que pueden albergar las
entidades de estos tipos construidos, al igual que las operaciones que sobre ellos se pueden
aplicar, dependern del constructor y tipos empleados para componer el nuevo tipo.
Ejemplo 41. En este ejemplo mostraremos una representacin de las expresiones de tipo, para
un subconjunto de los constructores de tipo del lenguaje Pascal.
Vectores (arrays). Un vector denota una agrupacin o coleccin de elementos homogneos.
Su semntica suele representarse mediante la asociacin de un ndice (comnmente de tipo
entero) a un elemento del tipo empleado para construir el array. La operacin ms comn
ser, pues, el acceso a un elemento a partir de un ndice tpicamente representado con el
operador [] o ().
Si T es una expresin de tipo e I es una expresin de tipo subrango, entonces la expresin
de tipo array(I,T) denota el tipo vector (array) de elementos de tipo T e ndices I. As,
la siguiente declaracin:
var a: array [1..10] of integer;
hara
que
un
compilador de Pascal
array(1..10,integer) al identificador a.
asociase
la
expresin
de
tipo
Punteros. Desde el punto de vista semntico, un puntero es un modo indirecto para referenciar un elemento del tipo empleado en su construccin. Por esta caracterstica son muy
empleados para definir estructuras recursivas. Algunas implementaciones hacen que un
puntero denote una direccin de memoria; en otras, simplemente albergan el identificador
nico de un objeto o variable. Desde el punto de vista basado en la abstraccin, las opera-
80
Comprobacin de Tipos
ciones ms comunes son: desreferenciar para acceder al elemento al que apuntan, asignacin entre punteros, y obtencin y liberacin de memoria.
Si T es una expresin de tipo, entonces pointer(T) es una expresin de tipo que representa el tipo puntero a un elemento de tipo T. A modo de ejemplo, la siguiente declaracin
en Pascal:
var b: array [-2..2] of real;
81
El lenguaje de programacin ANSI C define las operaciones vlidas a aplicar sobre una
unin como las mismas existentes para los registros [Kernighan91]: asignacin, obtencin
de direccin y acceso al miembro. Tampoco existe comprobacin adicional de acceso a un
campo incorrecto54. Para la fase de anlisis semntico, no sera necesario, pues, crear un
nuevo constructor de tipo para las uniones. Sin embargo, la fase de generacin de cdigo s
lo requiere, ya que las direcciones de memoria de cada campo no sern las mimas que en el
caso de los registros. Mediante un constructor de tipo union, se podra asociar a la variable entero_o_real la expresin de tipo:
union( (entero x int) x (real x float) )
Expresiones de Tipo
char pointer(integer)
integer void
(integer x integer) integer
Ntese cmo, sin que el lenguaje de programacin Pascal posea el tipo void como los
lenguajes basados en C se puede emplear esta expresin de tipo para representar los procedimientos, como casos especiales de funciones. Podra crearse un constructor de tipos
distinto para los procedimientos, y no sera necesaria la utilizacin de la expresin de tipo
void. Sin embargo, se perdera el tratamiento comn que se le da a funciones y procedimientos, desde la fase de anlisis semntico y generacin de cdigo.
54
55
82
Comprobacin de Tipos
Otro elemento a destacar es cmo las funciones que reciben ms de un parmetro emplean
un producto o tupla de los tipos de cada uno de sus parmetros, como tipo base para llevar
a cabo la transformacin.
Clases. Las clases denotan un tipo de objetos que posee una estructura y comportamiento
comn. La mayora de los lenguajes orientados a objetos poseen el concepto de clase para
indicar un tipo de objeto. A la hora de representar este tipo por parte de un procesador de
lenguaje, es necesario tener en cuenta un conjunto de caractersticas propias de los modelos
computacionales orientados a objetos. Centrndonos en el anlisis semntico, las caractersticas principales a tener en cuenta a la hora de representar las clases son:
83
Para un lenguaje que nicamente posee tipos simples, podremos utilizar una representacin mediante enteros, enumerados, caracteres o cadena de caracteres, por ejemplo.
Sin embargo, no todos estos tipos sern vlidos cuando el lenguaje a procesar posea algn
constructor de tipo. Por ejemplo, si adicionalmente a los tipos simples existe el constructor
de tipo puntero, no ser posible emplear enteros, enumerados y caracteres, puesto que,
mediante el constructor de tipos pointer, se puede construir infinitos tipos:
pointer(char), pointer( pointer(char) ), pointer( pointer( pointer
(char)))...
Empleado cadenas de caracteres para representar las expresiones de tipos no poseemos la restriccin previa. Se puede representar cada expresin de tipo con la notacin
descrita por Alfred Aho. No obstante, cada vez que queramos extraer un tipo que forma
parte de otro tipo compuesto, deberamos procesar la cadena de caracteres con la complejidad que ello conlleva. En el caso de los punteros no sera excesivamente complejo, pero
aparecera mayor dificultad conforme surgiesen nuevos constructores de tipos. Se podra
representar de un modo ms sencillo con estructuras de datos recursivas. La representacin
de las expresiones de tipo mencionadas podra pasar a ser, en el lenguaje C, la siguiente:
typedef enum enum_tipos {
entero, caracter, logico, real, puntero
} tipo_t;
typedef struct struct_puntero {
tipo_t tipo;
struct struct_puntero *puntero;
} expresion_tipo;
Esta estructura de tipos sera ms fcil de manipular que una cadena de caracteres,
cuando existiesen ms constructores de tipos. La obtencin de los tipos empleados para
componer cada uno de los tipos construidos ser a travs del acceso a un campo del registro.
La principal limitacin de este tipo de estructura de datos es que, precisamente, slo
modela datos. En la fase de anlisis semntico y generacin de cdigo ser necesario asociar
a estas estructuras un conjunto de rutinas, tales como comprobaciones de validez semntica
o generacin de cdigo intermedio. Existirn rutinas especficas para cada expresin de
tipo, y otras comunes a todas ellas. Haciendo uso de tcnicas ofrecidas por lenguajes orientados a objetos tales como encapsulamiento, herencia y polimorfismo podremos resolver
esta problemtica de un modo ms sencillo.
El problema de representar estructuras compuestas recursivamente de un modo jerrquico, aparece en diversos contextos dentro del campo de la computacin. El patrn de
diseo Composite [GOF02] ha sido utilizado para modelar y resolver este tipo de problemas.
Permite crear estructuras compuestas recursivamente, tratando tanto los objetos simples
como los compuestos de un modo uniforme. Podremos representar su modelo esttico
mediante el siguiente diagrama de clases:
84
Comprobacin de Tipos
Componente
Cliente
operacion1()
operacion2()
Hoja
Composite
operacion1()
operacion2()
operacion1()
operacionEspecifica1()
parametros
ExpresionTipo
de
getBytes() : int
1 ExpresionTipo() : String
flecha() : ExpresionTipo
a corchete(t : ExpresionTipo) : ExpresionTipo
parentesis(p : List) : ExpresionTipo
punto(c : String) : ExpresionTipo
1
equivalente(et : ExpresionTipo) : ExpresionTipo
devolucion
1
campos
: String
: String
Array
desde
hasta
getBytes()
expresionTipo()
corchete()
getDe()
equivalente()
Record
Char
Void
getBytes()
expresionTipo()
expresionTipo()
Pointer
getBytes()
expresionTipo()
flecha()
getDe()
equivalente()
Integer
getBytes()
expresionTipo()
getBytes()
expresionTipo()
punto()
getCampos()
equivalente()
Function
getBytes()
expresionTipo()
parentesis()
getDevoluc ion()
getParametros()
equivalente()
Error
expresionTipo()
Mtodos flecha, corchete, parentesis y punto: Estos mtodos calculan (infieren) el tipo resultante tras aplicar un operador del lenguaje, a partir de una expresin de
tipo (el objeto implcito) y, en algn caso, otras expresiones de tipo pasadas como parmetro. La implementacin por omisin es devolver siempre el tipo Error, indicando
as que dicha operacin no est semnticamente definida para el tipo. Cada tipo que
implemente este operador deber redefinir el mtodo relacionado. Por ejemplo, el tipo
Pointer implementa el mtodo flecha puesto que esta operacin est permitida par
este tipo; la expresin de tipo que devuelve es el tipo al que apunta.
Mtodo expresionTipo: Devuelve una cadena de caracteres representativa de su
expresin de tipo, siguiendo la notacin descrita por Aho (Ejemplo 41). Su objetivo
principal es facilitar las tareas de depuracin.
Mtodo getBytes: Es un mero ejemplo de cmo los tipos del lenguaje poseen funcionalidad propia de la fase de generacin de cdigo. Este mtodo devuelve el tamao
en bytes necesario para albergar una variable de ese tipo.
Mtodo equivalente: Indica si dos expresiones de tipo son o no equivalentes entre
s. Posee una implementacin por omisin enfocada a los tipos simples: dos tipos son
equivalentes si son instancias de la misma clase. Existen diversos modos de definir la
equivalencia entre tipos; profundizaremos en esta cuestin en 6.6.
Las clases derivadas poseen mtodos adicionales propios de su comportamiento especfico,
tales como getCampos (registro), getDevolucion y getParametros (funcin), getA
(puntero) y getDe (array).
Ntese cmo los tipos compuestos poseen asociaciones al tipo base: un puntero requiere
un tipo al que apunta (a); un array al tipo que colecciona (de); un registro requiere una co86
Comprobacin de Tipos
leccin a sus campos (campos), cualificada por el nombre del campo (String); una funcin requiere una asociacin al tipo que devuelve (devolucion), as como una coleccin
cualificada por el nombre de cada uno de sus parmetros (parametros).
El hecho de que todas las asociaciones estn dirigidas hacia la clase base de la jerarqua,
hace que cada tipo compuesto pueda formarse con cualquier otro tipo incluyendo l mismo gracias al polimorfismo.
A modo de ejemplo, la siguiente expresin de tipo:
record( (c x array(1..10,pointer(char))) x
(f x (integer,pointer(char))->pointer(integer)) x
(p x (char,integer)->void) )
: Function devolucion
desde=1
hasta=10
parametros
: Function
devolucion
parametros
de
: Pointer
: Integer
a
: Char
: Pointer
a
: Char
: Pointer
: Char
: Integer
: Void
a
: Integer
El diagrama de objetos posee una estructura jerrquica de rbol. Esta estructura, empleando el mismo diagrama de clases, podra haberse creado en memoria mediante un grafo acclico dirigido (DAG), con el consecuente ahorro de memoria. La instanciacin de objetos
mediante esta estructura puede llevarse a cabo implementando una tabla de tipos que no
cree expresiones de tipo ya existentes.
Una implementacin de ejemplo es la mostrada en el apndice C.1.
87
A continuacin se muestra una gramtica libre de contexto capaz de reconocer sintcticamente el lenguaje:
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
declaraciones1
declaracion
tipo1
|
|
|
listatipos
|
|
|
(9)
(10)
(11)
(12)
(13)
(14)
(15)
88
tipos1
VAR declaraciones
BEGIN expresiones END .
declaraciones2 declaracion ;
ID : tipo
INTEGER
CHAR
^ tipo2
ARRAY [ CTE_ENTERA1 .. CTE_ENTERA2 ]
OF tipo2
FUNCTION ( listatipos ) : tipo2
PROCEDURE ( listatipos )
RECORD listacampos END
tipos
tipos2 , tipo
tipo
Comprobacin de Tipos
(16)
(17)
(18)
(19)
(20)
(21)
(22)
(23)
(24)
(25)
(26)
(27)
(28)
(29)
(30)
(31)
listacampos1
expresiones1
expresion1
listaexps
exps1
|
|
|
|
|
|
|
listacampos2 ID : tipo ;
expresiones2 expresion ;
( expresion2 )
CTE_ENTERA
CTE_CARCTER
ID
expresion2 ^
expresion2 [ expresion3 ]
expresion2 . ID
ID ( listaexps )
exps
exps2 , expresion
expresion
Desarrollaremos una definicin dirigida por sintaxis que implemente un sistema de tipos,
asignando expresiones de tipos a cada construccin del lenguaje. Puesto que tenemos declaraciones de identificadores, necesitaremos una tabla de smbolos (objeto ts) que ofrezca
las operaciones insertar y buscar.
En la declaracin de una variable, el smbolo no terminal tipo podr definir un atributo
sintetizado que albergue su expresin de tipo:
(5)
(6)
(7)
(8)
tipo1.et =
tipo1.et =
tipo1.et =
tipo1.et =
tipo2.et)
integer
char
pointer(tipo2.et)
array( CTE_ENTERA1.valor..CTE_ENTERA2.valor,
La declaracin de los tipos funcin, procedimiento y registro requieren una lista de tipos
para poder coleccionar parmetros y campos. Un modo de representar esta coleccin es
mediante el constructor de tipo producto (cartesiano):
(12)
(13)
(14)
(15)
(16)
(17)
listatipos.et = tipos.et
listatipos.et = ()
tipos1.et = ( tipos2.et x tipo.et )
tipos1.et = ( tipo.et )
listacampos1.et = ( listacampos2.et x
( ID.valor x tipo.et) )
listacampos1 = ()
Una vez asignadas las expresiones de tipo producto, tanto a los parmetros de funciones y
procedimientos como a los campos de los registros, podremos acabar de sintetizar el atributo tipo.et:
(9) tipo1.et = listatipos.et tipo1.et
(10) tipo1.et = listatipos.et void
(11) tipo1.et = record(listacampos.et)
Tras llevar a cabo el clculo del atributo tipo.et, podremos insertar los identificadores
con su tipo adecuado en la tabla de smbolos. De este modo, cuando posteriormente aparezca un identificador en una expresin, podremos conocer su tipo.
(4)
ts.insertar(ID.valor, tipo.et)
89
El resto de la definicin dirigida por sintaxis tendr por objetivo implementar la parte del
sistema de tipos en la que se asignar expresiones de tipo a cada una de las expresiones del
lenguaje. El sistema de tipos deber, pues, asignar al no terminal expresion un atributo
que represente su tipo. En el caso de haberse producido algn error, el tipo a asignar ser
error.
(20)
(21)
(22)
(23)
(24)
expresion1.et = expresion2.et
expresion1.et = integer
expresion1.et = char
expresion1.et = ts.buscar(ID.valor)
if (expresion2.et == pointer(t))
expresion1.et = t
else expresion1.et = error
(25) if (expresion2.et==array(i,t) && expresion3.et==integer)
expresion1.et = t
else expresion1.et = error
(26) if (expresion2.et == record(et1 x (ID.valor x t) x et2)
expresion1.et = t
else expresion1.et = error
La primera regla semntica es necesaria para que la gramtica atribuida sea completa. Las
reglas 21 y 22 se limitan a asignar el tipo de la expresin al ser sta una constante. En la
produccin en la que aparece un identificador en la parte derecha, se accede a la tabla de
smbolos para conocer la expresin de tipo del identificador. Si ste no se ha declarado
previamente en el programa si no est en la tabla, el mtodo buscar devolver el tipo
error.
En el caso del operador de desreferenciar, la comprobacin es que la expresin a la que se
aplica ste ha de ser de tipo puntero. La expresin de tipo inferida es el tipo apuntado por
el puntero. Para los arrays la inferencia del tipo es similar. Sin embargo, es necesario comprobar que la expresin utilizada como ndice sea de tipo entero. Ntese cmo, en el caso
ms general, no podremos saber en tiempo de compilacin si la expresin est comprendida en el subrango del array, ya que este valor ser evaluado en tiempo de ejecucin56.
Para los registros, la comprobacin del operador punto es, por un lado, ratificar que el tipo
de la expresin al que se le aplica dicho operador es de tipo registro. Por otra parte, debemos encontrar un campo con igual nombre que el identificador ubicado en la parte derecha
del punto. Si no fuere as, el tipo inferido ser error. En la notacin empleada en el ejemplo, la expresin de tipo record(et1 x (ID.valor x t) x et2) indica que et1 y
et2 pueden ser cualquier expresin de tipo incluyendo ninguna. De este modo, t hace
referencia a la expresin de tipo asociada al campo que coincide con el valor del identificador.
En la invocacin a funciones y procedimientos, vuelve a aparecer el producto de expresiones de tipos:
(28)
(29)
(30)
(31)
56
listaexps.et
listaexps.et
exps1.et = (
exps1.et = (
= exps.et
= ()
exps2.et x expresion.et )
expresion.et )
Puede haber casos en los que un compilador pueda dar un error. Por ejemplo, el acceso v[23] si el
array v fue declarado con el subrango 1..10. Sin embargo, esto no es siempre factible: v[i] depende
del valor que posea la variable i en la ejecucin del programa.
90
Comprobacin de Tipos
Una vez inferido el tipo de los parmetros, podremos comprobar la validez semntica de la
invocacin:
(27) if ( ts.buscar(ID.valor)==(tp td) && tp==listaexps.et )
expresion1.et = td
else expresion1.et = error
La regla anterior comprueba que el identificador empleado en la invocacin haya sido declarado como funcin o procedimiento, y que el tipo de todos los parmetros reales coincida con los tipos de los parmetros formales. Si la condicin es verdadera, el tipo inferido
resultante de la invocacin ser el tipo que devuelva la funcin void en el caso de un
procedimiento. En cualquier otro caso, el tipo resultante ser error.
Una vez implementado el sistema de tipos, es posible conocer el tipo de toda construccin
sintctica. El comprobador de tipos deber verificar que todas las reglas semnticas relativas a los tipos se cumplen. En nuestro caso, podemos identificar los siguientes puntos en
los que el comprobador podra dar un mensaje de error:
P
B
(4)
(18)
!ts.existe(ID.valor)
expresiones1.et != error
El comprobador de tipos deber verificar que la expresin no haya sintetizado una expresin de tipo error, y que un identificador no sea declarado ms de una vez.
Ejemplo 44. En este ejemplo se mostrar cmo, a partir de la representacin de las expresiones
de tipo diseadas en el Ejemplo 42, la implementacin del sistema de tipos del ejercicio
anterior es realmente evidente. El procesamiento del lenguaje fuente ser llevado a cabo en
una nica pasada, gracias a la sencillez del mismo y a que no se va a desarrollar la fase de
generacin de cdigo. El siguiente fragmento de la especificacin byaccJ realiza las fases de
anlisis sintctico y semntico:
programa: VAR declaraciones BEGIN expresiones END '.'
;
declaraciones: declaraciones declaracion ';'
| /* vacio */
;
declaracion: ID ':' tipo { if (ts.containsKey($1.sval)) {
System.err.println("Error en linea "+
lexico.getYyline());
System.err.println("El ID "+$1.sval+
" ya est declarado.");
}
ts.put($1.sval,$3.obj); }
;
tipo: INTEGER
{ $$=new ParserVal(new Integer()); }
| CHAR
{ $$=new ParserVal(new Char()); }
| '^' tipo {$$=new ParserVal(new Pointer((ExpresionTipo)$2.obj));}
| ARRAY '[' CTE_ENTERA DOS_PUNTOS CTE_ENTERA ']' OF tipo
{ $$=new ParserVal(new Array($3.ival,$5.ival,
(ExpresionTipo)$8.obj)); }
| FUNCTION '(' listatipos ')' ':' tipo
{ $$=new ParserVal(new Function((ExpresionTipo)$6.obj,
(List)$3.obj)); }
| PROCEDURE '(' listatipos ')'
{ $$=new ParserVal(new Function(new Void(),
(List)$3.obj)); }
| RECORD listacampos END
{ $$=new ParserVal(new Record((Map)$2.obj)); }
;
listatipos: tipos { $$=$1; }
91
La primera rutina semntica propia del comprobador de tipos es la ubicada en la produccin declaracion: si el identificador que se est declarando ya existe en la tabla de smbolos, se muestra un error semntico al programador.
La construccin de las expresiones de tipo a la hora de declarar un identificador ha sido
implementada mediante el empleo de constructores. Para el no terminal tipo se almacena,
dentro del adaptador ParserVal, una referencia a un objeto ExpresionTipo. En el caso
de los parmetros de una funcin, el producto de expresiones de tipo se ha representado
con el interfaz List del paquete java.util. Del mismo modo, para el producto de los
campos de un registro se ha utilizado el interfaz Map.
Para las distintas alternativas de producciones en las que expresion aparece en la parte
izquierda, el sistema de tipos se limita a inferir el tipo de cada una de las expresiones. En el
caso de que la expresin sea una constante, su tipo es el de la constante. Si es un identifica92
Comprobacin de Tipos
}
public
Los objetos Error (que modelan la expresin de tipo error) poseen un atributo con el
mensaje de error, y otro dos con el nmero de lnea y columna en el que ste se produjo.
Por omisin, los cuatro operadores devuelven un error semntico. Cada tipo que defina el
operador como vlido deber redefinir (derogar) este funcionamiento por omisin:
// * De la clase Pointer
public ExpresionTipo flecha() {
return a;
}
// * De la clase Array
public ExpresionTipo corchete(ExpresionTipo et) {
if (!(et instanceof Integer))
return new Error("Slo se permiten ndices enteros");
return de;
}
// * De la clase Record
public ExpresionTipo punto(String campo) {
if (!campos.containsKey(campo))
return new Error("No se encuentra el campo \""+campo+"\"");
return (ExpresionTipo)campos.get(campo);
}
// * De la clase Function
public ExpresionTipo parentesis(List params) {
if (parametros.size()!=params.size())
return new Error("El nmero de parmetros con coincide");
for (int i=0;i<parametros.size();i++) {
ExpresionTipo et=(ExpresionTipo)parametros.get(i);
if (!et.equivalente((ExpresionTipo)params.get(i)))
return new Error("El parmetro nmero "+(i+1)+
" no es equivalente con el valor pasado");
}
return devolucion;
}
Ntese cmo cada uno de estos mtodos implementa la parte del sistema de tipos asociada
a cada uno de los operadores oportunos. En el caso de que el tipo no coincida, se ejecutar
el funcionamiento por omisin devolviendo el tipo error. Los casos correctos y su semntica son:
93
Operador flecha sobre el tipo pointer. Simplemente devuelve el tipo al que apunta.
Operador corchete sobre el tipo array. Si el tipo del ndice es entero, devuelve el tipo
que contiene. En caso contrario devuelve el tipo error con el mensaje oportuno.
Operador punto sobre el tipo record. Si el valor del identificador ubicado a la derecha
del punto coincide con uno de los campos del registro, se devuelve la expresin de tipo
asociada; si no, error.
Operador parntesis sobre el tipo . Si el nmero de parmetros reales y cada un de
sus tipos coincide con el nmero y tipos de los parmetros formales, el tipo inferido es
el tipo que devuelva la funcin void para procedimientos. En caso contrario, error
con un mensaje apropiado.
Una vez el sistema de tipos infiera el tipo de cada expresin, el comprobador de tipos, en la
produccin expresiones, verificar si el tipo inferido es error. En ese caso, mostrar
por la salida estndar de error el nmero de lnea y columna as como el mensaje de error, y
seguir llevando a cabo el anlisis del cdigo fuente. Uno de los beneficios de emplear esta
expresin de tipo especial (error) es que, de un modo sencillo, se puede implementar un
procesador de lenguaje capaz de recuperarse ante los errores de tipo.
En el caso de que el tipo inferido en cada expresin sea correcto, a modo de ejemplo se
muestra el nmero de lnea y columna y la expresin de tipo de tipo inferida. Los mtodos
expresionTipo y getBytes son implementados por cada tipo especfico de un modo
recursivo, haciendo uso de la estructura del patrn Composite.
El siguiente programa de entrada genera la salida mostrada:
Archivo de entrada
VAR
vector:array[1..10] of
integer;
puntero:^integer;
pDoble:^^integer;
v:^array[1..10] of
^char;
w:array[1..10] of ^char;
f:function(integer,
^char):^integer;
p:procedure(^integer);
r:record
dia:integer;
mes:integer;
anio:integer;
end;
BEGIN
45;
'A';
vector[3];
puntero^;
pDoble^^;
pDoble^;
vector[puntero^];
v^[puntero^]^;
w[f(3,w[1])^]^;
p(f(r.dia,w[2]));
END.
94
Linea
Linea
Linea
Linea
Linea
Linea
Linea
Linea
Linea
Linea
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
columna
columna
columna
columna
columna
columna
columna
columna
columna
columna
11:
12:
18:
17:
17:
16:
25:
22:
23:
25:
integer
char
integer
integer
integer
pointer(integer)
integer
char
char
void
Comprobacin de Tipos
Como caso contrario, se muestra un programa en la que cada expresin del mismo posee
un error semntico. Los mensajes son mostrados por el procesador en la salida estndar de
error:
Archivo de entrada
VAR
vector:array[1..10]
of integer;
puntero:^integer;
pDoble:^^integer;
v:^array[1..10]
of ^char;
w:array[1..10]
of ^char;
f:function(integer,
^char):^integer;
p:procedure(^integer);
r:record
dia:integer;
mes:integer;
anio:integer;
end;
BEGIN
i;
vector['a'];
puntero[3];
vector[puntero];
puntero.campo;
puntero^^;
vector^;
f(3);
f(3,3);
f(p(puntero),w[3]);
r.campo;
END.
Posee un error de tipo puesto que no es posible asignar un nmero real a un nmero entero. La parte derecha de la asignacin posee un tipo real. El sistema de tipos del lenguaje
Pascal identifica en una de sus reglas que el producto de un entero y un real devuelve un
95
tipo real. No es necesario ejecutar la aplicacin para que el comprobador de tipos (esttico)
d un mensaje de error en la sentencia de asignacin.
Aquellas comprobaciones de tipos que sean llevadas a cabo en tiempo de ejecucin
por de un procesador de lenguaje se definen como dinmicas. Cualquier verificacin relativa a los tipos puede llevarse a cabo dinmicamente, ya que es en tiempo de ejecucin
cuando se puede conocer el valor exacto que toma una expresin. Sin embargo, aunque
estas comprobaciones son ms efectivas, poseen dos inconvenientes:
Devuelve la divisin de dos valores pasados como parmetros. En funcin de los valores
de ambos parmetros, puede devolver un valor entero (si ambos son enteros y la divisin
posee resto cero) real o racional (en funcin de la implementacin), e incluso podr generar
un error en tiempo de ejecucin si uno de los parmetros es, por ejemplo, una lista. La siguiente tabla muestra invocaciones de ejemplo, la salida del intrprete y, si lo hubiere, el
mensaje dinmico de error generado por el intrprete Allegro Common Lisp 6.2:
Entrada
Salida
Tipo Inferido
(division 2 1)
entero
(division 2 3)
2/3
racional
0.6666667
real
entero
1.0
real
(division 2 3.0)
(division () 1)
96
Comprobacin de Tipos
es procesado por el compilador llevando a cabo las comprobaciones de tipos estticas, resultando todas ellas correctas. En la ejecucin, sin embargo, se produce un error dinmico,
lanzando la mquina virtual de Java la excepcin IndexOutOfBoundsException.
Los sistemas ms sofisticados en la inferencia de tipos se dan en determinados lenguajes funcionales, notablemente en ML, Miranda y Haskell. En estos lenguajes, los programadores tienen la posibilidad de declarar explcitamente los tipos de las variables, en
cuyo caso los compiladores se comportan como el resto de lenguajes con comprobacin
esttica de tipos. No obstante, los programadores tambin pueden rehusar dichas declaraciones dejando al compilador la tarea de inferir los tipos, basndose en los tipos de las
constantes y la estructura sintctica del lenguaje. Lo realmente interesente es que la inferencia y comprobacin de tipos la realiza estticamente el compilador, no siendo necesaria la
ejecucin de la aplicacin57.
Ejemplo 48. El siguiente cdigo es la implementacin de una funcin recursiva del clculo de la
serie de Fibonacci, en el lenguaje ML:
fun fib(n) =
let fun fib_helper(f1, f2, i) =
if i = n then f2
else fib_helper( f2, f1+f2, i+1)
in
fib_helper(0, 1, 0)
end;
La funcin anterior recibe el nmero de la serie como parmetro (n) y nos devuelve su
valor. El cuerpo de la funcin es una invocacin a otra funcin anidada definida dentro de
ella: fib_helper. sta finaliza en el caso de haber sido invocada n veces. En caso contrario, calcula el siguiente valor de la serie.
El compilador de ML asocia el tipo entero al parmetro i, porque se le suma a ste el valor
de la constante 1 en la cuarta lnea. De modo similar, asigna al parmetro n el tipo entero,
ya que esta variable se compara con i en la lnea anterior. La nica invocacin posible a la
funcin fib_helper recibe tres constantes enteras como parmetros; f1, f2 e i sern,
pues, enteros. Puesto que la funcin anidada devuelve en la tercera lnea f2, sta poseer el
tipo entero. Por el mismo motivo, la funcin principal tambin devolver un entero.
Una vez llevado a cabo todo este proceso de comprobacin e inferencia de tipos esttica, el
compilador nos devuelve un mensaje indicndonos la expresin de tipo de la funcin fib:
int int.
Un lenguaje posee un sistema de tipos fuerte58 si es capaz de asegurar que no se
vaya a aplicar una operacin a un elemento del lenguaje que no soporte dicha operacin
(que no sea de dicho tipo), detectndose siempre los errores de tipo ya bien sea esttica o
57
Existen multitud de lenguajes que no requieren la declaracin de los tipos de las variables u objetos:
Smalltalk, Python, Perl y la mayora de lo lenguajes de Scripting (TCL, PHP o {J,Java,ECMA}Script).
Sin embargo, la inferencia de tipos de todos ellos se realiza dinmicamente.
58
Strongly typed. En ocasiones traducido como fuertemente tipificado.
97
dinmicamente [Scott00]. Determinados errores de tipo slo pueden ser detectados dinmicamente.
Ejemplo 49. El siguiente cdigo Java ha de implementar comprobaciones dinmicas y estticas
para hacer que el programa no posea errores de tipo:
import java.io.*;
public class AccesoArray {
public static void main(String args[]) throws Exception {
int array[]=new int[10];
int i=Integer.parseInt( (new BufferedReader(
new InputStreamReader(System.in))).readLine() );
System.out.println(array[i]);
}
}
Safe.
98
Comprobacin de Tipos
((vector<int>*)s)->push_back(3);
return 0;
}
El anterior es un programa vlido para un compilador de C++. La segunda lnea del programa principal ahorma la cadena de caracteres a un puntero a vector de enteros, e intenta insertar un 3 al final. El resultado de la ejecucin del programa anterior es imprevisible
y el comit ISO/ANSI la deja como dependiente de la implementacin [ANSIC++].
La mayor parte de los lenguajes que poseen comprobacin esttica de tipos requieren adicionalmente comprobacin dinmica, para poder ofrecer un sistema de tipos fuerte.
Ada es un ejemplo de este caso. Los campos variantes de los registros de Ada son compilados del mismo modo que en Pascal. Sin embargo, en Ada se genera adicionalmente cdigo
para, en tiempo de ejecucin, comprobar que el acceso a un campo variante sea acorde con
el valor del campo selector. Pascal no lleva a cabo esta comprobacin.
La siguiente tabla muestra la relacin existente entre la seguridad60 respecto al tipo y el momento en el que se lleva a cabo las comprobaciones, refirindonos a casos reales
de lenguajes de programacin:
Sin comproba- Slo Comproba- Comprobacin Est- Slo Comprocin de tipos
cin Esttica
tica y Dinmica
bacin Dinmica
Safe
C#, Miranda,
Haskell, ML.
Unsafe Ensamblador,
BCPL
Safety.
99
if (expresion2.et == pointer(t))
es semnticamente correcto. Un compilador que implemente el estndar ISO/ANSI aceptar el programa como vlido, generando un cdigo objeto. Tanto en las dos ltimas asignaciones del programa principal, como en las dos invocaciones a funciones, aparece la
equivalencia estructural de tipos que dicho lenguaje implementa en su sistema de tipos.
En 6.3 indicbamos cmo el modo ms comn de modelar las expresiones de tipo en un procesador de lenguaje era mediante estructuras compuestas recursivamente. stas se crean de un modo jerrquico, pudiendo dar lugar a estructuras de rbol o ms eficientes estructuras de grafo acclicos dirigidos (DAGs). En el caso de que se estn representando las expresiones de tipo con un DAG, cada expresin de tipo tendr una nica
representacin en memoria. Por tanto, la comprobacin de equivalencia, en este caso, se
61
Ntese que no se est abordando el problema de la conversin, implcita o explcita, de tipos que se da
en muchos lenguajes de programacin esto ser tratado en el siguiente punto. Lo que se est acometiendo aqu es la equivalencia (correspondencia o igualdad) entre tipos.
100
Comprobacin de Tipos
reduce a verificar la identidad del nodo: si los dos nodos del DAG son el mismo, entonces
las expresiones de tipo son estructuralmente equivalentes.
En el cado de implementar las expresiones de tipo con una estructura de rbol, hace
que stas puedan estar repetidas y que, por tanto, la equivalencia estructural de tipos no est
ligada a la identidad de los nodos del rbol. El proceso que deber llevarse a cabo ser
comprobar recursivamente que las estructuras de los dos nodos sean equivalentes.
Ejemplo 53. En el Ejemplo 43 se desarroll una definicin dirigida por sintaxis que implementaba un sistema de tipos de un subconjunto del Pascal. Los tipos simples definidos eran
char, integer, subrango, void y error. Los constructores de tipo que tenamos eran
pointer, array, (funcin) y record.
Para poder implementar un sistema de tipos con equivalencia estructural, el sistema de tipos se centrar en una funcin equivale que recibir dos expresiones de tipo, representadas mediante un rbol, y devolver si ambas son o no equivalentes.
boolean
if
if
if
if
if
La comparacin realizada es respecto a las dos clases de los dos objetos Java. En
ANSI/ISO C++ esta funcionalidad se puede obtener aplicando el operador de igualdad a
la devolucin del operador typeid62.
62
Esta funcionalidad del ISO/ANSI C++ se denomina RTTI y ser explicada con ms detenimiento en
6.7.
101
La comparacin estructural de los tipos construidos se obtiene mediante un proceso recursivo de comparacin de equivalencia. La implementacin es muy sencilla:
// * De la clase Pointer
public boolean equivalente(ExpresionTipo et) {
return et instanceof Pointer &&
a.equivalente(((Pointer)et).a);
}
// * De la clase Array
public boolean equivalente(ExpresionTipo et) {
if (!(et instanceof Array))
return false;
Array array=(Array)et;
return array.desde==desde && array.hasta==hasta &&
array.de.equivalente(de);
}
// * De la clase Function
public boolean equivalente(ExpresionTipo et) {
if (!(et instanceof Function))
return false;
Function function=(Function)et;
if (parametros.size()!=function.parametros.size())
return false;
for (int i=0;i<parametros.size();i++)
if (!((ExpresionTipo)parametros.get(i)).equivalente(
(ExpresionTipo)function.parametros.get(i)))
return false;
return devolucion.equivalente(function.devolucion);
}
// * De la clase Record
public boolean equivalente(ExpresionTipo et) {
if (!(et instanceof Record))
return false;
Record record=(Record)et;
if (campos.size()!=record.campos.size())
return false;
Iterator it=record.campos.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry=(Entry) it.next();
// * Tienen que tener igual nombre para los campos
String key=(String)entry.getKey();
if (!campos.containsKey(key))
return false;
ExpresionTipo expTipo=(ExpresionTipo)entry.getValue();
if (!expTipo.equivalente((ExpresionTipo)campos.get(key)))
return false;
}
return true;
}
102
Comprobacin de Tipos
La nica sentencia del programa principal resulta un error semntico. Aunque la estructura de las dos clases A y B coincida, y ambas implementen los dos tipos IA e IB, no
existe equivalencia al ser los nombres de los tipos distintos.
Existe un caso particular que se puede dar en aquellos lenguajes que permitan definir nuevos identificadores de tipo como type en Pascal, Ada y Haskell, o typedef en
C++. Dicho caso se da cuando un tipo se define igual que el nombre de otro tipo, establecindose as como un alias del segundo. De este modo, si un lenguaje con equivalencia de
tipos basada en nombres identifica un alias de un tipo como equivalente al tipo al que hace
referencia, se dice que posee equivalencia de declaracin.
Ejemplo 56. En el siguiente programa en Modula-2:
103
MODULE EquivalenciaDeclaracion.
TYPE
celsius = REAL;
fahrenheit = REAL;
centigrados = celsius;
VAR
c: celsius;
f: fahrenheit;
m: centigrados;
BEGIN
f := c; (* Error *)
m := c; (* OK *)
END
EquivalenciaDeclaracion.
104
Comprobacin de Tipos
Hace que el segundo bucle sea un 4,33% ms rpido que el primero, gracias a que no tiene
que efectuar conversiones (implcitas) de tipo en tiempo de ejecucin.
La mayor parte de los lenguajes compilados tienen en la actualidad a reducir la
coercin de tipos. Un ejemplo es la eliminacin, por parte del lenguaje Java, de conversiones implcitas del C tales como la interpretacin de enteros como valores lgicos. En Java
existe el tipo boolean (bool en C++) que es incompatible (implcita y explcitamente)
con cualquier otro tipo.
Por otro lado, existen diseadores de lenguajes que opinan que la coercin de tipos
ofrece un modo natural de ofrecer extensibilidad de las abstracciones, haciendo ms sencilla la utilizacin de nuevos tipos en conjuncin con los que ofrece el lenguaje. C++, en
particular, ofrece mecanismos extremadamente ricos para establecer coercin de tipos. Al
procurar un mecanismo tan rico de coercin, las reglas del sistema de tipos de este lenguaje
hacen que dichos mecanismos sean complejos de entender y emplear correctamente.
Ejemplo 58. El siguiente programa en C++ define una clase Entero, con conversin implcita a
dos tipos bsicos del lenguaje: int y double.
#include <iostream>
using namespace std;
class Entero {
int entero;
public:
Entero(int n) { entero=n; }
operator double() const { return entero; }
int getEntero() const { return entero; }
Entero operator+(const Entero &e) const {
return Entero(entero+e.entero);
105
}
};
void fe(Entero n) { cout<<n.getEntero()<<endl; }
void fd(double n) { cout<<n<<endl; }
int main() {
fe(3);
Entero e(3);
fd(e);
return 0;
}
106
Comprobacin de Tipos
siendo r una variable real e i entera, genera en Java un error de compilacin indicando que puede haber una prdida de precisin. Para asumir la posible prdida, el programador deber realizar la conversin de un modo explcito, mediante el operador de
ahormado (cast):
i=(int)r;
Ahora el programador asume el riesgo de perder la parte decimal del real y el compilador aceptar la conversin.
Las conversiones explcitas (al igual que las implcitas) suelen generar cdigo adicional que ser ejecutado en tiempo de ejecucin. En algunos lenguajes con sistemas de
tipos fuertes suelen ejecutar, adicionalmente, un cdigo de validacin de la conversin,
verificando que no se produzca desbordamiento. Este tipo de conversiones explcitas
siempre llevan a cabo una modificacin de la representacin interna de la informacin.
Sin embargo, existe en ocasiones la necesidad de convertir el tipo de una expresin,
sin que se produzca una modificacin de la representacin interna de la informacin. Lo
que se busca es poder aplicar operaciones de un tipo distinto al real, sin que se cambie la
representacin interna de la informacin. Esta posibilidad hace que un lenguaje, por definicin, no sea seguro. Un posible escenario en el que puede interesar hacer conversiones
explcitas, sin convertir la representacin de la informacin, es en la implementacin de un
nuevo algoritmo de gestin de memoria.
Ejemplo 60. En el lenguaje C, el modo de hacer conversiones explcitas sin modificar la
representacin de los datos, es mediante los ahormados a punteros. Puesto que se permite
la conversin entre punteros de distintos tipos, lo buscado se puede obtener del siguiente
modo:
#include <iostream>
65
107
template<typename Tipo>
Tipo *nuevo() {
const unsigned numeroBytes=100;
static unsigned ultimo=0;
static char memoria[numeroBytes];
if (ultimo+sizeof(Tipo)<=numeroBytes) {
ultimo+=sizeof(Tipo);
return (Tipo*)(memoria+ultimo-sizeof(Tipo));
}
return 0;
}
int main() {
unsigned max=0;
int *p=nuevo<int>();
*p=3;
long double*q=nuevo<long double>();
*q=34.65;
std::cout<<*p<<'\t'<<*q<<std::endl;
return 0;
}
El programa anterior implementa una funcin nuevo capaz de ofrecer memoria para cualquier tipo, con un tamao limitado a 100 bytes66. Primero se aplica, en la funcin nuevo, la
conversin de un puntero de caracteres a un puntero a cualquier tipo. Posteriormente, en el
programa principal, se aplica el operador * para convertir el tipo sin modificar la representacin interna de la informacin en este caso, un array de caracteres.
Ejemplo 61. El lenguaje C++ sigue empleando el operador cast heredado del C. Sin embargo,
para distinguir las distintas semnticas de dicho operador, ha creado distintas versiones del
mismo. En este ejemplo compararemos su operador static_cast que efecta una conversin de la representacin interna del dato a convertir, con el reinterpret_cast que
no realiza tal modificacin.
#include <iostream>
using namespace std;
int main() {
double d=33.44;
int i1,i2;
i1=static_cast<int>(d);
i2=reinterpret_cast<int&>(d);
cout<<i1<<'\t'<<i2<<endl;
return 0;
}
El programa anterior hace que el entero i1 posea el valor 33, ejecutndose el cdigo de
conversin en tiempo de ejecucin. De forma contraria, el valor i2 depende de la plataforma empleada, puesto que no modifica la representacin interna del double, mostrando
su valor como si de un entero se tratase.
Como hemos mencionado, en los lenguajes orientados a objetos es comn tener un
tipo general para cualquier objeto. En C++ es void*; en Modula-2, address; en Modula-3 refany; en Eiffel y Clu, any; en Java y C#, Object. Este tipo genrico es utilizado a
la hora de implementar clases contenedoras de cualquier objeto (vectores, listas, pilas, colas...). La conversin de cualquier objeto es implcita, puesto que estas referencias, o bien
no poseen ninguna operacin (void*), o bien su conjunto de operaciones es tan reducido
66
En ISO/ANSI C++, el tamao de un char es un byte. Los caracteres extendidos se obtienen con el
tipo wchar_t.
108
Comprobacin de Tipos
Esta conversin suele denominarse downcast, por el sentido descendente que sigue en la jerarqua de
herencia.
109
#include <iostream>
#include <string>
using namespace std;
class Base {
public:
~Base() {}
virtual const char *quienSoy() {return "Base";}
};
class Derivada: public Base {
public:
virtual const char *quienSoy() {return "Derivada";}
};
int main() {
Base *base=new Derivada;
Derivada *derivada=dynamic_cast<Derivada*>(base);
if (derivada)
cout<<"Conversin correcta.\n"<<
"Quien es: "<<derivada->quienSoy()<<".\n"<<
"Tipo: "<<typeid(*derivada).name()<<'.'<<endl;
else
cout<<"Conversin errnea.\n";
return 0;
}
El operador dynamic_cast lleva a cabo la comprobacin dinmica del tipo del objeto
apuntado. Si ste posee un tipo compatible con el solicitado, devuelve un puntero al tipo
demandado apuntando al objeto. En caso contrario devuelve 0. Otra caracterstica de RTTI
es el operador typeid que devuelve un objeto type_info con informacin respecto a su
tipo. En el ejemplo anterior, la conversin es correcta y se muestra que el tipo es
Derivada68.
expresion
expresion
expresion1
cte_entera
cte_real
expresion2 + expresion3
110
Comprobacin de Tipos
Se define una gramtica atribuida para calcular el cdigo a generar, en la que se infiere el
tipo de cada expresin:
P
(1)
(2)
(3)
R
expresion.tipo = I
expresion.codigo = PUSH_INT + cte_entera.valor
expresion.tipo = F
expresion.codigo = PUSH_FLT + cte_real.valor
expresion1.tipo=mayorTipo(expresion2.tipo,expresion3.tipo)
expresion1.codigo=expresion2.codigo +
coercion(expresion1.tipo,expresion2.tipo) +
expresion3.codigo +
coercion(expresion1.tipo,expresion3.tipo) +
suma(expresion1.tipo)
char mayorTipo(char t1,char t2) {
if ( t1 == F || t2 == F ) return F;
return I;
}
String coercion(char t1,char t2) {
if ( t1 == F && t2 == I ) return INT2FLT;
return ;
}
String suma(char tipo) {
if ( t1 == F) return ADD_FLT;
return ADD_INT;
}
Para la expresin 3+4+3.4, el cdigo a generar (el valor del atributo expresion.codigo) es la consecucin de las sentencias PUSH_INT 3 (apilar un entero),
PUSH_INT 4, ADD_INT (apilar los dos enteros en el tope de la pila), INT2FLT (convertir el entero en la pila a un real), PUSH_FLT 3.4 (apilar un real), ADD_FLT (sumar los
dos reales de la pila). Vemos cmo la gramtica atribuida infiere el tipo y, basndose en
ste, resuelve la sobrecarga: la primera suma se resuelve como una operacin entera, cuando la segunda es real.
En este ejemplo se muestra la relacin entre el concepto de sobrecarga y el de coercin de
tipos, existente en la mayora de lenguajes. Si un operador est sobrecargado es porque se
define para distintos tipos de operandos. Si el operador es binario (como el caso de la suma), puede ser que los dos operandos posean el mismo tipo, pero tambin es comn que
no sea as. Es en ese caso (en nuestro ejemplo, la suma de un entero y un real y viceversa)
es cuando, adems de resolver la sobrecarga, es necesario que el compilador realice una
conversin implcita de tipos.
El concepto de sobrecarga se emplea tambin para funciones y mtodos, donde el
mismo identificador se puede utilizar para implementaciones distintas. Comnmente, el
criterio necesario para sobrecargar un mtodo o funcin implementado previamente es
modificar el nmero de parmetros, o el tipo de alguno de ellos. En este contexto, la resolucin de la sobrecarga supone conocer a qu mtodo o funcin invocar. La resolucin se
llevar cabo a partir del nmero y tipo de los parmetros reales. El comprobador de tipos
deber tomar todos los tipos asociados a un identificador y resolver cul es el correcto ante
una invocacin dada.
111
La sobrecarga aade complejidad a un sistema de tipos de un compilador. Si unimos a sta otras caractersticas ya mencionadas, como la coercin de tipos, pueden darse
contextos ambiguos en los que la sobrecarga no pueda ser resuelta, y se tenga que generar
un error de compilacin.
Ejemplo 65. El siguiente programa en C++:
#include <iostream>
using namespace std;
void f(double,float) { cout<<"f(double,float)\n"; }
void f(float,double) { cout<<"f(float,double)\n"; }
int main() {
f(1.0,2.0); // Error
f(1.0f,2.0); // f(float,double)
f(1.0,2.0f); // f(double,float)
f(1.0f,2.0f); // Error
return 0;
}
Posee dos lneas en las que las invocaciones a una de las funciones f son ambiguas: la primera y la ltima. Puesto que los parmetros han de ser double y float, o viceversa, la
invocacin con dos double o dos float es ambigua69.
Un smbolo (operador o funcin) es polimrfico respecto al tipo si define una semntica independiente de los tipos a los que se aplique. Para que un smbolo sea polimrfico, deber definir un nico comportamiento para todos los tipos del sistema.
Un operador polimrfico del lenguaje C es el operador de direccin &. Este operador se puede aplicar a cualquier lvalue de un tipo T, devolviendo un puntero a T. La semntica no vara y se puede aplicar a determinados elementos, indistintamente del tipo que posean. En el lenguaje de programacin ML, el operador de comparacin de igualdad (=) es
polimrfico, puesto que acepta dos expresiones del mismo tipo y devuelve un valor lgico.
Sin embargo, los operadores de comparacin <, <=, >= y > no son polimrficos puesto
que, de forma contraria al operador de igualdad, no se pueden aplicar a cualquier tipo.
El modo de representar las expresiones de tipos polimrficas mediante la notacin
introducida por Alfred Aho ( 6.3) es empleando variables de tipo: variables que representan, dentro de una expresin de tipo, cualquier aparicin de otra expresin de tipo. Una
variable de tipo sirve para representar cualquier tipo dentro de otra expresin de tipo. Para
indicar que una variable de tipo podr ser sustituida por cualquier tipo, se utiliza el cuantificador universal . As, la expresin de tipo del operador & del lenguaje C tendr la siguiente expresin de tipo:
. pointer()
La expresin de tipos anterior indica que el operador & se puede aplicar sobre cualquier tipo del lenguaje y devuelve un puntero al tipo al que haya sido aplicado.
Ejemplo 66. En los entornos interactivos del lenguaje de programacin ML70, dado un smbolo
el entorno nos muestra su expresin de tipo asociada. As, podemos preguntarle a sistema
por la expresin de tipo del operador =. El entorno mostrar la siguiente expresin de tipo:
a * a -> bool
69
En el lenguaje de programacin C++, las constantes 1.0 y 2.0 son ambas double. Para que sean
float hay que aadirles el sufijo f (o F); para que sean long double, el sufijo es l (o L).
70
Como el sistema interactivo Standard ML of New Jersey (SML/NJ).
112
Comprobacin de Tipos
En ML, la expresin de tipo a indica que a es una variable de tipo al que se le aplica el
cuantificador universal el apstrofo. De este modo, el operador de igualdad en ML es
polimrfico, y su tipo asociado es una funcin que recibe un producto de cualquier par de
tipos, iguales entre s, devolviendo un valor lgico. El tipo de datos al que se le puede aplicar el operador binario = puede variar en cada invocacin; sin embargo, ambos operandos
han de ser del mismo tipo ya que en la expresin de tipo aparece la misma variable de
tipo, a.
El polimorfismo de tipos tambin se aplica al concepto de funcin y mtodo. Una
funcin o mtodo polimrfico es aqulla que es capaz de recibir y devolver parmetros de
cualquier tipo. En muchos lenguajes de programacin como C++, Eiffel o Ada esta
caracterstica es definida como genericidad71. El polimorfismo tambin es implementado
por lenguajes funcionales como ML, Haskell o Miranda.
El principal beneficio de la utilizacin de funciones polimrficas es que permiten
implementar algoritmos que manipulan estructuras de datos, independientemente del tipo
de elemento que contengan. De este modo, las implementaciones pueden desarrollarse de
un modo independiente al tipo, dejando al procesador de lenguaje la tarea de sustituir, en
cada invocacin, las variables de tipo por los tipos concretos.
Ejemplo 67. Supngase que, en el lenguaje de programacin C, se desea crear una funcin que
nos devuelva la longitud de una lista enlazada de enteros. Una implementacin sera la siguiente:
typedef struct s_l {
int informacion;
struct s_l *siguiente;
} lista;
unsigned longitud(lista *l) {
unsigned lon=0;
while (l) {
lon++;
l=l->siguiente;
}
return lon;
}
113
template<typename Tipo>
struct Lista {
int informacion;
Lista<Tipo> *siguiente;
};
template<typename Tipo>
unsigned longitud(Lista<Tipo> *l) {
unsigned lon=0;
while (l) {
lon++;
l=l->siguiente;
}
return lon;
}
El cuantificador universal en C++ se define mediante identificadores dentro de una plantilla (template). Ntese cmo, en esta versin, la funcin longitud es vlida para cualquier lista, independientemente del tipo que contenga.
Un procesador de un lenguaje que ofrezca polimorfismo, deber implementar un
mecanismo de unificacin: proceso de encontrar una sustitucin para cada variable de tipo
con otra expresin de tipo, acorde al empleo del operador o funcin polimrfico. Puesto
que el operador & del lenguaje C posee la expresin de tipo:
. pointer()
Al utilizar este operador en la expresin &i, donde i es una variable entera, el algoritmo de unificacin encontrar la sustitucin de por integer como vlida y, por tanto,
se determinar el tipo de la expresin &i como pointer(integer). Las variables de
tipo que reciben un valor tras aplicar una sustitucin, se dice que son instanciadas.
Un mecanismo de unificacin es una tcnica potente. Adems de su utilizacin en
la inferencia de tipos polimrficos [Milner78], desarrolla un papel fundamental en el modelo computacional del lenguaje Prolog. Los algoritmos de unificacin tambin se emplean
para tcnicas de reconocimiento o emparejamiento de patrones72, ampliamente utilizados
en lenguajes como Perl o awk.
Cabe mencionar que en los lenguajes orientados a objetos, el trmino polimorfismo
suele emplearse para lo que realmente es polimorfismo de subtipos (herencia): poder referirse, mediante un supertipo, a cualquier tipo derivado. Puesto que todos los tipos derivados ofrecen las operaciones de un tipo base, el compilador acepta el tratamiento polimrfico de objetos derivados mediante referencias a su supertipo. Ntese cmo esto es un subconjunto de la amplitud del concepto de polimorfismo, que implica un tratamiento genrico para cualquier tipo no slo para las clases que heredan de una dada.
6.9. Inferencia de Tipos
Aunque ya hemos utilizado el concepto de inferencia de tipos a lo largo de todo este libro, este trmino puede definirse como el problema de determinar el tipo de una construccin del lenguaje. sta es la tarea fundamental de un sistema de tipos. Hemos visto
cmo existen lenguajes con inferencia esttica de tipos (tiempo de compilacin) e inferencia
dinmica (tiempo de ejecucin).
Como hemos descrito a lo largo del punto 1 de este libro, la inferencia de tipos es
un problema de naturaleza compleja en el que se ha de tener en cuenta:
72
Pattern matching.
114
Comprobacin de Tipos
73
Por ejemplo, en el lenguaje de programacin Java, aunque se ahorme una expresin entera a otra lgica, esta conversin no se permite.
74
Ambos presentes en el lenguaje C++.
115
CUESTIONES DE REVISIN
1. Qu determina si una regla de un lenguaje es parte del anlisis semntico o
anlisis sintctico? Ponga un par de ejemplos para cada caso.
2. Defina semntica de un lenguaje de programacin. Enumere los distintos tipos
de lenguajes de especificacin semntica que conozca.
3. Por qu es imposible detectar ciertos errores en tiempo de compilacin? Ponga
tres ejemplos.
4. Qu significa anotar o decorar un rbol sintctico?
5. Defina y explique los conceptos de rbol sintctico, rbol sintctico abstracto,
sintaxis concreta y sintaxis abstracta de un lenguaje de programacin.
6. Qu es un atributo de un smbolo gramatical? Qu es una regla semntica?
7. Qu es una gramtica atribuida?
8. Indique las diferencias entre una gramtica atribuida y una definicin dirigida
por sintaxis.
9. Qu un atributo calculado en una produccin?
10. Cul es la diferencia entre un atributo heredado y uno sintetizado? Cmo se
representa dicha diferencia en un rbol sintctico?
11. Qu es un compilador de una pasada? Qu ventajas e inconvenientes ofrece
respecto a los compiladores de varias pasadas?
12. Indique qu es un grafo de dependencias de una gramtica atribuida.
13. Qu es un ordenamiento topolgico de un grafo de dependencias de una gramtica atribuida? Cul es su principal utilidad?
14. Con qu tipo de recorrido de un AST puede evaluarse una gramtica Satribuida? Y una L-atribuida?
15. Indique las diferencias entre una definicin dirigida por sintaxis y un esquema
de traduccin.
16. Qu significa que una gramtica sea S-atribuida y L-atribuida? Qu implica y
por qu es una faceta importante?
17. Qu es una gramtica atribuida completa? Qu relacin existe con el concepto
de gramtica atribuida bien definida? Por qu es un concepto importante?
18. Cul es el significado de evaluar una gramtica?
19. Indique, adems las gramticas atribuidas S y L-atribuidas, cules pueden ser
evaluadas, a partir de su rbol sintctico, con una nica visita de cada uno de sus
nodos.
20. Indique qu tipo de recorrido ha de efectuarse sobre un AST para poder evaluar
una gramtica L-atribuida visitando una sola vez cada nodo del rbol.
21. Es posible convertir una gramtica L-atribuida a S-atribuida? Y en el sentido
contrario? Razone ambas respuestas.
117
22. Indique cuales son los dos mtodos principales empleados para evaluar una
gramtica atribuida. Explique ambos.
23. Cul es la diferencia entre una regla semntica y una rutina semntica?
24. Algunos compiladores realizan el anlisis semntico al mismo tiempo que el sintctico. Otros, en fase de anlisis sintctico, crean un AST y posteriormente realizan las comprobaciones semnticas. Indique las ventajas e inconvenientes de
ambas alternativas.
25. Bajo qu circunstancias puede calcularse un atributo heredado en una herramienta de anlisis sintctico ascendente?
26. Cules son los principales objetivos de la existencia de tipos en los lenguajes de
programacin?
27. De cuntas formas distintas podra definirse el concepto de tipo?
28. Defina comprobacin de tipos esttica y dinmica. Indique dos lenguajes que
posea cada una de ellas.
29. Ponga un ejemplo de dos comprobaciones dinmicas de tipo que realice un lenguaje que usted conozca.
30. Cul es la diferencia entre equivalencia de tipos y compatibilidad de tipos?
Ponga dos ejemplos.
31. Defina y relacione expresin de tipo, sistema de tipos y comprobador de tipos.
32. Explique las diferencias surgidas a la hora de representar expresiones de tipo
mediante estructuras de rboles o grafos acclicos dirigidos.
33. Qu significa que un lenguaje sea fuerte respecto al tipo? Y que sea seguro?
Cite caractersticas del lenguaje C que hacen que no sea seguro.
34. Qu relacin existe entre el concepto de lenguaje seguro, comprobacin esttica de tipos y comprobacin dinmica de tipos?
35. Cmo suelen gestionar en tiempo de ejecucin los errores de tipo los lenguajes
seguros?
36. Qu es la coercin y conversin de tipos?
37. Qu significa que un smbolo est sobrecargado?
38. Defina polimorfismo de tipos. Dnde se suele dar en los lenguajes de programacin. Ponga dos ejemplos de cada caso.
39. Defina y explique los distintos tipos de equivalencia de tipos que conoce.
40. Qu relacin existe entre la sobrecarga de operadores y las coerciones de un
lenguaje de programacin?
41. Indique las diferencias existentes entre un operador polimrfico y un sobrecargado. Ejemplifquelo con dos casos.
42. Cite complejidades del problema de la inferencia de tipos en un lenguaje como
el C++?
43. Qu es un algoritmo de unificacin? En qu lenguajes se emplea?
44. Cite las caractersticas polimrficas del lenguaje C++.
118
EJERCICIOS PROPUESTOS
1. Dada la sentencia 3*(61+-3), vlida para la gramtica atribuida del Ejemplo 11,
muestre el rbol sintctico decorado y un AST alternativo. Cul sera su sintaxis
abstracta?
2. Ample la gramtica atribuida del Ejemplo 14 para que sea capaz de utilizar todas
las bases de 2 a 16. La base se pospondr al nmero entre llaves. Por defecto ser
base 10. Ejemplos 0110{2}, 123{10}, af45{16}, 981.
3. Respecto a la gramtica atribuida del Ejemplo 14. Puede evaluarse con una nica
pasada, visitando una nica vez cada nodo del rbol sintctico? Si optamos por
hacer un compilador de varias pasadas, se podra decorar su AST con una sola visita de sus nodos?
4. Implemntese, mediante el patrn de diseo Visitor, un evaluador de la gramtica
del Ejemplo 14.
5. Implemente una gramtica atribuida capaz de traducir expresiones infijas a expresiones prefijas. Por ejemplo, traducir de a/2+10*c a + / a 2 * 10 c.
6. Escriba una gramtica atribuida para calcula el valor real del nmero descrito por el
no terminal real de la siguiente gramtica:
real num . num
num num digito
| digito
digito 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Escriba una gramtica atribuida para comprobar si el rbol binario es de bsqueda. Como ejemplo, (2 (1 nil nil) (3 nil nil)) es un rbol de
bsqueda, pero no lo es (2 (0 nil nil) (1 nil nil)).
8. Dada la siguiente gramtica atribuida GA={G,A,R,B} siendo G y R:
P
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
P
S
MasLogico1
MasLogico1
MasLogico
Logico
Logico
Logico
Termino
Termino
Logico MasLogico
AND Logico MasLogico2
OR Logico MasLogico2
Termino1 + Termino2
Termino1 Termino2
Termino
CTE_ENTERA
CTE_REAL
R
119
y siendo B:
P
(2) MasLogico1.inicial==ENTERO
Logico.tipo==ENTERO
(3) MasLogico1.inicial==ENTERO
Logico.tipo==ENTERO
Es la gramtica GA una gramtica S-atribuida? Es la gramtica GA una gramtica L-atribuida? Pertenece la sentencia 3+4.5 al lenguaje definido por la gramtica GA? Pertenece la sentencia 3-2 OR 3+2.1 al lenguaje definido por la
gramtica GA? Es la gramtica GA una gramtica atribuida completa? Est
GA bien definida?
9. Muestre el grafo de dependencias del ejercicio anterior.
10. Dada la siguiente gramtica G libre de contexto:
P
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
S
logico
implica
implica
masLogico1
masLogico1
masLogico
masImplica1
masImplica
logico masLogico
implica masImplica
true
false
AND logico masLogico2
OR logico masLogico2
120
Ejercicios Propuestos
Especifquese sobre ella una gramtica atribuida bien definida que restrinja el
lenguaje reconocido a aquellos programas que posean igual nmero de tokens A
y B. Demustrese su condicin de gramtica completa.
14. Indique una representacin de las expresiones de tipo de las clases del Pascal para
los cuatro puntos que hemos identificado en el Ejemplo 41.
15. Dado el siguiente cdigo C:
typedef struct {
int a, b;
} nodo, *ptr_nodo;
nodo aa[100];
ptr_nodo bb(int x, nodo y);
19. Implemente el Ejemplo 41, creando las expresiones de tipo mediante un DAG en
lugar de empleando un rbol.
20. Identifique las expresiones de tipo necesarias y escriba una definicin dirigida por
sintaxis para realizar un comprobador de tipos de comparaciones de igualdad de un
pseudopascal con las siguientes caractersticas:
121
w:array[1..10] of ^char;
BEGIN
45=vector[3];
pDoble^^=puntero^;
puntero=pDoble^;
puntero^=vector[puntero^];
v^[puntero^]^='3';
v^=w;
END.
VAR
VAR
pChar:^char;
puntero:^integer;
pInt:^integer;
pDoble:^^integer;
BEGIN
BEGIN
pChar=pInt;
puntero=pDoble;
END.
END.
VAR
VAR
v:ARRAY[1..10] OF char;
v:ARRAY[1..10] OF char;
BEGIN
BEGIN
v['a']='a';
v[3]=3;
END.
END.
VAR
VAR
v:ARRAY[1..10] OF char;
v:ARRAY[1..10] OF char;
w:ARRAY[1..10] OF integer;
w:ARRAY[1..11] OF char;
BEGIN
BEGIN
v=w;
v=w;
END.
END.
21. Implemente, mediante el patrn de diseo Composite y comprobador de tipos equivalente a la definicin dirigida por sintaxis del ejercicio anterior.
22. Dada la siguiente gramtica:
|
|
|
|
|
|
<S>
<var>
<exp>
<term>
<masTerm>
<fact>
<masFact>
<var> = <exp>
ID
<term> <masTerm>
<fact> <masFact>
+ <term> <masTerm>
- <term> <masTerm>
CTE_ENTERA
CTE_REAL
( <exp> )
* <fact> <masFact>
/ <fact> <masFact>
Disee sobre ella una definicin dirigida por sintaxis para que el atributo
var.tipo infiera el tipo adecuado, entero o real, en la asignacin. Adems, el
atributo var.valor ha de poseer la evaluacin de la expresin asignada, siguiendo las precedencias tpicas en los lenguajes de programacin.
23. Supngase las declaraciones generadas por la siguiente gramtica:
(1)
(2)
(3)
(4)
(5)
122
D
L
L
T
T
id L
, id L
: T
integer
real
Ejercicios Propuestos
Construya un esquema de traduccin para introducir el tipo de cada identificador en una tabla de smbolos.
24. La siguiente expresin en el lenguaje C:
(t)-x
123
Evaluacin de un AST
125
ExpresionUnaria.java
package ast;
/**
* Nodo expresin unaria del AST.<br/>
* 13-oct-2005<br/>
* Procesadores de Lenguaje<br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo<br/>
* @author Francisco Ortin
*/
public class ExpresionUnaria extends Expresion {
/**
* El operador unario de la expresin
*/
public String operador;
/**
* El operando de la expresin unaria
*/
public Expresion operando;
/**
* @param operador El operador utilizado para construir la expresin
* @param operando La expresin a la que se aplica el operador unario
*/
public ExpresionUnaria(String operador,Expresion operando) {
this.operador=operador;
this.operando=operando;
}
/**
* Mtodo necesario para hacer la invocacin correcta a los
* mtodos visitar de los visitor
* @param visitor Recorrido que aceptar el rbol
* @param param Un parmetro opcional para hacer ms verstil el recorrido
* @return Un posible valor devuelto al finalizar el recorrido
*/
public Object aceptar(visitor.Visitor visitor, Object param) {
return visitor.visitar(this,param);
}
}
ExpresionBinaria.java
package ast;
/**
* Nodo expresin binaria del AST.<br/>
* La asignacin es un caso particular de sta.<br/>
* 13-oct-2005<br/>
* Procesadores de Lenguaje<br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo<br/>
* @author Francisco Ortin
*/
public class ExpresionBinaria extends Expresion {
/**
* El operador unario de la expresin
*/
public String operador;
/**
* Los operandos de la expresin binaria
*/
public Expresion operando1;
/**
* Los operandos de la expresin binaria
*/
public Expresion operando2;
/**
* @param operador El operador utilizado para construir la expresin binaria
* @param operando1 El primer operando de la expresin
* @param operando2 El segundo operando de la expresin
*/
public ExpresionBinaria(String operador, Expresion operando1, Expresion operando2) {
this.operador=operador;
this.operando1=operando1;
126
Evaluacin de un AST
this.operando2=operando2;
}
/**
* Mtodo necesario para hacer la invocacin correcta a los
* mtodos visitar de los visitor
* @param visitor Recorrido que aceptar el rbol
* @param param Un parmetro opcional para hacer ms verstil el recorrido
* @return Un posible valor devuelto al finalizar el recorrido
*/
public Object aceptar(visitor.Visitor visitor, Object param) {
return visitor.visitar(this,param);
}
}
ConstanteEntera.java
package ast;
/**
* Nodo constante entera del AST.<br/>
* 13-oct-2005<br/>
* Procesadores de Lenguaje<br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo<br/>
* @author Francisco Ortin
*/
public class ConstanteEntera extends Expresion {
/**
* El valor de la constante entera
*/
public int valor;
/**
* @param valor Valor de la constante entera
*/
public ConstanteEntera(int valor) {
super('I');
this.valor=valor;
}
/**
* Mtodo necesario para hacer la invocacin correcta a los
* mtodos visitar de los visitor
* @param visitor Recorrido que aceptar el rbol
* @param param Un parmetro opcional para hacer ms verstil el recorrido
* @return Un posible valor devuelto al finalizar el recorrido
*/
public Object aceptar(visitor.Visitor visitor, Object param) {
return visitor.visitar(this,param);
}
}
ConstanteReal.java
package ast;
/**
* Nodo constante real del AST.<br/>
* 13-oct-2005<br/>
* Procesadores de Lenguaje<br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo<br/>
* @author Francisco Ortin
*/
public class ConstanteReal extends Expresion {
/**
* El valor de la constante entera
*/
public double valor;
/**
* @param valor El valor de la constante real
*/
public ConstanteReal(double valor) {
super('F');
this.valor=valor;
}
/**
* Mtodo necesario para hacer la invocacin correcta a los
* mtodos visitar de los visitor
* @param visitor Recorrido que aceptar el rbol
* @param param Un parmetro opcional para hacer ms verstil el recorrido
127
Implementacin en C++
ast.h
#ifndef _ast_h
#define _ast_h
#include "visitor.h"
#include <iostream>
#include <sstream>
using namespace std;
class Expresion {
public:
virtual ~Expresion() {}
virtual void aceptar(Visitor*) = 0;
// * Atributos
double valor;
char tipo;
ostringstream codigo;
};
class ExpresionUnaria: public Expresion {
Expresion *operando;
char operador;
ExpresionUnaria(ExpresionUnaria&) {}
ExpresionUnaria &operator=(ExpresionUnaria&) {return *this;}
public:
ExpresionUnaria(char operador,Expresion *operando) {
this->operador=operador;
this->operando=operando;
}
~ExpresionUnaria() { delete operando; }
virtual void aceptar(Visitor *v) {v->visitar(this);}
char getOperador() const { return operador; }
Expresion *getOperando() { return operando; }
};
class ExpresionBinaria: public Expresion {
Expresion *operando1,*operando2;
char operador;
ExpresionBinaria(ExpresionBinaria&) {}
ExpresionBinaria &operator=(ExpresionBinaria&) {return *this;}
public:
ExpresionBinaria(char operador,Expresion *operando1,Expresion *operando2) {
this->operador=operador;
this->operando1=operando1;
this->operando2=operando2;
}
~ExpresionBinaria() { delete operando1; delete operando2; }
virtual void aceptar(Visitor *v) {v->visitar(this);}
char getOperador() const { return operador; }
Expresion *getOperando1() { return operando1; }
Expresion *getOperando2() { return operando2; }
};
class ConstanteEntera: public Expresion {
public:
ConstanteEntera(int v) {valor=v;}
virtual void aceptar(Visitor *v) {v->visitar(this);}
};
class ConstanteReal: public Expresion {
public:
ConstanteReal(double v) {valor=v;}
virtual void aceptar(Visitor *v) {v->visitar(this);}
};
#endif
128
Evaluacin de un AST
VisitorSemantico.java
package semantico;
import ast.*;
import visitor.Visitor;
/**
* Clase visitor que lleva a cabo la inferencia y comprobacin de
* tipos del analizador semntico.<br/>
* 13-oct-2005<br/>
* Procesadores de Lenguaje<br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo<br/>
* @author Francisco Ortin
*/
public class VisitorSemantico extends Visitor {
/* (non-Javadoc)
* @see visitor.Visitor#visitar(ast.ExpresionBinaria, java.lang.Object)
*/
public Object visitar(ExpresionBinaria exp, Object param) {
exp.operando1.aceptar(this,null);
exp.operando2.aceptar(this,null);
exp.tipo=tipoMayor(exp.operando1.tipo,exp.operando2.tipo);
if (exp.operador.equals("=")&& exp.operando1.tipo=='I' && exp.operando2.tipo=='F')
exp.tipo='E';
return null;
}
/* (non-Javadoc)
* @see visitor.Visitor#visitar(ast.ExpresionUnaria, java.lang.Object)
*/
public Object visitar(ExpresionUnaria exp, Object param) {
exp.operando.aceptar(this,null);
exp.tipo=exp.operando.tipo;
129
return null;
}
/* (non-Javadoc)
* @see visitor.Visitor#visitar(ast.ConstanteReal, java.lang.Object)
*/
public Object visitar(ConstanteReal cr, Object param) {
cr.tipo='F';
return null;
}
/* (non-Javadoc)
* @see visitor.Visitor#visitar(ast.ConstanteEntera, java.lang.Object)
*/
public Object visitar(ConstanteEntera ce, Object param) {
ce.tipo='I';
return null;
}
/** Mtodo privado que calcula el tipo promocin de otros dos <br/>
* 'E' -> error<br/>
* 'I' -> entero<br/>
* 'F' -> real <br/>
* @param tipo1 Un tipo
* @param tipo2 El segundo tipo
* @return El tipo promocionado
*/
private char tipoMayor(char tipo1,char tipo2) {
if (tipo1=='E'||tipo2=='E') return 'E';
if (tipo1=='F'||tipo2=='F') return 'F';
return 'I';
}
}
VisitorGeneradorCodigo.java
package generacioncodigo;
import
import
import
import
java.io.Writer;
java.io.IOException;
ast.*;
visitor.Visitor;
/**
* Visitor de generacin de cdigo de una expresin.<br/>
* 13-oct-2005<br/>
* Procesadores de Lenguaje<br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo<br/>
* @author Francisco Ortin
*/
public class VisitorGeneradorCodigo extends Visitor {
/**
* Dnde vamos a generar el cdigo
*/
private Writer out;
/**
* @param out Flujo de salida donde se genera el cdigo
*/
public VisitorGeneradorCodigo(Writer out) {
this.out = out;
}
/* (non-Javadoc)
* @see visitor.Visitor#visitar(ast.ExpresionBinaria, java.lang.Object)
*/
public Object visitar(ExpresionBinaria exp, Object param) {
try {
exp.operando1.aceptar(this,null);
if (exp.operando1.tipo=='I' && exp.tipo=='F' )
out.write("\tITOF\n"); // * Integer to Float
exp.operando2.aceptar(this,null);
// * Tipo del operador
out.write("\t"+exp.tipo); // I o F
// * Operador
switch (exp.operador.charAt(0)){
case '+': out.write("ADD\n"); break;
case '-': out.write("SUB\n"); break;
case '*': out.write("MUL\n"); break;
case '/': out.write("DIV\n"); break;
case '=': out.write("STORE\n"); break;
default: assert false;
}
}
130
Evaluacin de un AST
catch(IOException t) {
System.err.println("Error generando cdigo.");
System.exit(-1);
}
return null;
}
/* (non-Javadoc)
* @see visitor.Visitor#visitar(ast.ExpresionUnaria, java.lang.Object)
*/
public Object visitar(ExpresionUnaria exp, Object param) {
try {
exp.operando.aceptar(this,null);
out.write("\t"+exp.tipo+"NEG\n"); // * Instruccin INEG o FNEG
}
catch(IOException t) {
System.err.println("Error generando cdigo.");
System.exit(-1);
}
return null;
}
/* (non-Javadoc)
* @see visitor.Visitor#visitar(ast.ConstanteReal, java.lang.Object)
*/
public Object visitar(ConstanteReal cr, Object param) {
try {
out.write("\tPUSHF\t"+cr.valor+"\n");
}
catch(IOException t) {
System.err.println("Error generando cdigo.");
System.exit(-1);
}
return null;
}
/* (non-Javadoc)
* @see visitor.Visitor#visitar(ast.ConstanteEntera, java.lang.Object)
*/
public Object visitar(ConstanteEntera ce, Object param) {
try {
out.write("\tPUSHI\t"+ce.valor+"\n");
}
catch(IOException t) {
System.err.println("Error generando cdigo.");
System.exit(-1);
}
return null;
}
}
VisitorCalculo.java
package optimizacioncodigo;
import ast.*;
import visitor.Visitor;
/**
* Visitor de clculo del valor de una expresin.<br/>
* Clase utilizada para optimizar las expresiones, calculando el valor
* de las expresiones constantes.<br/>
* 13-oct-2005<br/>
* Procesadores de Lenguaje<br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo<br/>
* @author Francisco Ortin
*/
public class VisitorCalculo extends Visitor {
/* (non-Javadoc)
* @see visitor.Visitor#visitar(ast.ExpresionBinaria, java.lang.Object)
*/
public Object visitar(ExpresionBinaria exp, Object param) {
exp.operando1.aceptar(this,null);
exp.operando2.aceptar(this,null);
switch(exp.operador.charAt(0)) {
case
'+':
exp.valorCalculado=exp.operando1.valorCalculado+exp.operando2.valorCalculado; break;
case
'-':
exp.valorCalculado=exp.operando1.valorCalculadoexp.operando2.valorCalculado; break;
case
'*':
exp.valorCalculado=exp.operando1.valorCalculado*exp.operando2.valorCalculado; break;
case
'/':
exp.valorCalculado=exp.operando1.valorCalculado/exp.operando2.valorCalculado; break;
131
case
'%':
exp.valorCalculado=exp.operando1.valorCalculado%exp.operando2.valorCalculado; break;
case '=': exp.valorCalculado=exp.operando1.valorCalculado; break;
default: assert false;
}
if (exp.tipo=='I')
exp.valorCalculado=(int)exp.valorCalculado;
return null;
}
/* (non-Javadoc)
* @see visitor.Visitor#visitar(ast.ExpresionUnaria, java.lang.Object)
*/
public Object visitar(ExpresionUnaria exp, Object param) {
assert exp.operador.equals("-");
// * Calculo el valor del operando
exp.operando.aceptar(this,null);
exp.valorCalculado=-exp.operando.valorCalculado;
return null;
}
/* (non-Javadoc)
* @see visitor.Visitor#visitar(ast.ConstanteReal, java.lang.Object)
*/
public Object visitar(ConstanteReal cr, Object param) {
cr.valorCalculado=cr.valor;
return null;
}
/* (non-Javadoc)
* @see visitor.Visitor#visitar(ast.ConstanteEntera, java.lang.Object)
*/
public Object visitar(ConstanteEntera ce, Object param) {
ce.valorCalculado=ce.valor;
return null;
}
}
VisitorDebug.java
package visitor;
import ast.*;
import java.io.PrintStream;
/**
* Recorrido para generar una representacin textual del AST.<br/>
* 13-oct-2005<br/>
* Procesadores de Lenguaje<br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo<br/>
* @author Francisco Ortin
*/
public class VisitorDebug extends Visitor {
/**
* El flujo donde se escribir el rbol
*/
private PrintStream out;
/**
* @param out El flujo donde se escribir el rbol
*/
public VisitorDebug(PrintStream out) {
this.out=out;
}
/**
* Muestra en el flujo de salida el subrbol expresin binara
* @see visitor.Visitor#visitar(ast.ExpresionBinaria, java.lang.Object)
*/
public Object visitar(ExpresionBinaria exp, Object param) {
int nivel=((Integer)param).intValue();
// * Prefijo
out.print(construyePrefijo(nivel));
// * Nodo
out.print("("+exp.operador+", tipo: "+exp.tipo+
", valor calculado: "+exp.valorCalculado);
out.println();
// * Hijos
exp.operando1.aceptar(this,new Integer(nivel+1));
exp.operando2.aceptar(this,new Integer(nivel+1));
// * Final del nodo
out.println(construyePrefijo(nivel)+")");
return null;
}
132
Evaluacin de un AST
/**
* Muestra en el flujo de salida el subrbol expresin unaria
* @see visitor.Visitor#visitar(ast.ExpresionUnaria, java.lang.Object)
*/
public Object visitar(ExpresionUnaria exp, Object param) {
int nivel=((Integer)param).intValue();
// * Prefijo
out.print(construyePrefijo(nivel));
// * Nodo
out.print("("+exp.operador+", tipo: "+exp.tipo+
", valor calculado: "+exp.valorCalculado);
out.println();
// * Hijo
exp.operando.aceptar(this,new Integer(nivel+1));
// * Final del nodo
out.println(construyePrefijo(nivel)+")");
return null;
}
/**
* Muestra en el flujo de salida el nodo constante entera
* @see visitor.Visitor#visitar(ast.ConstanteEntera, java.lang.Object)
*/
public Object visitar(ConstanteEntera ce, Object param) {
int nivel=((Integer)param).intValue();
out.print(construyePrefijo(nivel));
out.println("(CTE_ENTERA, valor:"+ce.valor+" )");
return null;
}
/**
* Muestra en el flujo de salida el nodo constante entera
* @see visitor.Visitor#visitar(ast.ConstanteReal, java.lang.Object)
*/
public Object visitar(ConstanteReal cr, Object param) {
int nivel=((Integer)param).intValue();
out.print(construyePrefijo(nivel));
out.println("(CTE_ENTERA, valor:"+cr.valor+" )");
return null;
}
/**
* Construye la cadena prefijo para mostrar el rbol
* @param nivel El nivel de sangrado
* @return La cadena prefijo
*/
protected String construyePrefijo(int nivel) {
StringBuilder sb=new StringBuilder();
for (int i=0;i<nivel;i++)
sb.append("| ");
return sb.toString();
}
}
Implementacin en C++
visitor.h
#ifndef _visitor_h
#define _visitor_h
class ExpresionUnaria;
class ExpresionBinaria;
class ConstanteEntera;
class ConstanteReal;
class Visitor {
public:
virtual void visitar(ExpresionUnaria *) = 0;
virtual void visitar(ExpresionBinaria *) = 0;
virtual void visitar(ConstanteEntera *) = 0;
virtual void visitar(ConstanteReal *) = 0;
};
#endif
visitorsemantico.h
#ifndef _visitorsemantico_h
#define _visitorsemantico_h
#include "ast.h"
class VisitorSemantico: public Visitor{
133
visitorsemantico.cpp
#include "visitorsemantico.h"
char VisitorSemantico::tipoMayor(char tipo1,char tipo2) {
// 'E' -> error
// 'I' -> entero
// 'F' -> real
if (tipo1=='E'||tipo2=='E') return 'E';
if (tipo1=='F'||tipo2=='F') return 'F';
return 'I';
}
void VisitorSemantico::visitar(ExpresionUnaria *e) {
// * Calculo el tipo del operando
e->getOperando()->aceptar(this);
// * Calculo el tipo del nodo
e->tipo=e->getOperando()->tipo;
}
void VisitorSemantico::visitar(ExpresionBinaria *e) {
// * Calculo el tipo de los operandos
e->getOperando1()->aceptar(this);
e->getOperando2()->aceptar(this);
// * Calculo el tipo del nodo
e->tipo=tipoMayor(e->getOperando1()->tipo,e->getOperando2()->tipo);
// * Error semntico
if (e->getOperador()=='=' &&
e->getOperando1()->tipo=='I' && e->getOperando2()->tipo=='F')
e->tipo='E';
}
void VisitorSemantico::visitar(ConstanteEntera *c)
c->tipo='I';
}
void VisitorSemantico::visitar(ConstanteReal *c)
c->tipo='F';
}
visitorgc.h
#ifndef _visitorgc_h
#define _visitorgc_h
#include "ast.h"
class VisitorGC: public Visitor{
public:
void visitar(ExpresionUnaria *) ;
void visitar(ExpresionBinaria *);
void visitar(ConstanteEntera *) ;
void visitar(ConstanteReal *) ;
};
#endif
visitorgc.cpp
#include "visitorgc.h"
#include <cassert>
void VisitorGC::visitar(ExpresionUnaria *e) {
assert(e->getOperador()=='-');
// * Genero el cdigo del operando
e->getOperando()->aceptar(this);
e->codigo<<e->getOperando()->codigo.str();
// * Genero el cdigo de negacin
e->codigo<<"\t"<<e->tipo<<"NEG\n"; // * Instruccin INEG o FNEG
}
void VisitorGC::visitar(ExpresionBinaria *e) {
// * Cdigo para apilar el primer operando
e->getOperando1()->aceptar(this);
e->codigo<<e->getOperando1()->codigo.str();
134
Evaluacin de un AST
visitorcalculo.h
#ifndef _visitorcalculo_h
#define _visitorcalculo_h
#include "ast.h"
class VisitorCalculo: public Visitor{
public:
void visitar(ExpresionUnaria *) ;
void visitar(ExpresionBinaria *);
void visitar(ConstanteEntera *) ;
void visitar(ConstanteReal *) ;
};
#endif
visitorcalculo.cpp
#include "visitorcalculo.h"
#include <cassert>
void VisitorCalculo::visitar(ExpresionUnaria *e) {
assert(e->getOperador()=='-');
// * Calculo el valor del operando
e->getOperando()->aceptar(this);
e->valor= - e->getOperando()->valor;
}
void VisitorCalculo::visitar(ExpresionBinaria *e) {
// * Calculo el valor de los operandos
e->getOperando1()->aceptar(this);
e->getOperando2()->aceptar(this);
// * Calculo el valor del nodo
switch (e->getOperador()){
case '+': e->valor=e->getOperando1()->valor+e->getOperando2()->valor;
case '-': e->valor=e->getOperando1()->valor-e->getOperando2()->valor;
case '*': e->valor=e->getOperando1()->valor*e->getOperando2()->valor;
case '/': e->valor=e->getOperando1()->valor/e->getOperando2()->valor;
case '=': e->valor=e->getOperando2()->valor; break;
default: assert(0);
}
if (e->getOperando1()->tipo=='I' && e->getOperando2()->tipo=='I')
e->valor=(int)e->valor;
}
void VisitorCalculo::visitar(ConstanteEntera *c)
void VisitorCalculo::visitar(ConstanteReal *c)
break;
break;
break;
break;
{}
{}
135
visitormostrar.h
#ifndef _visitormostrar_h
#define _visitormostrar_h
#include "ast.h"
#include <iostream>
class VisitorMostrar: public Visitor{
std::ostream &flujo;
public:
VisitorMostrar(std::ostream &f):flujo(f) {}
void visitar(ExpresionUnaria *) ;
void visitar(ExpresionBinaria *);
void visitar(ConstanteEntera *) ;
void visitar(ConstanteReal *) ;
};
#endif
visitormostrar.cpp
#include "visitormostrar.h"
#include <iostream>
using namespace std;
void VisitorMostrar::visitar(ExpresionUnaria *e) {
flujo<<"( "<<e->getOperador()<<' ';
e->getOperando()->aceptar(this);
flujo<<" )";
}
void VisitorMostrar::visitar(ExpresionBinaria *e){
flujo<<"( "<<e->getOperador()<<' ';
e->getOperando1()->aceptar(this);
flujo<<' ';
e->getOperando2()->aceptar(this);
flujo<<" )";
}
void VisitorMostrar::visitar(ConstanteEntera *c) {
flujo<<(int)(c->valor);
}
void VisitorMostrar::visitar(ConstanteReal *c) {
flujo<<c->valor;
}
136
Evaluacin de un AST
sentencia: expresion
{ ast=(Expresion)$1.obj; }
;
expresion:
expresion
'+'
expresion
{
$$=new
ParserVal(new
ExpresionBinaria("+",(Expresion)$1.obj,(Expresion)$3.obj)); }
| expresion '-' expresion
{ $$=new ParserVal(new ExpresionBinaria("",(Expresion)$1.obj,(Expresion)$3.obj)); }
|
expresion
'*'
expresion
{
$$=new
ParserVal(new
ExpresionBinaria("*",(Expresion)$1.obj,(Expresion)$3.obj)); }
|
expresion
'/'
expresion
{
$$=new
ParserVal(new
ExpresionBinaria("/",(Expresion)$1.obj,(Expresion)$3.obj)); }
|
expresion
'%'
expresion
{
$$=new
ParserVal(new
ExpresionBinaria("%",(Expresion)$1.obj,(Expresion)$3.obj)); }
|
expresion
'='
expresion
{
$$=new
ParserVal(new
ExpresionBinaria("=",(Expresion)$1.obj,(Expresion)$3.obj)); }
| '-' expresion %prec MENOSUNARIO
{
$$=new
ParserVal(new
ExpresionUnaria("",(Expresion)$2.obj)); }
| '(' expresion ')'
{ $$=$2; }
| CTE_ENTERA
{ $$=new ParserVal(new ConstanteEntera($1.ival)); }
| CTE_REAL
{ $$=new ParserVal(new ConstanteReal($1.dval)); }
;
%%
// * Referencia al analizador lxico
private Lexico lexico;
// * Referencia al ast
public Expresion ast;
// * Llamada al analizador lxico
private int yylex () {
int token=0;
try {
token=lexico.yylex();
} catch(Throwable e) {
System.err.println ("Error Lxico en lnea " + lexico.getYyline()+
" y columna "+lexico.getYycolumn()+":\n\t"+e);
}
return token;
}
// * Manejo de Errores Sintcticos
public void yyerror (String error) {
System.err.println ("Error Sintctico en lnea " + lexico.getYyline()+
" y columna "+lexico.getYycolumn()+":\n\t"+error);
}
// * Constructor del Sintctico
public Parser(Lexico lexico,String ficheroSalida) {
this.lexico = lexico;
lexico.setParser(this);
}
// * El yyparse original no es pblico
public int parse() {
return yyparse();
}
// * El yylval no es un atributo pblico
public void setYylval(ParserVal yylval) {
this.yylval=yylval;
}
Lexico.flex
package lexico;
import sintactico.ParserVal;
import sintactico.Parser;
/**
* Especificacin lxica del lenguaje.<br/>
* 13-oct-2005<br/>
* Procesadores de Lenguaje<br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo<br/>
* @author Francisco Ortin
*/
%%
// ************ Opciones ********************
// % debug // * Muy buena opcin para depurar
%byaccj
%class Lexico
%public
%unicode
%line
137
%column
%{
// ************ Atributos y mtodos ********************
// * El analizador sintctico
private Parser parser;
public void setParser(Parser parser) {
this.parser=parser;
}
// * Para acceder al nmero de lnea (yyline es package)
public int getYyline() {
// * Flex empieza en cero
return yyline+1;
}
// * Para acceder al nmero de columna (yycolumn es package)
public int getYycolumn() {
// * Flex empieza en cero
return yycolumn+1;
}
%}
// ************ Patrones (macros) ********************
ConstanteEntera = [0-9]+
FinDeLinea = \n | \r | \r\n
Basura = {FinDeLinea} | [ \t\f]
ComentarioC = "/*" ~"*/"
ComentarioCpp = "//".*\n
Mantisa = [0-9]+"."[0-9]*
Exponente = [Ee]("+"|"-")?[0-9]+
%%
// ************ Acciones ********************
// * Caracteres que ignoramos
{Basura} {}
// * Comentarios
{ComentarioC} {}
{ComentarioCpp}
{}
// * Constante Entera
{ConstanteEntera} { parser.setYylval(new ParserVal(Integer.parseInt(yytext())));
return Parser.CTE_ENTERA; }
// * Constante Real
{Mantisa}{Exponente}?
{ parser.setYylval(new ParserVal(Double.parseDouble(yytext())));
return Parser.CTE_REAL; }
[0-9]+{Exponente}?
{ parser.setYylval(new ParserVal(Double.parseDouble(yytext())));
return Parser.CTE_REAL; }
// * Tokens de longitud 1
"+" |
"-" |
"*" |
"/" |
"%" |
"=" |
"(" |
")"
{ parser.setYylval(new ParserVal(yytext()));
return (int) yycharat(0); }
// * Cualquier otro carcter
.
{ System.err.println ("Error Lxico en lnea " + getYyline() +
" y columna "+getYycolumn()+":\n\tCarcter \'"+
yycharat(0)+"\' desconocido.");
}
Main.java
import
import
import
import
import
import
import
sintactico.Parser;
lexico.Lexico;
semantico.VisitorSemantico;
optimizacioncodigo.*;
generacioncodigo.*;
visitor.VisitorDebug;
java.io.*;
/**
* Prueba del compilador.<br/>
* 13-oct-2005 <br/>
* Procesadores de Lenguaje <br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo <br/>
138
Evaluacin de un AST
*
* @author Francisco Ortin
*/
public class Main {
public static void main(String args[]) throws IOException {
if (args.length<2) {
System.err.println("Necesito el archivo de entrada y salida.");
return;
}
FileReader fr=null;
try {
fr=new FileReader(args[0]);
} catch(IOException io) {
System.err.println("El archivo "+args[0]+" no se ha podido abrir.");
return;
}
// * Creamos lxico y sintctico
Lexico lexico = new Lexico(fr);
Parser parser = new Parser(lexico,args[1]);
// * "Parseamos"
parser.parse();
// * Visitamos
parser.ast.aceptar(new VisitorSemantico(),null);
if (parser.ast.tipo!='E') {
System.out.println("Anlisis semntico finalizado satisfactoriamente.");
parser.ast.aceptar(new VisitorCalculo(),null);
System.out.println("Valor de la expresin: "+parser.ast.valorCalculado);
FileWriter fw=new FileWriter(args[1]);
parser.ast.aceptar(new VisitorGeneradorCodigo(fw),null);
fw.close();
System.out.println("Cdigo generado en el fichero: "+args[1]);
}
else
System.err.println("El programa no es semnticamente vlido.");
parser.ast.aceptar(new VisitorDebug(System.out),new Integer(0));
}
}
139
}
#include "visitorsemantico.h"
#include "visitorgc.h"
#include "visitorcalculo.h"
#include "visitormostrar.h"
#include <iostream>
using namespace std;
int main() {
yyparse();
VisitorSemantico semantico;
ast->aceptar(&semantico);
if (ast->tipo=='E')
cerr<<"El programa no es semnticamente vlido."<<endl;
else {
cout<<"\nAnlisis semntico finalizado correctamente.\n";
VisitorCalculo calculo;
ast->aceptar(&calculo);
cout<<"\nValor de la expresin: "<<ast->valor<<endl;
VisitorGC gc;
ast->aceptar(&gc);
cout<<"\nCdigo generado:\n"<<ast->codigo.str()<<endl;
}
VisitorMostrar traza(cout);
cout<<"\nAST generado:\n";
ast->aceptar(&traza);
cout<<endl;
delete ast;
}
lexico.l
%{
#include
#include
#include
#include
<string.h>
<stdlib.h>
"ast.h"
"yytab.h"
unsigned yylineno=1;
void errorLexico(char);
void inicializarLexico(char *,char *);
%}
comentario \/\/.*\n
linea
\n
letra
[a-zA-Z]
numero
[0-9]
real
{numero}+(\.{numero}+)?
alfa
[{numero}{letra}]
read
[Rr][Ee][Aa][Dd]
write
[Ww][Rr][Ii][Tt][Ee]
main
[Mm][Aa][Ii][Nn]
integer
[Ii][Nn][Tt][Ee][Gg][Ee][Rr]
%%
[ \t]+
; // * Se ignoran los espacions y tabuladores
{comentario}
{ yylineno++; }
{linea}
{ yylineno++; }
{numero}+
{ yylval.entero=atoi(yytext); return CTE_ENTERA; }
{real}([Ee][\+\-]?{numero}+)? { yylval.real=atof(yytext); return CTE_REAL; }
\+
|
\|
\*
|
\/
|
\(
|
\)
|
%
|
=
|
\}
|
\{
|
,
|
;
{ return yytext[0]; } // Token=Caracter ASCII
.
{ errorLexico(yytext[0]); } // Caracter no perteneciente al lenguaje
%%
void errorLexico(char c) {
printf("Error lexico en linea %d. Caracter %c no perteneciente al lenguaje.\n",
yylineno,c);
exit(-1);
}
void inicializarLexico(char *fOrigen,char *fDestino) {
yyin=fopen(fOrigen,"r");
if (yyin==NULL) {
printf("Error abriendo el fichero %s.\n",fOrigen);
exit(-1);
}
140
Evaluacin de un AST
yyout=fopen(fDestino,"w");
if (yyout==NULL) {
printf("Error abriendo el fichero %s.\n",fDestino);
exit(-1);
}
}
141
En el Ejemplo 28 se present un mecanismo para poder evaluar una gramtica Latribuida en una nica pasada. Para ello, se estableca un esquema de traduccin de las reglas semnticas a cdigo. La ejecucin de las reglas se procesaba al mismo tiempo que la
tarea de reconocer sintcticamente el lenguaje, mediante un analizador descendente recursivo predictivo. La gramtica del ejemplo es LL1, resultado muy sencillo el desarrollo del
analizador sintctico.
La implementacin se ha realizado en el lenguaje de programacin Java, distribuyndose en tres paquetes: sintctico, lxico y errores. ste ser el orden en el que se presentar el cdigo fuente de cada uno de los mdulos.
B.1 Mdulo Sintctico
Sintactico.java
package latribuida.sintactico;
/**
* <p>Title: Implementancin LAtribuida</p>
* <p>Description: Ejemplo de Implementacion de un Evaluador Descendente Recursivo
* LL1, sobre una gramtica LAtribuida</p>
* <p>Copyright: Copyright (c) 2004</p>
* <p>Company: Universidad de Oviedo</p>
* @author Francisco Ortn
* @version 1.0
*/
import latribuida.lexico.*;
/** Clase que ofrece toda la funcionalidad del analizador sintctico */
public class Sintactico {
/** Analizador lxico */
private Lexico lexico;
/** Constructor con el archivo de entrada */
public Sintactico(String nombreFichero) {
lexico=new Lexico(nombreFichero);
}
/** Constructor con entrada estndar */
public Sintactico() { lexico=new Lexico(); }
/** Mtodo que reconoce sintcticamente el no terminal "expresion".<br/>
* Produccin: expresion -> terminos masTerminos<br/>
* Adems evala los atributos calculados en las reglas semnticas
* de esta produccin. Recibe los atributos heredados (ninguno) y
* devuelve los sintetizados (expresion.valor)<br/>
*/
public int expresion() {
// * Regla: masTerminos.operando1 = termino.valor
int masTerminosOperando1=termino();
// expresion.valor = masTerminos.valor
return masTerminos(masTerminosOperando1);
}
/** Mtodo que reconoce sintcticamente el no terminal "masTerminos".<br/>
* Produccion: masTerminos1 -> '+' termino masTerminos2<br/>
* Produccion: masTerminos1 -> '-' termino masTerminos2<br/>
* Produccion: masTerminos1 -> lambda<br/>
* Adems evala los atributos calculados en las reglas semnticas
* de esta produccin. Recibe los atributos heredados (masTerminos.operando1)
143
144
Lexico.java
package latribuida.lexico;
/**
* <p>Title: Implementancin LAtribuida</p>
* <p>Description: Ejemplo de Implementacion de un Evaluador Descendente Recursivo LL1,
sobre una gramtica LAtribuida</p>
* <p>Copyright: Copyright (c) 2004</p>
* <p>Company: Universidad de Oviedo</p>
* @author Francisco Ortn
* @version 1.0
*/
import java.io.*;
/** Clase que ofrece toda la funcionalidad del analizador lxico */
public class Lexico {
/** Flujo de entrada */
private PushbackReader in;
/** Nmero de lnea */
private int yylineno=1;
/** Nmero de lnea */
public int getYYlineno() {return yylineno; }
/** Tokens */
public static final int CTE_ENTERA=256;
/** Valor semntico del token */
private Atributo yylval;
/** Atributo del token */
public Atributo getYYlval() { return yylval; }
/** Atributo que indica el token actual */
private int token;
/** Mtodo que devuelve el token actual */
public int getToken() { return token; }
/** Mtodo que comprueba que el siguiente token es el pasado. Si as
* es, avanza el token actual al siguiente. */
public void match(int token) {
if (this.token==token) {
// * Avanzamos
this.token=yylex();
return;
}
latribuida.errores.Error.esperaba(yylineno,token,this.token);
}
/** Construye un analizador lxico sobre la entrada estndar */
public Lexico() {
in=new PushbackReader(new InputStreamReader(System.in));
token=yylex(); // * Leemos el primer token
}
/** Construye un analizador lxico sobre el nombre del fichero pasado */
public Lexico(String fichero) {
try {
this.in=new PushbackReader(new FileReader(fichero));
token=yylex(); // * Leemos el primer token
145
} catch(Exception e) {
System.err.println("El fichero "+fichero+" no se encuentra.");
e.printStackTrace();
System.exit(-1);
}
}
private void errorLectura(Exception e) {
System.err.println("No se puede leer de disco.\n");
e.printStackTrace();
System.exit(-2);
}
private int getCar() {
int ch=0;
try { ch=in.read(); }
catch (Exception e) { errorLectura(e); }
if (ch=='\n') yylineno++;
if (ch=='\r') {
try { // * En Java \n, \r y \r\n son saltos de carro
int otroCh=in.read();
if (otroCh!='\n')
in.unread(otroCh);
} catch (Exception e) {
errorLectura(e);
}
yylineno++;
ch='\n';
}
return ch;
}
private void putCar(int c) {
try { in.unread(c); }
catch (Exception e) {
System.err.println("Buffer lleno.\n");
e.printStackTrace();
System.exit(-3);
}
if (c=='\n') yylineno--;
}
/** Mtodo que devuelve el siguiente token.<br/>
* Este mtodo es privado. El analizador sintctico deber utilizar los
* mtodos getToken y match. */
private int yylex() {
int ch=0;
ch=getCar();
// * Basura
while (ch==' '||ch=='\t'||ch=='\n'||ch=='\r') ch=getCar();
// * Nmero
if (ch>='0'&&ch<='9') {
StringBuffer s=new StringBuffer();
while (ch>='0'&&ch<='9') {
s.append((char)ch);
ch=getCar();
}
putCar(ch);
yylval=new Atributo( Integer.parseInt(s.toString()) );
return CTE_ENTERA;
}
// * Cualquier otro carcter
yylval=new Atributo((char)ch);
return ch;
}
/** Mtodo de prueba de la clase */
public static void main(String[] args) {
Lexico lexico=null;
if (args.length>=1) lexico = new Lexico();
else lexico=new Lexico();
int token;
while ((token=lexico.yylex()) != -1 ) {
System.out.println("Nmero de lnea: "+lexico.getYYlineno());
System.out.println("\tNmero de token: "+token);
System.out.print("\tAtributo:");
if (token==CTE_ENTERA)
System.out.println(lexico.getYYlval().entero);
else System.out.println(lexico.getYYlval().carcter);
}
}
}
146
147
Comprobador de Tipos
149
}
/**
* Inferencia de tipos para la invocacin a funciones
* La implementacin por omisin es devolver un error de tipo
* @param parametros Las expresiones de tipo utilizadas como parmetros
* @return El tipo inferido
*/
public ExpresionTipo parentesis(List parametros) {
return new Error("Operacin () no permitida.");
}
/**
* Inferencia de tipos para el acceso a campos de un registro
* La implementacin por omisin es devolver un error de tipo
* @param campo El identificador del campo
* @return El tipo inferido
*/
public ExpresionTipo punto(String campo) {
return new Error("Operacin . no permitida.");
}
/** Implementa la equivalencia estructural de tipos
* La implementacin por omisin es vlida para los tipos simples
* (comparacin por identidad)
* @param et La segunda expresin de tipo (la primera es this)
* @return Si son o no equivalentes
*/
public boolean equivalente(ExpresionTipo et) {
return this.getClass().equals(et.getClass());
}
}
Integer.java
package tipos;
/**
* Expresin de tipo Integer.<br/>
* Sigue el patrn de diseo <i>Composite</i>.<br/>
* 13-oct-2005 <br/>
* Procesadores de Lenguaje <br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo <br/>
*
* @author Francisco Ortin
*/
public class Integer extends ExpresionTipo {
/* (non-Javadoc)
* @see tipos.ExpresionTipo#getBytes()
*/
public int getBytes() {
return 4;
}
/* (non-Javadoc)
* @see tipos.ExpresionTipo#expresionTipo()
*/
public String expresionTipo() {
return "integer";
}
}
Char.java
package tipos;
/**
* Expresin de tipo Char.<br/>
* Sigue el patrn de diseo <i>Composite</i>.<br/>
* 13-oct-2005 <br/>
* Procesadores de Lenguaje <br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo <br/>
*
* @author Francisco Ortin
*/
public class Char extends ExpresionTipo {
/* (non-Javadoc)
* @see tipos.ExpresionTipo#getBytes()
*/
public int getBytes() {
return 1;
}
150
Comprobador de Tipos
/* (non-Javadoc)
* @see tipos.ExpresionTipo#expresionTipo()
*/
public String expresionTipo() {
return "char";
}
}
Pointer.java
package tipos;
/**
* Expresin de tipo Pointer.<br/>
* Sigue el patrn de diseo <i>Composite</i>.<br/>
* 13-oct-2005 <br/>
* Procesadores de Lenguaje <br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo <br/>
*
* @author Francisco Ortin
*/
public class Pointer extends ExpresionTipo {
/**
* @param a Tipo del elemento apuntado
*/
public Pointer(ExpresionTipo a) {
super();
this.a = a;
}
/* (non-Javadoc)
* @see tipos.ExpresionTipo#getBytes()
*/
public int getBytes() {
return 4;
}
/**
* Atributo <code>a</code> apunta al tipo al que apunta el puntero
*/
private ExpresionTipo a;
/**
* @return La expresin de tipo a la que apunta el puntero
*/
public ExpresionTipo getA() {
return a;
}
/* (non-Javadoc)
* @see tipos.ExpresionTipo#expresionTipo()
*/
public String expresionTipo() {
return "pointer("+a.expresionTipo()+")";
}
/* (non-Javadoc)
* @see tipos.ExpresionTipo#equivalente(tipos.ExpresionTipo)
*/
public boolean equivalente(ExpresionTipo et) {
return et instanceof Pointer &&
a.equivalente(((Pointer)et).a);
}
/* (non-Javadoc)
* @see tipos.ExpresionTipo#flecha()
*/
public ExpresionTipo flecha() {
return a;
}
}
Array.java
package tipos;
/**
* Expresin de tipo Array.<br/>
* Sigue el patrn de diseo <i>Composite</i>.<br/>
* 13-oct-2005 <br/>
* Procesadores de Lenguaje <br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo <br/>
151
*
* @author Francisco Ortin
*/
public class Array extends ExpresionTipo {
/**
* @param desde ndice menor
* @param hasta ndice mayor
* @param de Expresin de tipo base
*/
public Array(int desde, int hasta, ExpresionTipo de) {
super();
this.desde = desde;
this.hasta = hasta;
this.de = de;
}
private int desde,hasta;
/**
* @return El ndice mnimo del array
*/
public int getDesde() { return desde; }
/**
* @return El ndice mximo del array
*/
public int getHasta() { return hasta; }
private ExpresionTipo de;
/**
* @return El tipo base del array
*/
public ExpresionTipo getDe() { return de; }
/* (non-Javadoc)
* @see tipos.ExpresionTipo#getBytes()
*/
public int getBytes() {
return (hasta-desde+1)*de.getBytes();
}
/* (non-Javadoc)
* @see tipos.ExpresionTipo#expresionTipo()
*/
public String expresionTipo() {
return "Array("+desde+".."+
hasta+","+de.expresionTipo()+")";
}
/* (non-Javadoc)
* @see tipos.ExpresionTipo#corchete(tipos.ExpresionTipo)
*/
public ExpresionTipo corchete(ExpresionTipo et) {
if (!(et instanceof Integer))
return new Error("Slo se permiten ndices enteros");
return de;
}
/* (non-Javadoc)
* @see tipos.ExpresionTipo#equivalente(tipos.ExpresionTipo)
*/
public boolean equivalente(ExpresionTipo et) {
if (!(et instanceof Array))
return false;
Array array=(Array)et;
return array.desde==desde && array.hasta==hasta &&
array.de.equivalente(de);
}
}
Record.java
package tipos;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
/**
* Expresin de tipo Record.<br/>
* Sigue el patrn de diseo <i>Composite</i>.<br/>
* 13-oct-2005 <br/>
* Procesadores de Lenguaje <br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo <br/>
*
* @author Francisco Ortin
152
Comprobador de Tipos
*/
public class Record extends ExpresionTipo {
private Map campos;
/**
* @return Las expresiones de tipo de los campos del registro
*/
public Map getCampos() { return campos; }
/* (non-Javadoc)
* @see tipos.ExpresionTipo#getBytes()
*/
/**
* @param campos La memoria asociativa con el conjunto de campos del registro
*/
public Record(Map campos) {
this.campos = campos;
}
public int getBytes() {
int suma=0;
Iterator it=campos.values().iterator();
while(it.hasNext())
suma+=((ExpresionTipo)it.next()).getBytes();
return suma;
}
/* (non-Javadoc)
* @see tipos.ExpresionTipo#expresionTipo()
*/
public String expresionTipo() {
StringBuilder sb=new StringBuilder("record(");
Iterator it=campos.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry=(Entry) it.next();
sb.append(" (").append(entry.getKey()).append(
" x ").append(entry.getValue()).append(") ");
}
sb.append(")");
return sb.toString();
}
/* (non-Javadoc)
* @see tipos.ExpresionTipo#equivalente(tipos.ExpresionTipo)
*/
public boolean equivalente(ExpresionTipo et) {
if (!(et instanceof Record))
return false;
Record record=(Record)et;
if (campos.size()!=record.campos.size())
return false;
Iterator it=record.campos.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry=(Entry) it.next();
// * Tienen que tener igual nombre para los campos
String key=(String)entry.getKey();
if (!campos.containsKey(key))
return false;
ExpresionTipo expTipo=(ExpresionTipo)entry.getValue();
if (!expTipo.equivalente((ExpresionTipo)campos.get(key)))
return false;
}
return true;
}
/* (non-Javadoc)
* @see tipos.ExpresionTipo#punto(java.lang.String)
*/
public ExpresionTipo punto(String campo) {
if (!campos.containsKey(campo))
return new Error("No se encuentra el campo \""+campo+"\"");
return (ExpresionTipo)campos.get(campo);
}
}
Function.java
package tipos;
import java.util.List;
/**
* Expresin de tipo Function.<br/>
* Sigue el patrn de diseo <i>Composite</i>.<br/>
* 13-oct-2005 <br/>
153
Error.java
package tipos;
154
Comprobador de Tipos
import lexico.Lexico;
/**
* Expresin de tipo Error.<br/>
* Sigue el patrn de diseo <i>Composite</i>.<br/>
* 13-oct-2005 <br/>
* Procesadores de Lenguaje <br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo <br/>
*
* @author Francisco Ortin
*/
public class Error extends ExpresionTipo {
/* (non-Javadoc)
* @see tipos.ExpresionTipo#getBytes()
*/
public int getBytes() {
assert false;
return 0;
}
/* (non-Javadoc)
* @see tipos.ExpresionTipo#expresionTipo()
*/
public String expresionTipo() {
return "error";
}
private String mensaje;
private int numeroLinea;
private int numeroColumna;
private static Lexico lexico;
public static void setLexico(Lexico l) {
lexico=l;
}
public Error(String s) {
assert lexico!=null;
mensaje=s;
numeroLinea=lexico.getYyline();
numeroColumna=lexico.getYycolumn();
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
public String toString() {
return "Error en la lnea "+numeroLinea+
", columna "+numeroColumna+".\n\t"+
mensaje;
}
}
Void.java
package tipos;
/**
* Expresin de tipo Void.<br/>
* Sigue el patrn de diseo <i>Composite</i>.<br/>
* 13-oct-2005 <br/>
* Procesadores de Lenguaje <br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo <br/>
*
* @author Francisco Ortin
*/
public class Void extends ExpresionTipo {
/* (non-Javadoc)
* @see tipos.ExpresionTipo#getBytes()
*/
public int getBytes() {
assert false;
return 0;
}
/* (non-Javadoc)
* @see tipos.ExpresionTipo#expresionTipo()
*/
public String expresionTipo() {
return "void";
155
}
}
Implementacin en C++
tipos.h
#ifndef _tipos_h
#define _tipos_h
#include <string>
#include <map>
#include <vector>
#include <iostream>
#include <cassert>
using namespace std;
// * Clase componente del patrn Composite
class ExpresionTipo {
public:
virtual ~ExpresionTipo() {}
// * Para el generador de cdigo
virtual unsigned getBytes() const = 0;
// * Por depuracin
virtual string expresionTipo() const = 0;
// * Mtodos de anlisis semntico, con comportamiento predefinido
virtual ExpresionTipo *flecha();
virtual ExpresionTipo *corchete(const ExpresionTipo*);
virtual ExpresionTipo *parentesis(const vector<ExpresionTipo*>&);
virtual ExpresionTipo *punto(const string&);
// * Equivalencia de expresiones de tipo
virtual bool equivalente(const ExpresionTipo *) const;
};
// * Clase hoja del patrn Composite: tipo simple Integer
class Integer: public ExpresionTipo {
Integer(Integer&) {}
Integer &operator=(Integer&) { return *this; }
public:
Integer() {}
// * Para el generador de cdigo
unsigned getBytes() const { return 4; }
// * Por depuracin
string expresionTipo() const { return "integer"; }
};
// * Clase hoja del patrn Composite: tipo simple Char
class Char: public ExpresionTipo {
Char(Char&) {}
Char&operator=(Char&) { return *this; }
public:
Char() {}
// * Para el generador de cdigo
unsigned getBytes() const { return 1; }
// * Por depuracin
string expresionTipo() const { return "char"; }
};
// * Clase hoja del patrn Composite: tipo simple Void
class Void: public ExpresionTipo {
Void(Void&) {}
Void&operator=(Void&) { return *this; }
public:
Void() {}
// * Para el generador de cdigo
unsigned getBytes() const { return 0; }
// * Por depuracin
string expresionTipo() const { return "void"; }
};
// * Clase hoja del patrn Composite: tipo simple Error
class Error: public ExpresionTipo {
Error(Error&) {}
Error&operator=(Error&) { return *this; }
string mensaje;
unsigned numeroLinea;
public:
Error(const string &s) {
mensaje=s; extern unsigned yylineno; numeroLinea=yylineno; }
// * Para el generador de cdigo
unsigned getBytes() const { assert(0); return 0; }
// * Por depuracin
string expresionTipo() const { return "error"; }
friend ostream &operator<<(ostream &,const Error &);
156
Comprobador de Tipos
};
inline ostream &operator<<(ostream &o,const Error &e) {
return o<<"Error en la lnea "<<e.numeroLinea<<". "<<e.mensaje;
}
// * Clase compuesta del patrn Composite: tipo compuesto Pointer
class Pointer: public ExpresionTipo {
Pointer(Pointer&) {}
Pointer &operator=(Pointer&) { return *this; }
ExpresionTipo *a;
public:
Pointer(ExpresionTipo *a) { this->a=a; }
~Pointer() { delete a; }
// * Para el generador de cdigo
unsigned getBytes() const { return 4; }
// * Por depuracin
string expresionTipo() const;
// * Comprobaciones semnticas
ExpresionTipo *flecha() { return a; }
// * Equivalencia de expresiones de tipo
virtual bool equivalente(const ExpresionTipo *) const;
// * Mtodos especficos
const ExpresionTipo *getA() const { return a; }
};
// * Clase compuesta del patrn Composite: tipo compuesto Array
class Array: public ExpresionTipo {
Array(Array&) {}
Array&operator=(Array&) { return *this; }
int desde,hasta;
ExpresionTipo *de;
public:
Array(int desde,int hasta,ExpresionTipo *de) {
this->desde=desde; this->hasta=hasta; this->de=de; }
~Array() { delete de; }
// * Para el generador de cdigo
unsigned getBytes() const { return (hasta-desde+1)*de->getBytes(); }
// * Por depuracin
string expresionTipo() const;
// * Comprobaciones semnticas
ExpresionTipo *corchete(const ExpresionTipo *e) {
if (!dynamic_cast<const Integer*>(e))
return new Error("El ndice ha de ser de tipo entero.");
return de;
}
// * Equivalencia de expresiones de tipo
virtual bool equivalente(const ExpresionTipo *) const;
// * Mtodos especficos
const ExpresionTipo *getDe() const { return de; }
};
// * Clase compuesta del patrn Composite: tipo compuesto funcin ->
class Function: public ExpresionTipo {
Function(Function&) {}
Function&operator=(Function&) { return *this; }
ExpresionTipo *devolucion;
vector<ExpresionTipo*> parametros;
public:
Function(ExpresionTipo *d,const vector<ExpresionTipo*> &v):
devolucion(d),parametros(v) {}
~Function();
// * Para el generador de cdigo
unsigned getBytes() const { return 4; }
// * Por depuracin
string expresionTipo() const;
// * Comprobaciones semnticas
ExpresionTipo *parentesis(const vector<ExpresionTipo*>&) ;
// * Equivalencia de expresiones de tipo
virtual bool equivalente(const ExpresionTipo *) const;
// * Mtodos especficos
const ExpresionTipo *getDevolucion() const { return devolucion; }
const vector<ExpresionTipo*> &getParametros() const { return parametros; }
};
// * Clase compuesta del patrn Composite: tipo compuesto Record
class Record: public ExpresionTipo {
Record(Record&) {}
Record &operator=(Record&) { return *this; }
map<string,ExpresionTipo*> campos;
public:
Record(const map<string,ExpresionTipo*> &c): campos(c) {}
~Record();
// * Para el generador de cdigo
unsigned getBytes() const;
// * Por depuracin
157
tipos.cpp
#include "tipos.h"
#include <map>
#include <string>
#include <vector>
#include <sstream>
#include <cstring>
using namespace std;
/******** Expresion Tipo ********************************************/
ExpresionTipo *ExpresionTipo::flecha() {
return new Error("Operacin ^ no permitida.");
}
ExpresionTipo *ExpresionTipo::corchete(const ExpresionTipo*) {
return new Error("Operacin [] no permitida.");
}
ExpresionTipo *ExpresionTipo::punto(const string&) {
return new Error("Operacin . no permitida.");
}
ExpresionTipo *ExpresionTipo::parentesis(const vector<ExpresionTipo*>&) {
return new Error("Operacin [] no permitida.");
}
// * Funcionamiento por omisin para los tipos simples
bool ExpresionTipo::equivalente(const ExpresionTipo *et) const {
return typeid(*this)==typeid(*et); // * RTTI
}
/******** Pointer ********************************************/
string Pointer::expresionTipo() const {
ostringstream o;
o<<"pointer("<<a->expresionTipo()<<")";
return o.str();
}
bool Pointer::equivalente(const ExpresionTipo *et) const {
const Pointer *puntero=dynamic_cast<const Pointer*>(et);
if (!puntero) return false;
return a->equivalente(puntero->a);
}
/******** Array ********************************************/
string Array::expresionTipo() const {
ostringstream o;
o<<"array("<<desde<<".."<<hasta<<","<<de->expresionTipo()<<")";
return o.str();
}
bool Array::equivalente(const ExpresionTipo *et) const {
const Array *array=dynamic_cast<const Array*>(et);
if (!array) return false;
return desde==array->desde && hasta==array->hasta &&
array->equivalente(array->de);
}
/******** Function ********************************************/
Function::~Function(){
delete devolucion;
for (unsigned i=0;i<parametros.size();i++)
delete parametros[i];
}
ExpresionTipo *Function::parentesis(const vector<ExpresionTipo*> &v) {
if (v.size()!=parametros.size()) {
ostringstream o;
o<<"La funcin posee "<<parametros.size()<<" parmetros y se le estn "<<
"pasando "<<v.size()<<".";
158
Comprobador de Tipos
159
if (it1->first!=it2->first)
return false;
if (!it1->second->equivalente(it2->second))
return false;
}
return true;
}
ts.h
#ifndef _ts_h
#define _ts_h
#include
#include
#include
#include
"tipos.h"
<map>
<string>
<sstream>
160
Comprobador de Tipos
161
} catch(Throwable e) {
System.err.println ("Error Lxico en lnea " + lexico.getYyline()+
" y columna "+lexico.getYycolumn()+":\n\t"+e);
}
return token;
}
// * Manejo de Errores Sintcticos
public void yyerror (String error) {
System.err.println ("Error Sintctico en lnea " + lexico.getYyline()+
" y columna "+lexico.getYycolumn()+":\n\t"+error);
}
// * Constructor del Sintctico
public Parser(Lexico lexico) {
this.lexico = lexico;
lexico.setParser(this);
}
// * El yyparse original no es pblico
public int parse() {
return yyparse();
}
// * El yylval no es un atributo pblico
public void setYylval(ParserVal yylval) {
this.yylval=yylval;
}
Lexico.flex
package lexico;
import sintactico.ParserVal;
import sintactico.Parser;
/**
* Especificacin lxica del lenguaje.<br/>
* 13-oct-2005<br/>
* Procesadores de Lenguaje<br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo<br/>
* @author Francisco Ortin
*/
%%
// ************ Opciones ********************
// % debug // * Muy buena opcin para depurar
%byaccj
%class Lexico
%public
%unicode
%line
%column
%{
// ************ Atributos y mtodos ********************
// * El analizador sintctico
private Parser parser;
public void setParser(Parser parser) {
this.parser=parser;
}
// * Para acceder al nmero de lnea (yyline es package)
public int getYyline() {
// * Flex empieza en cero
return yyline+1;
}
// * Para acceder al nmero de columna (yycolumn es package)
public int getYycolumn() {
// * Flex empieza en cero
return yycolumn+1;
}
%}
// ************ Patrones (macros) ********************
ConstanteEntera = [0-9]+
FinDeLinea = \n | \r | \r\n
Basura = {FinDeLinea} | [ \t\f]
ComentarioC = "/*" ~"*/"
ComentarioCpp = "//".*\n
Letra = [a-zA-Z]
Identificador = {Letra}+({Letra}|[0-9])*
Integer = [Ii][Nn][Tt][Ee][Gg][Ee][Rr]
162
Comprobador de Tipos
Char = [Cc][Hh][Aa][Rr]
Begin = [Bb][Ee][Gg][Ii][Nn]
Var = [Vv][Aa][Rr]
End = [Ee][Nn][Dd]
Array = [Aa][Rr][Rr][Aa][Yy]
Function = [Ff][Uu][Nn][Cc][Tt][Ii][Oo][Nn]
Procedure = [Pp][Rr][Oo][Cc][Ee][Dd][Uu][Rr][Ee]
Record = [Rr][Ee][Cc][Oo][Rr][Dd]
Of = [Oo][Ff]
%%
// ************ Acciones ********************
// * Caracteres que ignoramos
{Basura} {}
// * Comentarios
{ComentarioC} {}
{ComentarioCpp}
{}
// * Constante Entera
{ConstanteEntera} { parser.setYylval(new ParserVal(Integer.parseInt(yytext())));
return Parser.CTE_ENTERA; }
// * Constante Caracter
'.'
{ parser.setYylval(new ParserVal(yycharat(1)));
return Parser.CTE_CARACTER; }
'\\t'
{ parser.setYylval(new ParserVal('\t'));
return Parser.CTE_CARACTER; }
'\\n'
{ parser.setYylval(new ParserVal('\n'));
return Parser.CTE_CARACTER; }
'\\[0-9]+' { StringBuffer sb=new StringBuffer(yytext());
sb.delete(0,2);
sb.delete(sb.length()-1,sb.length());
parser.setYylval(new ParserVal(Integer.parseInt(sb.toString())));
return Parser.CTE_CARACTER; }
// * Palabras reservadas
{Var}
{ parser.setYylval(new ParserVal(yytext())); return Parser.VAR; }
{Integer}
{ parser.setYylval(new ParserVal(yytext())); return Parser.INTEGER; }
{Char}
{ parser.setYylval(new ParserVal(yytext())); return Parser.CHAR; }
{Begin}
{ parser.setYylval(new ParserVal(yytext())); return Parser.BEGIN; }
{End}
{ parser.setYylval(new ParserVal(yytext())); return Parser.END; }
{Array}
{ parser.setYylval(new ParserVal(yytext())); return Parser.ARRAY; }
{Function} { parser.setYylval(new ParserVal(yytext())); return Parser.FUNCTION; }
{Procedure} { parser.setYylval(new ParserVal(yytext())); return Parser.PROCEDURE; }
{Record} { parser.setYylval(new ParserVal(yytext())); return Parser.RECORD; }
{Of}
{ parser.setYylval(new ParserVal(yytext())); return Parser.OF; }
// * Identificadores
{Identificador}
{ parser.setYylval(new ParserVal(yytext()));
return Parser.ID; }
// * Tokens de longitud 1
";" |
"," |
":" |
"." |
"]" |
"[" |
"^" |
"+" |
"-" |
"*" |
"/" |
"%" |
"=" |
"(" |
")"
{ parser.setYylval(new ParserVal(yytext()));
return (int) yycharat(0); }
".."
{ parser.setYylval(new ParserVal(yytext()));
return Parser.DOS_PUNTOS; }
Main.java
import
import
import
import
import
sintactico.Parser;
lexico.Lexico;
tipos.Error;
java.io.FileReader;
java.io.IOException;
163
/**
* Prueba del compilador.<br/>
* 13-oct-2005 <br/>
* Procesadores de Lenguaje <br/>
* Escuela Politcnica Superior de Ingenieros<br/>
* Universidad de Oviedo <br/>
*
* @author Francisco Ortin
*/
public class Main {
public static void main(String args[]) throws IOException {
if (args.length<1) {
System.err.println("Necesito el archivo de entrada.");
return;
}
FileReader fr=null;
try {
fr=new FileReader(args[0]);
} catch(IOException io) {
System.err.println("El archivo "+args[0]+" no se ha podido abrir.");
return;
}
// * Creamos lxico y sintctico
Lexico lexico = new Lexico(fr);
Parser parser = new Parser(lexico);
Error.setLexico(lexico);
// * "Parseamos"
parser.parse();
}
}
164
Comprobador de Tipos
lexico.l
%{
#include <string.h>
#include <stdlib.h>
#include "tipos.h"
#include <map>
#include <vector>
using namespace std;
#include "yytab.h"
unsigned yylineno=1;
void errorLexico(char);
void inicializarLexico(char *,char *);
%}
comentario \/\/.*\n
linea
\n
letra
[a-zA-Z]
numero
[0-9]
real
{numero}+(\.{numero}+)?
alfa
({numero}|{letra})
var
[Vv][Aa][Rr]
begin
[Bb][Ee][Gg][Ii][Nn]
end
[Ee][Nn][Dd]
function [Ff][Uu][Nn][Cc][Tt][Ii][Oo][Nn]
procedure
[Pp][Rr][Oo][Cc][Ee][Dd][Uu][Rr][Ee]
record
[Rr][Ee][Cc][Oo][Rr][Dd]
array
[Aa][Rr][Rr][Aa][Yy]
of
[Oo][Ff]
integer
[Ii][Nn][Tt][Ee][Gg][Ee][Rr]
char
[Cc][Hh][Aa][Rr]
%%
[ \t]+
; // * Se ignoran los espacions y tabuladores
{comentario}
{ yylineno++; }
165
{linea}
{ yylineno++; }
{numero}+
{ yylval.entero=atoi(yytext); return CTE_ENTERA; }
\'{alfa}\'
{ yylval.caracter=yytext[1]; return CTE_CARACTER; }
{var}
{ return VAR; }
{begin}
{ return BEGINPR;}
{end}
{ return END;}
{function}
{ return FUNCTION;}
{procedure}
{ return PROCEDURE;}
{record}
{ return RECORD;}
{array}
{ return ARRAY;}
{of}
{ return OF; }
{integer}
{ return INTEGER; }
{char}
{ return CHAR; }
{letra}{alfa}* { strcpy(yylval.cadena,yytext); return ID; }
".."
{ return DOS_PUNTOS; }
\.
|
\:
|
\,
|
\[
|
\]
|
\(
|
\)
|
\^
|
\;
|
;
{ return yytext[0]; } // Token=Caracter ASCII
.
{ errorLexico(yytext[0]); } // Caracter no perteneciente al lenguaje
%%
void errorLexico(char c) {
printf("Error lexico en linea %d. Caracter %c no perteneciente al lenguaje.\n",
yylineno,c);
exit(-1);
}
void inicializarLexico(char *fOrigen,char *fDestino) {
yyin=fopen(fOrigen,"r");
if (yyin==NULL) {
printf("Error abriendo el fichero %s.\n",fOrigen);
exit(-1);
}
yyout=fopen(fDestino,"w");
if (yyout==NULL) {
printf("Error abriendo el fichero %s.\n",fDestino);
exit(-1);
}
}
166
REFERENCIAS BIBLIOGRFICAS
[Aho90]
[ANSIC++]
Programming Language C++. Doc No: X3J16/96-0225. ANSI (American National Stardards Institute).1996.
[ANTLR]
[Atkinson86]
[Bischoff92]
Kurt M. Bischoff. User Manual for Ox: an Attribute Grammar Compiling System
based on Yacc, Lex and C. Technical Report 92-30. Department of Computer
Science, Iowa State University. 1992.
[Bjorner82]
[Cardelli97]
Luca Cardelli. Type Systems. The Computer Science and Engineering Handbook,
CRC Press. 1997.
[Cueva03]
J.M. Cueva, R. Izquierdo, A.A. Juan, M.C. Luengo, F. Ortn, J.E. Labra. Lenguajes, Gramticas y Autmatas en Procesadores de Lenguaje. Servitec. 2003.
[Cueva98]
[Eckel00]
[Engelfriet84]
[FNC-2]
The
FNC-2
attribute
rocq.inria.fr/oscar/www/fnc2/
[GOF02]
Erich Gamma, Richard Elm, Ralph Johnson, John Vlissides. Patrones de diseo:
elementos de software orientado a objetos reutilizable. Pearson Educacin. 2002.
[Goldberg83]
Adele Goldberg, David Robson. Smalltalk-80: the language and its implementation. Addison-Wesley. 1983.
[Gosling00]
[Hoare73]
[Hopcroft02]
[Jansen93]
[JavaCC]
[Jazayeri75]
grammar
system.
http://www-
167
[Johnson75]
S. C. Johnson. YACC yet another compiler compiler. Technical Report Computing Science TR32, AT&T Bell Laboratories, Murray Hill. 1975.
[Kernighan91]
[Kernighan91]
[Knuth68]
[Labra01]
[Labra03]
J.E. Labra, J.M. Cueva, R. Izquierdo, A.A. Juan, M.C. Luengo, F. Ortn. Intrpretes y Diseo de Lenguajes de Programacin. Servitec. 2003.
[Louden97]
[LRC]
System
for
generating efficient incremental
http://www.cs.uu.nl/groups/ST/Software/LRC/
[Lucas69]
Peter Lucas, Kurt Walk. On the Formal Description of PL/I. Annual Review of
Automatic Programming. Oxford Pergamon Press. 1969.
[Mason92]
Tony Mason, John Levine, Doug Brown. Lex & Yacc. 2nd Edition. O'Reilly &
Associates. 1992.
[Meinke92]
[Milner78]
Robin Milner. A theory of type polymorphism in programming. Journal of Computer and Systems Sciences 17. 1978.
[Milner84]
Robin Milner. A proposal for standard ML. CDM Symposium on Lisp and Functional Programming Languages. 1984.
[Mosses91]
attribute
evaluators.
[Nielson92]
[Pascal82]
[Pierce02]
[SableCC]
[Saraiva99]
[Scott00]
[Stroustrup93]
[Waite84]
168
Referencias Bibliogrficas
[Watt00]
David A. Watt, Deryck Brown.Programming Language Processors in Java: Compilers and Interpreters. Prentice Hall. 2000.
[Watt96]
[Wilhelm95]
169