Documentos de Académico
Documentos de Profesional
Documentos de Cultura
LibroCompiladores Galvez PDF
LibroCompiladores Galvez PDF
Sun, el logotipo de Sun, Sun M icrosystems y Java son marcas o marcas registradas de Sun M icrosystems
Inc. en los EE.U U . y otros pases. El personaje de Duke es una marca de Sun M icrosystems Inc.
PC Lex y PC Y acc son productos de A braxas Softw are Inc.
JFlex est liberado con licencia GPL.
Cup est protegido por las licencias de cdigo abierto, siendo compatible con la licencia GPL.
JavaC C est sujeto a la licencia BSD (B erkeley Software Distribution) de cdigo abierto.
Compiladores
Traductores y Compiladores
con Lex/Yacc, JFlex/cup y JavaCC
ndice
Prlogo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vii
Captulo 1 Introduccin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1 Visin general . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Concepto de traductor. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2.1 Tipos de traductores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2.1.1 Traductores del idioma . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2.1.2 Compiladores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2.1.3 Intrpretes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2.1.4 Preprocesadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2.1.5 Intrpretes de comandos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2.1.6 Ensambladores y macroensambladores . . . . . . . . . . . . . . . . . 5
1.2.1.7 Conversores fuente-fuente . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.2.1.8 Compilador cruzado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.2.2 Conceptos bsicos relacionados con la traduccin . . . . . . . . . . . 6
1.2.2.1 Compilacin, enlace y carga. . . . . . . . . . . . . . . . . . . . . . . . . 6
1.2.2.2 Pasadas de compilacin . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.2.2.3 Compilacin incremental . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.2.2.4 Autocompilador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.2.2.5 Metacompilador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.2.2.6 Descompilador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3 Estructura de un traductor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3.1 Construccin sistemtica de compiladores . . . . . . . . . . . . . . . . 12
1.3.2 La tabla de smbolos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.4 Ejemplo de compilacin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.4.1 Preprocesamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.4.2 Etapa de anlisis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.4.2.1 Fase de anlisis lexicogrfico . . . . . . . . . . . . . . . . . . . . . . . 17
1.4.2.2 Fase de anlisis sintctico . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.4.2.2.1 Compilacin dirigida por sintaxis . . . . . . . . . . . . . 18
1.4.2.3 Fase de anlisis semntico . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.4.3 Etapa de sntesis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.4.3.1 Fase de generacin de cdigo intermedio . . . . . . . . . . . . . . 19
1.4.3.2 Fase de optimizacin de cdigo . . . . . . . . . . . . . . . . . . . . . 20
1.4.3.3 Fase de generacin de cdigo mquina . . . . . . . . . . . . . . . 20
i
ndice
ii
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
iii
ndice
iv
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
5.6.5 Node Scope Hooks . . . . . . . . . . .... .... ... . ... . ... . ... . 160
5.6.6 El ciclo de vida de un nodo . . . .... .... ... . ... . ... . ... . 163
5.6.7 Visitor Support . . . . . . . . . . . . . .... .... ... . ... . ... . ... . 164
5.6.7.1 Parmetros de Ejecucin . . .... .... ... . ... . ... . ... . 164
5.6.8 Estados en JTree . . . . . . . . . . . .... .... ... . ... . ... . ... . 165
5.6.8.1 Objetos Node . . . . . . . . . . .... .... ... . ... . ... . ... . 165
5.6.8.2 Pasos de Ejecucin . . . . . . .... .... ... . ... . ... . ... . 166
5.6.9 Manual de Referencia de JJDoc .... .... ... . ... . ... . ... . 167
v
ndice
vi
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Prlogo
vii
Prlogo
viii
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Captulo 1
Introduccin
1
Introduccin
2
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
1.2.1.2 Compiladores
Es aquel traductor que tiene como entrada una sentencia en lenguaje formal
y como salida tiene un fichero ejecutable, es decir, realiza una traduccin de un cdigo
de alto nivel a cdigo mquina (tambin se entiende por compilador aquel programa
que proporciona un fichero objeto en lugar del ejecutable final).
1.2.1.3 Intrpretes
Es como un compilador, solo que la salida es una ejecucin. El programa de
entrada se reconoce y ejecuta a la vez. No se produce un resultado fsico (cdigo
mquina) sino lgico (una ejecucin). Hay lenguajes que slo pueden ser interpretados,
como p.ej. SNOBOL (StriNg Oriented SimBOlyc Language), LISP (LISt Processing),
algunas versiones de BASIC (Beginners All-purpose Symbolic Instruction Code), etc.
Su principal ventaja es que permiten una fcil depuracin. Entre los
inconvenientes podemos citar, en primer lugar, la lentitud de ejecucin , ya que al
ejecutar a la vez que se traduce no puede aplicarse un alto grado de optimizacin; por
ejemplo, si el programa entra en un bucle y la optimizacin no est muy afinada, las
mismas instrucciones se interpretarn y ejecutarn una y otra vez, enlenteciendo la
ejecucin del programa. Otro inconveniente es que durante la ejecucin, el intrprete
debe residir en memoria, por lo que consumen ms recursos.
3
Introduccin
1.2.1.4 Preprocesadores
Permiten modificar el programa fuente antes de la verdadera compilacin.
Hacen uso de macroinstrucciones y directivas de compilacin. Por ejemplo, en lenguaje
C, el preprocesador sustituye la directiva #include Uno.c por el cdigo completo que
contiene el fichero Uno.c, de manera que cuando el compilador comienza su
ejecucin se encuentra con el cdigo ya insertado en el programa fuente (la figura 1.3
ilustra esta situacin). Algunas otras directivas de preprocesamiento permiten compilar
trozos de cdigos opcionales (lenguajes C y Clipper): #fi, #ifdef, #define, #ifndef, etc.
Los preprocesadores suelen actuar de manera transparente para el programador,
pudiendo incluso considerarse que son una fase preliminar del compilador.
4
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
5
Introduccin
con lo que se consigue una mayor portabilidad en los programas de alto nivel.
Por ejemplo, si un ordenador slo dispone de un compilador de Pascal, y
queremos ejecutar un programa escrito para otra mquina en COBOL, pues un
conversor de COBOL a Pascal solucionar el problema. No obstante el programa
fuente resultado puede requerir retoques manuales debido a diversos motivos:
En situaciones en que el lenguaje destino carece de importantes caractersticas
que el lenguaje origen s tiene. Por ejemplo un conversor de Java a C,
necesitara modificaciones ya que C no tiene recolector de basura.
En situaciones en que la traduccin no es inteligente y los programas destino
son altamente ineficientes.
6
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
7
Introduccin
Figura 1.6. Labor realizada por el cargador. El cargador suele ser parte del sistema
operativo
Cada vez que una instruccin mquina hace referencia a una direccin de
memoria (partiendo de la direccin 0), el microprocesador se encarga automticamente
de sumar a dicha direccin la direccin absoluta de inicio de su segmento. Por ejemplo
para acceder a la variable x almacenada en la direccin 1Fh, que se encuentra en el
segmento de datos ubicado en la direccin 8A34h, la instruccin mquina har
referencia a 1Fh, pero el microprocesador la traducir por 8A34h+1Fh dando lugar a
un acceso a la direccin 8A53h: dir absoluta del segmento en memoria + dir relativa
de x en el segmento = dir absoluta de x en memoria.
8
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
9
Introduccin
1.2.2.4 Autocompilador
Es un compilador escrito en el mismo lenguaje que compila (o parecido).
Normalmente, cuando se extiende entre muchas mquinas diferentes el uso de un
compilador, y ste se desea mejorar, el nuevo compilador se escribe utilizando el
lenguaje del antiguo, de manera que pueda ser compilado por todas esas mquinas
diferentes, y d como resultado un compilador ms potente de ese mismo lenguaje.
1.2.2.5 Metacompilador
Este es uno de los conceptos ms importantes con los que vamos a trabajar.
Un metacompilador es un compilador de compiladores. Se trata de un programa que
acepta como entrada la descripcin de un lenguaje y produce el compilador de dicho
lenguaje. Hoy por hoy no existen metacompiladores completos, pero s parciales en los
que se acepta como entrada una gramtica de un lenguaje y se genera un autmata que
reconoce cualquier sentencia del lenguaje . A este autmata podemos aadirle cdigo
para completar el resto del compilador. Ejemplos de metacompiladores son: Lex,
YACC, FLex, Bison, JavaCC, JLex, Cup, PCCTS, MEDISE, etc.
Los metacompiladores se suelen dividir entre los que pueden trabajar con
gramticas de contexto libre y los que trabajan con gramticas regulares. Los primeros
se dedican a reconocer la sintaxis del lenguaje y los segundos trocean los ficheros
fuente y lo dividen en palabras.
PCLex es un metacompilador que genera la parte del compilador destinada a
reconocer las palabras reservadas. PCYACC es otro metacompilador que genera la
parte del compilador que informa sobre si una sentencia del lenguaje es vlida o no.
JavaCC es un metacompilador que ana el reconocimiento de palabras reservadas y la
aceptacin o rechazo de sentencias de un lenguaje. PCLex y PCYACC generan cdigo
C y admiten descripciones de un lenguaje mediante gramticas formales, mientras que
JavaCC produce cdigo Java y admite descripciones de lenguajes expresadas en
notacin BNF (Backus-Naur Form). Las diferencias entre ellas se irn estudiando en
10
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
temas posteriores.
1.2.2.6 Descompilador
Un descompilador realiza una labor de traduccin inversa, esto es, pasa de un
cdigo mquina (programa de salida) al equivalente escrito en el lenguaje que lo gener
(programa fuente). Cada descompilador trabaja con un lenguaje de alto nivel concreto.
La descompilacin suele ser una labor casi imposible, porque al cdigo
mquina generado casi siempre se le aplica una optimizacin en una fase posterior, de
manera que un mismo cdigo mquina ha podido ser generado a partir de varios
cdigos fuente. Por esto, slo existen descompiladores de aquellos lenguajes en los que
existe una relacin biyectiva entre el cdigo destino y el cdigo fuente, como sucede
con los desensambladores, en los que a cada instruccin mquina le corresponde una
y slo una instruccin ensamblador.
Los descompiladores se utilizan especialmente cuando el cdigo mquina ha
sido generado con opciones de depuracin, y contiene informacin adicional de ayuda
al descubrimiento de errores (puntos de ruptura, seguimiento de trazas, opciones de
visualizacin de variables, etc.).
Tambin se emplean cuando el compilador no genera cdigo mquina puro,
sino pseudocdigo para ser ejecutado a travs de un pseudointrprete. En estos casos
suele existir una relacin biyectiva entre las instrucciones del pseudocdigo y las
construcciones sintcticas del lenguaje fuente, lo que permite reconstruir un programa
de alto nivel a partir del de un bloque de pseudocdigo.
11
Introduccin
12
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Figura 1.9. Creacin de tres compiladores (Pascal, C y COBOL) para una misma mquina Intel
13
Introduccin
Muestra una situacin en la que se quiere crear un compilador del lenguaje C para tres
mquinas diferentes: Dec-Alpha (Unix), Motorola (Mac OS) e Intel (MS-DOS). Cada
bloque de lneas punteadas agrupa un front-end con un back-end dando lugar a un
compilador completo.
De manera inversa se podran construir tres compiladores de Pascal, C y
COBOL para una misma mquina, p.ej. Intel, como se ilustra en la figura 1.9.
Por ltimo, la creacin de compiladores de Pascal, C y COBOL para las
mquinas Dec-Alpha, Motorola e Intel, pasara por la combinacin de los mtodos
anteriores, tal como ilustra la figura 1.10.
14
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
bastante eficientes.
Tanto la etapa de anlisis como la de sntesis accede a esta estructura, por lo
que se halla muy acoplada al resto de fases del compilador. Por ello conviene dotar a
la tabla de smbolos de una interfaz lo suficientemente genrica como para permitir el
cambio de las estructuras internas de almacenamiento sin que estas fases deban ser
retocadas. Esto es as porque suele ser usual hacer un primer prototipo de un
compilador con una tabla de smbolos fcil de construir (y por tanto, ineficiente), y
cuando el compilador ya ha sido finalizado, entonces se procede a sustituir la tabla de
smbolos por otra ms eficiente en funcin de las necesidades que hayan ido surgiendo
a lo largo de la etapa de desarrollo anterior. Siguiendo este criterio, el esquema general
definitivo de un traductor se detalla en la figura 1.11. La figura 1.12 ilustra el esquema
por fases, donde cada etapa ha sido sustituida por las fases que la componen y se ha
hecho mencin explcita del preprocesador..
15
Introduccin
1.4.1 Preprocesamiento
Como ya hemos visto, el cdigo fuente de una aplicacin se puede dividir en
mdulos almacenados en archivos distintos. La tarea de reunir el programa fuente a
menudo se confa a un programa distinto, llamado preprocesador. El preprocesador
tambin puede expandir abreviaturas, llamadas macros, a proposiciones del lenguaje
fuente. En nuestro ejemplo, la constante PORCENTAJE se sustituye por su valor,
dando lugar al texto:
comision = fijo + valor * 8;
que pasa a ser la fuente que entrar al compilador.
16
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
17
Introduccin
18
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
19
Introduccin
20
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
registros.
Siguiendo el mismo ejemplo, y utilizando los registros R1 y R2 de un
microprocesador hipottico, la traduccin del cdigo optimizado podra ser:
MOVE [1Ah], R1
MULT #8.0, R1
MOVE [15h], R2
ADD R1, R2
MOVE R2, [10h]
El primer y segundo operandos de cada instruccin especifican una fuente y
un destino, respectivamente. Este cdigo traslada el contendido de la direccin [1Ah]
al registro R1, despus lo multiplica por la constante real 8.0. La tercera instruccin
pasa el contenido de la direccin [15h] al registro R2. La cuarta instruccin le suma el
valor previamente calculado en el registro R1. Por ltimo el valor del registro R2 se
pasa a la direccin [10h]. Como el lector puede suponer, la variable comision se
almacena en la direccin [10h], fijo en [15h] y valor en [1Ah].
21
Introduccin
22
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Captulo 2
Anlisis lexicogrfico
23
Anlisis lexicogrfico
Figura 2.1. Entradas y salidas de las dos primeras fases de la etapa de anlisis.
La frase Secuencia de Terminales hace referencia a la gramtica del sintctico;
pero tambin es posible considerar que dicha secuencia es de no terminales si
usamos el punto de vista del lexicogrfico.
Figura 2.2. La fase de anlisis lxico se halla bajo el control del anlisis
sintctico. Normalmente se implementa como una funcin de ste
24
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
25
Anlisis lexicogrfico
podemos asumir que dos expresiones pueden ir conectadas con cualquiera de dichos
operadores, por lo que se puede optar por agruparlos todos bajo la categora lxica
OPARIT (Operadores ARITmticos). En una fase posterior, puede resultar necesario
disgregar dicha categora en tantas otras como operadores semnticamente diferentes
haya: OPARIT desaparece y aparecen M AS, M ENOS, M ULT y DIV. Una
modificacin tal resulta trivial si se han separado adecuadamente ambos analizadores,
ya que consiste en sustituir el patrn agrupado (-|+|*|/) por los patrones
disgregados -, +, * y /.
26
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
| NUM NUM
sin embargo, los autmatas destinados a reconocer los componentes lxicos y al rbol
sintctico son radicalmente diferentes, tanto en su concepcin como en su
implementacin lo que, de nuevo, nos lleva a establecer una divisin entre estos
anlisis.
A modo de conclusin, se puede decir que es muy recomendable trabajar con
dos gramticas, una que se encarga del anlisis lxico y otra que se encarga del anlisis
sintctico. Dnde se pone el lmite entre lo que reconoce una y otra gramtica?, qu
se considera un componente bsico?, cuntas categoras gramaticales se establecen?
Si se crean muchas categoras gramaticales se estar complicando la gramtica del
sintctico, como ocurre p.ej. en el paso 2. En general, deben seguirse un par de reglas
bsicas para mantener la complejidad en unos niveles admisibles. La primera es que la
informacin asociada a cada categora lxica debe ser la necesaria y suficiente, lo que
quedar ms claro en captulos posteriores, cuando se conozca el concepto de atributo.
La segunda es que, por regla general, las gramticas que se planteen (regular y de
contexto libre) no deben verse forzadas, en el sentido de que los distintos conceptos
que componen el lenguaje de programacin a compilar deben formar parte de una o de
otra de forma natural; p.ej., el reconocimiento que deba hacerse carcter a carcter (sin
que stos tengan un significado semntico por s solos) debe formar parte del anlisis
lxico.
2.2.2.2 Eficiencia
La divisin entre anlisis lxico y sintctico tambin mejora la eficiencia del
compilador. Un analizador lxico independiente permite construir un procesador
especializado y potencialmente ms eficiente para las funciones explicadas en el
epgrafe 2.2.1. Gran parte del tiempo de compilacin se invierte en leer el programa
fuente y dividirlo en componentes lxicos. Con tcnicas especializadas de manejo de
buffers para la lectura de caracteres de entrada y procesamiento de patrones se puede
mejorar significativamente el rendimiento de un compilador.
2.2.2.3 Portabilidad
Se mejora la portabilidad del compilador, ya que las peculiaridades del
alfabeto de partida, del juego de caracteres base y otras anomalas propias de los
dispositivos de entrada pueden limitarse al analizador lxico. La representacin de
smbolos especiales o no estndares, como 8 en Pascal, se pueden aislar en el
analizador lxico.
Por otro lado, algunos lenguajes, como APL (A Program Language) se
benefician sobremanera del tratamiento de los caracteres que forman el programa de
entrada mediante un analizador aislado. El Diccionario de Informtica de la Oxford
University Press define este lenguaje de la siguiente forma:
... Su caracterstica principal es que proporciona un conjunto muy grande
de operadores importantes para tratar las rdenes multidimensionales junto con la
27
Anlisis lexicogrfico
capacidad del usuario de definir sus propios operadores. Los operadores incorporados
se encuentran representados, principalmente, por caracteres solos que utilizan un
conjunto de caracteres especiales. De este modo, los programas APL son muy concisos
y, con frecuencia, impenetrables.
28
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
categora lxica.
Todo esto nos lleva a los siguientes conceptos de fundamental importancia a
lo largo de nuestro estudio:
O Patrn: es una expresin regular.
O Token: es la categora lxica asociada a un patrn. Cada token se convierte en
un nmero o cdigo identificador nico. En algunos casos, cada nmero tiene
asociada informacin adicional necesaria para las fases posteriores de la etapa
de anlisis. El concepto de token coincide directamente con el concepto de
terminal desde el punto de vista de la gramtica utilizada por el analizador
sintctico.
O Lexema: Es cada secuencia de caracteres concreta que encaja con un patrn.
P.ej: 8", 23" y 50" son algunos lexemas que encajan con el patrn
(0'|1'|2'| ... |9') +. El nmero de lexemas que puede encajar con un patrn
puede ser finito o infinito, p.ej. en el patrn W HILE slo encaja el
lexema WHILE.
Una vez detectado que un grupo de caracteres coincide con un patrn, se
considera que se ha detectado un lexema. A continuacin se le asocia el nmero de su
categora lxica, y dicho nmero o token se le pasa al sintctico junto con informacin
adicional, si fuera necesario. En la figura 1.13 puede verse cmo determinadas
categoras llevan informacin asociada, y otras no.
Por ejemplo, si se necesita construir un analizador lxico que reconozca los
nmeros enteros, los nmeros reales y los identificadores de usuario en minsculas, se
puede proponer una estructura como:
Expresin Regular Terminal asociado
(0 ... 9) + NUM_ENT
(0 ... 9) *. (0 ... 9) + NUM_REAL
(a ... z) (a ... z 0 ... 9) * ID
Asociado a la categora gramatical de nmero entero se tiene el token
NUM _ENT que puede equivaler, p.ej. al nmero 280; asociado a la categora
gramatical nmero real se tiene el token NUM _REAL que equivale al nmero 281; y
la categora gramatical identificador de usuario tiene el token ID que equivale al
nmero 282. As, la estructura expresin regular-accin sera la siguiente (supuesto que
las acciones las expresamos en C o Java):
(0 ... 9) + { return 280;}
(0 ... 9) *. (0 ... 9) + { return 281;}
(a ... z) (a ... z 0...9) * { return 282;}
{ }
De esta manera, un analizador lxico que obedeciera a esta estructura, si
durante su ejecucin se encuentra con la cadena:
95.7 99 cont
29
Anlisis lexicogrfico
intentar leer el lexema ms grande de forma que, aunque el texto 95" encaja con el
primer patrn, el punto y los dgitos que le siguen .7" hacen que el analizador decida
reconocer 95.7" como un todo, en lugar de reconocer de manera independiente 95"
por un lado y .7" por otro; as se retorna el token NUM_REAL. Resulta evidente que
un comportamiento distinto al expuesto sera una fuente de problemas. A continuacin
el patrn y la accin asociada permiten ignorar los espacios en blanco. El 99"
coincide con el patrn de NUM_ENT, y la palabra cont con ID.
Para facilitar la comprensin de las acciones asociadas a los patrones, en vez
de trabajar con los nmeros 280, 281 y 282 se definen mnemotcnicos.
# define NUM_ENT 280
# define NUM_REAL 281
# define ID 282
( \t \n)
(0 ... 9)+ {return NUM_ENT;}
(0 ... 9) *. (0 ... 9) + {return NUM_REAL;}
(a ... z) (a ... z 0 ... 9) * {return ID;}
En esta nueva versin, los lexemas que entran por el patrn ( \t \n) no tienen
accin asociada, por lo que, por defecto, se ejecuta la accin nula o vaca.
El asociar un nmero (token) a cada categora gramatical se suele emplear
mucho en los metacompiladores basados en lenguajes puramente imperativos, como
pueda ser C o Pascal. Los metacompiladores ms modernos basados en programacin
orientada a objetos tambin asocian un cdigo a cada categora gramatical, pero en
lugar de retornar dicho cdigo, retornan objetos con una estructura ms compleja.
30
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
31
Anlisis lexicogrfico
32
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
33
Anlisis lexicogrfico
34
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
\noctal: representa el carcter cuyo valor ASCII es n octal. P.ej: \012 reconoce
el carcter decimal 10 que se corresponde con LF (Line Feed).
[ ]: permiten especificar listas de caracteres, o sea uno de los caracteres que
encierra, ej.: [abc] reconoce o la a, o la b, o la c, ( [abc] / (a|b|c) ).
Dentro de los corchetes los siguientes caracteres tambin tienen un sentido
especial:
-: indica rango. Ej.: [A-Z0-9] reconoce cualquier carcter de la A a
la Z o del 0' a 9'.
^: indica complecin cuando aparece al comienzo, justo detrs de [.
Ej.: [^abc] reconoce cualquier carcter excepto la a, la b o la c.
Ej.: [^A-Z] reconoce cualquier carcter excepto los de la A a la
Z.
?: aquello que le precede es opcional1. Ej.: a? / ( a | g ). Ej.: [A-Z]? reconoce
cualquier letra de la A a la Z o bien g. Ej.: a?b / ab | gb.
.: representa a cualquier carcter (pero slo a uno) excepto el retorno de carro
(\n). Es muy interesante porque nos permite recoger cualquier otro carcter
que no sea reconocido por los patrones anteriores.
|: indica opcionalidad (OR). Ej.: a|b reconoce a la a o a la b. Ej.: .|\n reconoce
cualquier carcter. Resulta curioso el patrn (.|\n)* ya que por aqu entra el
programa entero, y como yylex() tiene la premisa de reconocer el lexema ms
largo, pues probablemente ignorar cualquier otro patrn. Tambin resulta
probable que durante el reconocimiento se produzca un error por
desbordamiento del espacio de almacenamiento de la variable yytext que,
recordemos, almacena el lexema actual.
*: indica repeticin 0 o ms veces de lo que le precede.
+: indica repeticin 1 o ms veces de lo que le precede.
( ): permiten la agrupacin (igual que en las expresiones aritmticas).
{ }: indican rango de repeticin. Ej.: a{1,5} / aa?a?a?a? Las llaves vienen a ser
algo parecido a un * restringido. Tambin nos permite asignarle un nombre
a una expresin regular para reutilizarla en mltiples patrones; esto se ver
ms adelante.
1
La expresin g representa a la cadena vaca.
35
Anlisis lexicogrfico
36
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
37
Anlisis lexicogrfico
38
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
39
Anlisis lexicogrfico
es equivalente a:
abc*\n { yyless(yyleng-1); }
input(): consume el siguiente carcter de la entrada y lo aade al lexema actual.
P,ej., el programa:
%%
abc { printf (%s, yytext);
input( );
printf(%s, yytext);}
ante la entrada abcde entrara por este patrn: el lexema antes del input()
(en el primer printf) es abc, y despus del input (en el segundo printf) es
abcd.
output(char c): emite el carcter c por la salida estndar. PCLex para DOS no
soporta esta funcin, pero s el Lex para Unix.
unput(char c): des-consume el carcter c y lo coloca al comienzo de la entrada.
P.ej., supongamos que el programa:
%%
abc { printf (%s,yytext); unput(t); }
tal { printf (%s,yytext); }
recibe la entrada abcal. Los tres primeros caracteres del lexema coinciden
con el primer patrn. Despus queda en la entrada la cadena al, pero como
se ha hecho un unput(t), lo que hay realmente a la entrada es tal, que
coincide con el segundo patrn.
ECHO: macro que copia la entrada en la salida (es la accin que se aplica por
defecto a los lexemas que no encajan por ningn patrn explcito).
yymore(): permite pasar a reconocer el siguiente lexema, pero sin borrar el
contenido actual de yytext, por lo que el nuevo lexema ledo se concatena al
ya existente. PCLex para DOS no soporta esta funcin, pero s el Lex para
Unix.
La utilidad principal de yymore() reside en que facilita el reconocer
los literales entrecomillados. Supongamos que se desea construir un patrn
para reconocer literales entrecomillados. Una primera idea podra ser el
patrn:
\[^\]*\ /* Lexemas que empiecen por comillas,
cualquier cosa, y termine en comillas. */
que reconocera cadenas como Hola, adis, e incluso Ho;*. Sin
embargo una cadena como Hola, \camarada\ que, a su vez contiene
comillas dentro, dar problemas porque la primera comilla de camarada, es
considerada como unas comillas de cierre. La solucin a este problema pasa
por el siguiente bloque de cdigo Lex:
\[^\]*/\ { if (yytext[yyleng-1]==\\)
yymore();
40
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
else
input();}
Este patrn reconoce cadenas entrecomilladas (excepto las ltimas comillas),
de forma que las cadenas que tienen dentro comillas se reconocen por partes.
Siguiendo con el ejemplo de antes, Hola, \camarada\ se reconocera
como:
Este patrn fallara si los caracteres anteriores a las comillas de cierre fuesen
precisamente \\. Para solucionarlo habra que hacer uso de unput y de una
variable global que actuase como flag. Cualquier sentencia que haya detrs
de yymore() nunca se ejecutar, ya que acta como una especie de goto.
REJECT: rechaza el lexema actual, lo devuelve a la entrada y busca otro patrn.
REJECT le dice a yylex() que el lexema encontrado no corresponde
realmente a la expresin regular en curso, y que busque la siguiente expresin
regular a que corresponda. La macro REJECT debe ser lo ltimo que una
accin ejecute puesto que si hay algo detrs, no se ejecutar. P.ej. Para buscar
cuntas veces aparecen las palabras teclado y lado, se puede construir el
siguiente programa Lex:
%{
int t=0, l=0;
%}
%%
teclado {t ++; REJECT;}
lado {l ++;}
Ante la entrada teclado este programa se comporta de la siguiente forma:
1.- Entra por el patrn que lee el lexema ms largo que sera
teclado y ejecuta la accin asociada: se incrementa la variable t y
se rechaza el lexema actual, por lo que el texto entero se devuelve a
la entrada.
2.- Saca por pantalla tec que es la accin por defecto cuando se
encuentran caracteres que no encajan con ningn patrn.
3.- El lexema lado coincide con el segundo patrn y ejecuta la
accin asociada: se incrementa la variable l.
Para finalizar, resulta evidente que el programador no debe declarar ninguna
funcin o variable cuyo nombre coincida con las que acabamos de estudiar. De hecho
PCLex tambin genera una serie de variables auxiliares cuyo nombre consta de un solo
carcter, por lo que tampoco es bueno declarar variables o funciones con nombres de
una sola letra ni que empiecen por yy.
41
Anlisis lexicogrfico
Figura 2.5. Obtencin de un programa portable en Java a partir de una especificacin JFlex
42
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
43
Anlisis lexicogrfico
completas e interfaces.
A continuacin se describe en profundidad las siguientes dos reas.
2.5.2.1 Opciones
Las opciones ms interesantes se pueden clasificar en opciones de clase, de
la funcin de anlisis, de fin de fichero, de juego de caracteres y de contadores. Todas
ellas empiezan por el carcter % y no pueden estar precedidas por nada en la lnea en
que aparecen.
2.5.2.1.1 Opciones de clase
Las opciones de clase ms tiles son:
% class nombreClase. Por defecto, la clase que genera JFlex y que implementa
el analizador lexicogrfico se llama Yylex y se escribe, por tanto, en un
fichero Yylex.java. Utilizando esta opcin puede cambiarse el nombre de
dicha clase.
% implements interface1, interface2, etc. Genera una clase (por defecto Yylex)
que implementa las interfaces indicadas. El programador deber introducir los
mtodos necesarios para hacer efectiva dicha implementacin.
% extends nombreClase. Genera una clase que hereda de la clase indicada.
% public. Genera una clase pblica. La clase generada por defecto no posee
modificar de mbito de visibilidad.
% final. Genera una clase final, de la que nadie podr heredar.
% abstract. Genera una clase abstracta de la que no se pueden crear objetos.
% { bloqueJava % }.El programador tambin puede incluir sus propias
declaraciones y mtodos en el interior de la clase Yylex escribiendo stas
entre los smbolos % { y % }.
% init{ cdigoJava % init}. Es posible incluir cdigo en el constructor generado
automticamente por JFlex para la clase Yylex mediante el uso de esta
opcin.
2.5.2.1.2 Opciones de la funcin de anlisis
Estas opciones permiten modificar el mtodo o funcin encargada de realizar
el anlisis lexicogrfico en s. Las opciones ms tiles son:
44
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
45
Anlisis lexicogrfico
2.5.2.2 Declaraciones.
Adems de opciones, el programador puede indicar declaraciones de dos tipos
en el rea que nos ocupa, a saber, declaraciones de estados lxicos y declaraciones de
reglas.
2.5.2.2.1 Declaraciones de estados lxicos.
Los estado lxicos se declaran mediante la opcin:
%state estado1, estado2, etc.
y pueden usarse en el rea de reglas de la misma manera que en Lex. Adems, si hay
muchos patrones en el rea de reglas que comparten el mismo estado lxico, es posible
indicar ste una sola vez y agrupar a continuacin todas estas reglas entre llaves, como
puede observarse en el ejemplo preliminar del punto 2.5.1. El estado inicial viene dado
por la constante YYINITIAL y, a diferencia de Lex, puede utilizarse como cualquier
otro estado lxico definido por el usuario.
Una accin lxica puede cambiar de estado lxico invocando a la funcin
yybegin(estadoLxico) generada por JFlex.
2.5.2.2.2 Declaraciones de reglas.
En caso de que un patrn se utilice repetidas veces o cuando su complejidad
es elevada, es posible asignarle un nombre y utilizarlo posteriormente en cualquier otra
regla encerrndolo entre llaves, de manera anloga a como se estudi en Lex. La
sintaxis es:
nombre = patron
46
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
47
Anlisis lexicogrfico
48
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Captulo 3
Anlisis sintctico
49
Anlisis sintctico
50
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
51
Anlisis sintctico
52
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
E E+T
| T
T T*F
| F
F id
| num
| (E) Cuadro 3.1 Gramtica no ambigua que
reconoce expresiones aritmticas
3.4.1 Derivaciones
Una regla de produccin puede considerarse como equivalente a una regla de
reescritura, donde el no terminal de la izquierda es sustituido por la pseudocadena del
lado derecho de la produccin. Podemos considerar que una pseudocadena es cualquier
secuencia de terminales y/o no terminales. Formalmente, se dice que una pseudocadena
deriva en una pseudocadena , y se denota por Y , cuando:
53
Anlisis sintctico
Por ejemplo, partiendo de la gramtica del cuadro 3.2, vamos a realizar todas
las derivaciones a derecha e izquierda que podamos, a partir del axioma inicial, y
teniendo como objetivo construir la cadena de tokens: ( id 1 * id 2 + id 3). En cada
pseudocadena aparece apuntado por una flechita el no terminal que se escoge para
realizar la siguiente derivacin.
E E+E
| E*E
| num
| id
Cuadro 3.2 Gramtica ambigua que
| (E)
reconoce expresiones aritmticas
54
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
las derivaciones que hemos visto parten tambin del axioma inicial. De esta forma, en
cada derivacin se selecciona un nodo hoja del rbol que se est formando y se le
aaden sus hijos. En cada paso, las hojas del rbol que se va construyendo constituyen
la forma sentencial por la que discurre la derivacin. Por otro lado, la secuencia de
derivaciones no slo representa a un rbol sintctico, sino que tambin da informacin
de en qu orden se han ido aplicando las reglas de produccin.
La construccin de este rbol conlleva la aplicacin inteligente de las reglas
de produccin. Si la sentencia a reconocer es incorrecta, esto es, no pertenece al
lenguaje generado por la gramtica, ser imposible obtener su rbol sintctico. En otras
ocasiones una sentencia admite ms de un rbol sintctico. Cuando esto ocurre se dice
que la gramtica es ambigua. Este tipo de situaciones hay que evitarlas, de suerte que,
o bien se cambia la gramtica, o bien se aplican reglas desambiguantes (que veremos
posteriormente). Por ejemplo, la gramtica del cuadro 3.2 es ambigua puesto que ante
una suma mltiple no permite distinguir entre asociatividad a la derecha o a la
izquierda, esto es, ante la sentencia aux + cont + i que se traduce por los tokens: id aux
55
Anlisis sintctico
+ id cont + id i se pueden obtener los dos rboles distintos de la figura 3.3 (hemos puesto
uno arriba y otro abajo, aunque ambos se formaran de la misma manera).
56
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
E T+E
| T
T F*T
| F
F id
Cuadro 3.3 Gramtica no ambigua ni
| num
recursiva por la izquierda que reconoce
| (E)
expresiones aritmticas
57
Anlisis sintctico
( id + num ) * id + num
Mediante el rbol de la figura 3.4 se pueden derivar todas las posibles
sentencias reconocibles por la gramtica propuesta y el algoritmo que sigue el anlisis
descendente con retroceso consiste hacer una bsqueda en este rbol de la rama que
culmine en la sentencia a reconocer mediante un recorrido primero en profundidad.
Cuando se desecha una rama que posee la forma sentencial ? Pues, p.ej.,
cuando la secuencia de tokens a la izquierda del primer no terminal de no coincide
Figura 3.4. rbol universal de la gramtica del cuadro 3.3. Slo se han
representado las derivaciones a izquierda y los cuatro primeros niveles
del rbol
58
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
59
Anlisis sintctico
60
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
g|B
Repeticin 1 o ms veces
{B}
0 o ms veces
[B]
61
Anlisis sintctico
62
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
sent ( );
Si token == ; entonces
get_token()
si no
Error sintctico!
Fin si
Si token == FIN entonces
get_token()
si no
Error sintctico!
Fin si
Fin Prog2
Es importante darse cuenta de que para que el analizador sintctico pueda
comenzar el reconocimiento, la variable token debe tener ya un valor pre-cargado; a
esta variable se la llama token de prebsqueda (lookahead), y es de fundamental
importancia, puesto que es la que nos permitir tomar la decisin de por dnde debe
continuar el flujo en el diagrama de sintaxis, como veremos ms adelante. Tambin es
importante apreciar que lo que aqu se ha denotado genricamente por Error
63
Anlisis sintctico
!Error en expresin
get_token ( );
};
get_token( );
} while (token != EOF);
};
En este caso se considera al ; (PUNTOYCOMA) como un token de
seguridad, lo que permite hacer una recuperacin de errores mediante el mtodo panic
mode (ver punto 3.3.1).
}
}
64
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
trmino ( );
while ((token == MAS) || (token == MENOS) || (token == OR)) {
get_token( );
trmino ( );
}
}
65
Anlisis sintctico
66
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
regla general, estos diagramas pueden reconstruirse de manera que se adapten bien a
nuestro mecanismo de construccin de funciones, tal y como ilustra la figura 3.18.b.
El metacompilador JavaCC utiliza la notacin BNF para expresar una
gramtica, y construye una funcin recursiva por cada no terminal de la gramtica
incluyendo en ella toda la lgica interna de reconocimiento. En los casos de conflicto,
como el ilustrado anteriormente, el desarrollador del compilador puede darle a JavaCC
una indicacin para que utilice un mayor nmero de tokens de pre-bsqueda.
67
Anlisis sintctico
68
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
rechazar o reconocer cualesquiera sentencias, esto es, la tabla no se construye para cada
sentencia a reconocer, sino que est asociada exclusivamente a la gramtica y viene a
ser una representacin de su autmata finito con pila.
La secuencia siguiente muestra la ejecucin de este algoritmo para reconocer
o rechazar la sentencia id * ( id + id) $. La columna Accin indica qu punto del
algoritmo es el que se aplica, mientras que la columna Cadena de Entrada indica, a
la izquierda, qu tokens se han consumido, a la derecha lo que quedan por consumir y,
subrayado, el token de pre-bsqueda actual. Puede observarse cmo la ejecucin de
este mtodo es mucho ms eficiente que el caso con retroceso, e igual de eficiente que
el anlisis con funciones recursivas.
Pila de smbolos Cadena de Entrada Accin
$ E id * ( id + id ) $ 4.- M[E, id] = E T E
$ E T id * ( id + id ) $ 4.- M[T, id] = T F T
$ E T F id * ( id + id ) $ 4.- M[F, id] = F id
$ E T id id * ( id + id ) $ 2.- id = id
$ E T id * ( id + id ) $ 4.- M[T, *] = T * F T
$ E T F * id * ( id + id ) $ 2.- *=*
$ E T F id * ( id + id ) $ 4.- M[F, (] = F ( E )
$ E T ) E ( id * ( id + id ) $ 2.- (=(
$ E T ) E id * ( id + id ) $ 4.- M[E, id] = E T E
$ E T ) E T id * ( id + id ) $ 4.- M[T, id] = T F T
$ E T ) E T F id * ( id + id ) $ 4.- M[F, id] = F id
$ E T ) E T id id * ( id + id ) $ 2.- id = id
$ E T ) E T id * ( id + id ) $ 4.- M[T,+] = T g
$ E T ) E id * ( id + id ) $ 4.- M[E,+] = E + T E
$ E T ) E T + id * ( id + id ) $ 2.- +=+
$ E T ) E T id * ( id + id ) $ 4.- M[T, id] = T F T
$ E T ) E T F id * ( id + id ) $ 4.- M[F, id] = F id
$ E T ) E T id id * ( id + id ) $ 2.- id = id
$ E T ) E T id * ( id + id ) $ 4.- M[T, )] = T g
$ E T ) E id * ( id + id ) $ 4.- M[E, )] = E g
$ E T ) id * ( id + id ) $ 2.- )=)
$ E T id * ( id + id ) $ 4.- M[T, $] = T g
$ E id * ( id + id ) $ 4.- M[E, $] = E g
$ id * ( id + id ) $ 1.- $ = $ ACEPTAR
La secuencia de reglas aplicadas (secuencia de pasos de tipo 4) proporciona
la secuencia de derivaciones a izquierda que representa el rbol sintctico que reconoce
la sentencia.
Este mtodo tiene una ventaja sobre el anlisis con funciones recursivas, y es
que la construccin de la tabla de chequeo de sintaxis puede automatizarse, por lo que
una modificacin en la gramtica de entrada no supone un problema grande en lo que
a reconstruccin de la tabla se refiere. No sucede lo mismo con las funciones
69
Anlisis sintctico
recursivas, ya que estas tienen que ser reconstruidas a mano con la consecuente prdida
de tiempo y propensin a errores.
70
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Como puede verse, representa al rbol sintctico visto desde arriba conforme
se va construyendo ascendentemente: es una forma sentencial ascendente.
71
Anlisis sintctico
72
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Pila de reglas
Accin
utilizadas
-- g id * id Desplazar
-- id * id Reducir por F 6 id
5 F * id Reducir por T 6 F
5-4 T * id Reducir por E 6 T
5-4-2 E * id Desplazar
5-4-2 E* id Desplazar
5-4-2 E * id g Reducir por F 6 id
5-4-2-5 E*F g Reducir por T 6 F
5-4-2-5-4 E*T g Reducir por E 6 T
5-4-2-5-4-2 E*E g Retroceso (pues no hay nada por desplazar)
5-4-2-5-4 E*T g Retroceso (pues no hay nada por desplazar)
5-4-2-5 E*F g Retroceso (pues no hay nada por desplazar)
5-4-2 E * id g Retroceso (pues no hay nada por desplazar)
5-4-2 E* id Retroceso (pues ya se desplaz)
5-4-2 E * id Retroceso (pues ya se desplaz)
5-4 T * id Desplazar
5-4 T* id Desplazar
5-4 T * id g Reducir por F 6 id
5-4-5 T*F g Reducir por T 6 T * F
5-4-5-3 T g Reducir por E 6 T
5-4-5-3-2 E g Aceptar
Este mtodo es ms eficiente que el descendente con retroceso puesto que
consume los tokens a mayor velocidad y, por tanto, trabaja con mayor cantidad de
informacin a la hora de tomar cada decisin. No obstante resulta inviable para
aplicaciones prcticas pues su ineficiencia sigue siendo inadmisible.
73
Anlisis sintctico
74
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
s 0 X 1 s 1 X 2 s 2 ... X m s m
donde el smbolo s m se encuentra en la cima tal y como se muestra en la figura 3.25.
Cada uno de los X i es un smbolo de la gramtica (X i 0 (N c T) *) y los s i representan
distintos estados del autmata asociado a la gramtica; al conjunto de estados lo
denominaremos E: si 0 E. Cada estado s i tiene asociado un smbolo X i, excepto el s 0
que representa al estado inicial y que no tiene asociado smbolo ninguno. Los estados
se utilizan para representar toda la informacin contenida en la pila y situada antes del
propio estado. Consultando el estado en cabeza de la pila y el siguiente token a la
entrada se decide qu reduccin ha de efectuarse o bien si hay que desplazar.
Por regla general una tabla de anlisis para un reconocedor LR consta de dos
partes claramente diferenciadas entre s que representan dos tareas distintas, la tarea de
transitar a otro estado (GOTO) y la tarea de qu accin realizar (ACCION). La tabla
GOTO es de la forma E (N c T c {$}) y contiene estado de E, mientras que la tabla
75
Anlisis sintctico
76
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
E E+T
| T
T T*F
| F
Cuadro 3.4 Gramtica no ambigua que
F (E)
reconoce expresiones aritmticas en las que
| id
slo intervienen identificadores
77
Anlisis sintctico
78
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
79
Anlisis sintctico
3.5.6.1.2 Conflictos
Un conflicto se produce cuando el analizador no es capaz de generar la tabla
de chequeo de sintaxis para una gramtica. Bsicamente pueden darse dos tipos de
conflictos:
Desplazar/Reducir (Shift/Reduce). Suelen deberse a gramticas ambiguas.
Reducir/Reducir (Reduce/Reduce). Debidos a que la gramtica no admite un
anlisis LR(k).
El conflicto Desplazar/Reducir aparece cuando en la tabla de acciones aparece
en la misma casilla tanto una R de reducir como una D de desplazar, el conflicto es que
el programa no sabe si reducir o desplazar. Por ejemplo, una gramtica tan sencilla
como:
EXPR EXPR + EXPR
80
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
| num
resulta obviamente ambigua (es recursiva por la derecha y por la izquierda
simultneamente), luego ante una entrada tan sencilla como num + num + num
pueden producirse dos rboles sintcticos diferentes (ver figura 3.27).
Por otro lado, el conflicto Reducir/Reducir aparece cuando el analizador puede
aplicar dos o ms reglas ante determinadas situaciones. Esta situacin suele aparecer
cuando la gramtica no es LR(1), esto es, no es suficiente con un token de pre-
bsqueda para tomar la decisin de qu accin realizar. Por ejemplo, supongamos la
gramtica:
S aaBdd
| aCd
Ba
C aa
que nicamente reconoce las dos secuencia de tokens aaadd y aaad. Pues bien, una
gramtica tan sencilla no es LR(1) (ver figura 3.28). Supongamos que en esta figura se
desea reconocer la secuencia aaad; en tal caso lo correcto sera reducir por . Sin
embargo todo depende de si detrs del token de pre-bsqueda (una d), viene otra d
o lleva EOF. Un analizador LR(2) sera capaz de tomar la decisin correcta, pero no
uno LR(1).
81
Anlisis sintctico
82
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Captulo 4
Gramticas atribuidas
83
Gramticas atribuidas
84
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
85
Gramticas atribuidas
E1 E2 + T {E 1 = E 2 + T; }
De esta forma, la construccin del rbol sintctico tendra una reduccin ms, y cuando
sta se hiciera aparecera el resultado por pantalla. Ntese como la adicin de esta regla
no modifica la gramtica en el sentido de que sigue reconociendo el mismo lenguaje
pero, sin embargo, nos proporciona una clara utilidad al permitir visualizar nicamente
el resultado final del clculo.
86
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
87
Gramticas atribuidas
88
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
semnticas puede generar cdigo, guardar informacin en una tabla de smbolos, emitir
mensajes de error o realizar otras actividades, como p.ej. evaluar expresiones
aritmticas en una calculadora. La traduccin de la cadena de componentes lxicos es
el resultado obtenido al evaluar las reglas semnticas.
En la prctica, no todas las implementaciones tienen que seguir al pie de la
letra este esquema tan ntido de una fase detrs de otra. Hay casos especiales de
definiciones dirigidas por la sintaxis que se pueden implementar en una sola pasada
evaluando las reglas semnticas durante el anlisis sintctico, sin construir
explcitamente un rbol de anlisis sintctico o un grafo que muestre las dependencias
entre los atributos.
90
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
anlisis sintctico LALR(1). Tambin veremos un ejemplo con JavaCC pero, por ahora,
seguiremos profundizando en las definiciones dirigidas por sintaxis.
En una definicin dirigida por sintaxis, se asume que los tokens slo tienen
atributos sintetizados, ya que la definicin no proporciona ninguna regla semntica para
ellos. El analizador lxico es el que proporciona generalmente los valores de los
atributos de los terminales. Adems se asume que el smbolo inicial no tiene ningn
atributo heredado, a menos que se indique lo contrario, ya que en muchos casos suele
carecer de padre y de hermanos.
Figura 4.5. rbol de anlisis sintctico con anotaciones para reconocer 3*5+4\n.
Aunque una definicin dirigida por sintaxis no dice nada acerca de cmo ir
91
Gramticas atribuidas
calculando los valores de los atributos, est claro que debe existir una secuencia viable
de clculos que permita realizar dichos clculos. Vamos a ver una posible secuencia.
Considrese el nodo situado en el extremo inferior izquierdo que corresponde al uso
de la produccin F num. La regla semntica correspondiente, F.val:= num.val_lex,
establece que el atributo F.val en el nodo tenga el valor 3 puesto que el valor de
num.val_lex en el hijo de este nodo es 3. De forma similar, en el padre de este F, el
atributo T.val tiene el valor 3.
A continuacin considrese el nodo raz de la produccin T T * F. El valor
del atributo T.val en este nodo est definido por T.val := T 1.val * F.val. Cuando se
aplica la regla semntica en este nodo, T1 .val tiene el valor 3 y F.val el valor 5. Por
tanto, T.val adquiere el valor 15 en este nodo.
Por ltimo, la regla asociada con la produccin para el no terminal inicial, L
E \n, visualiza el valor de la expresin representada por E, esto es E.val.
Cuadro 4.2 Definicin dirigida por sintaxis con el atributo heredado L.her
92
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
de la declaracin. Entonces las reglas pasan hacia abajo este valor por el rbol
sintctico utilizando el atributo heredado L.her. Las reglas asociadas con las
producciones de L llaman al procedimiento aadetipo, que suponemos que aade el
tipo de cada identificador a su entrada en la tabla de smbolos (apuntada por el atributo
ptr_tds). Este ltimo detalle carece de importancia para el asunto que estamos tratando
en este momento y se ver con mayor detenimiento en captulos posteriores.
En la figura 4.6 se muestra un rbol de anlisis sintctico con anotaciones
para la cadena real id 1, id 2, id 3. El valor de L.her en los tres nodos de L proporciona
el tipo a los identificadores id 1, id 2 e id 3. Estos valores se obtienen calculando el valor
del atributo T.tipo en el hijo izquierdo de la raz y evaluando despus L.her de forma
descendente en los tres nodos de L en el subrbol derecho de la figura. En cada nodo
de L tambin se llama al procedimiento aadetipo para indicar en la tabla de smbolos
el hecho de que el identificador en el hijo de cada nodo L tiene tipo real.
Figura 4.6. rbol sintctico con el atributo heredado L.her en cada nodo L
93
Gramticas atribuidas
Los tres nodos del grafo de dependencias (marcados con un crculo de color)
representan los atributos sintetizados E3 .val, E1 .val y E2 .val en los nodos
correspondientes del rbol sintctico. El arco desde E1.val hacia E3.val muestra que
E3.val depende de E1.val y el arco hacia E3.val desde E2.val muestra que E3.val tambin
depende de E2.val. Las lneas de puntos representan al rbol de anlisis sintctico y no
son parte del grafo de dependencias.
En la figura 4.8 se muestra el grafo de dependencias para el rbol de anlisis
sintctico de la figura 4.6. Los arcos de este grafo estn numerados. Hay una arco() hacia
el nodo L.her desde el nodo T.tipo porque el atributo heredado L.her depende del
atributo T.tipo segn la regla semntica L.her:=T.tipo de la regla de produccin D
94
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
T L. Los dos arcos que apuntan hacia abajo ( y ) surgen porque Li+1.her depende de
Li.her segn la regla semntica L 1.her := L 2.her de la regla de produccin L 2 L 1, ID.
Cada una de las reglas semnticas aadetipo(id.ptr_tds, L.her) asociada con las
producciones de L conduce a la creacin de un falso atributo. Los nodos amarillos se
construyen para dichos falsos atributos (arcos , y ).
Figura 4.8. Grafo de dependencias para el rbol sintctico de la figura 4.6. El rbol
sintctico aparece de azul, los atributos de los tokens de verde, los atributos ficticios de
amarillo, y el resto de atributos se muestra en rosa.
95
Gramticas atribuidas
96
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
visitaEnProfundidad(m)
Fin Para
evaluar los atributos sintetizados de n
Fin visitaEnProfundidad
A continuacin se muestra una gramtica L-atribuida donde D hace referencia
a declaracin, T a tipo y L a lista de identificadores.
DTL {L.tipo = T.tipo;}
T integer {T.tipo = integer;}
T real {T.tipo = real;}
L L 1 , id {L 1.tipo = L.tipo;
id.tipo = L.tipo;}
L id {id.tipo = L.tipo;}
Sin embargo, la siguiente gramtica no es L-atribuida ya que propaga los
atributos de derecha a izquierda:
D id T {id.tipo = T.tipo;
D.tipo = T.tipo;}
D id D 1 {id.tipo = D 1.tipo;
D.tipo = D 1.tipo;}
T integer {T.tipo = integer;}
T real {T.tipo = real;}
97
Gramticas atribuidas
num.val_lex = 3
F.val = num.val_lex
(F.val = 3)
T.val = 7
+.nada = --
E.val = 3
98
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
A cd {printf(c + d)} B
Cuando se disea un esquema de traduccin, se deben respetar algunas
limitaciones para asegurarse que el valor de un atributo est disponible cuando una
accin se refiera a l. Estas limitaciones, derivadas del tipo de anlisis sintctico
escogido para construir el rbol, garantizan que las acciones no hagan referencia a un
atributo que an no haya sido calculado.
El ejemplo ms sencillo ocurre cuando slo se necesitan atributos sintetizados.
En este caso, se puede construir el esquema de traduccin creando una accin que
conste de una asignacin para cada regla semntica y colocando esta accin al final del
lado derecho de la regla de produccin asociada. Por ejemplo, la regla de produccin
T T 1 * F y la accin semntica T.val := T 1.val * F.val dan como resultado la
siguiente produccin y accin semntica:
T T 1 * F {T.val := T 1.val * F.val}
Si se tienen atributos tanto heredados como sintetizados, se debe ser ms
prudente, de manera que deben cumplirse algunas reglas a la hora de utilizar los
atributos de los smbolos gramaticales:
1.- Un atributo heredado para un smbolo en el lado derecho de una regla de
produccin se debe calcular en una accin antes que dicho smbolo.
2.- Una accin no debe referirse a un atributo sintetizado de un smbolo que
est a la derecha de la accin.
3.- Un atributo sintetizado para el no terminal de la izquierda slo se puede
calcular despus de que se hayan calculado todos los atributos a los que hace
referencia. La accin que calcula dichos atributos se debe colocar,
generalmente, al final del lado derecho de la produccin.
Los esquemas de traduccin bien definidos que cumplen estas restricciones
se dice que estn bien definidos. Ejemplo:
D T {L.her = T.tipo} L ;
T int {T.tipo = integer}
T real { T.tipo = real}
L {L 1.her = L.her} L 1 , id {aadetipo (id.ptr_tbs, L.her)}
L id {aadetipo(id.ptr_tbs, L.her)}
99
Gramticas atribuidas
posible ejecutar acciones a la vez que se hace el anlisis, sino que habra que reconocer
primero la sentencia, y en una fase posterior ejecutar las acciones con seguridad.
100
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
101
Gramticas atribuidas
| id = EXPR
SIncompleta if COND then S
| if COND then SCompleta else SIncompleta
que asocian siempre el else al if ms inmediato. De forma que una SCompleta es o una
proposicin if-then-else que no contenga proposiciones incompletas o cualquier otra
clase de proposicin no condicional (como por ejemplo una asignacin). Es decir,
dentro de un if con else solo se permiten if completos, lo que hace que todos los else
se agrupen antes de empezar a reconocer los if sin else.
102
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Este rea est compuesta por dos secciones opcionales. La primera de ellas
est delimitada por los smbolos % { y % } y permite codificar declaraciones ordinarias
en C. Todo lo que aqu se declare ser visible en las reas de reglas y de funciones, por
lo que suele usarse para crear la tabla de smbolos, o para incluirla (#include). Los
smbolos % { y % } deben comenzar obligatoriamente en la columna 0 (al igual que
todas las clusulas que se vern a continuacin)
La segunda seccin permite declarar los componentes lxicos (tokens) usados
en la gramtica. Para ello se utiliza la clusula % token seguida de una lista de
terminales (sin separarlos por comas); siguiendo el ejemplo del punto 4.3.1 habra que
declarar:
%token ID NUM
Adems, a efectos de aumentar la legibilidad, pueden utilizarse tantas clusulas
% token como sean necesarias; por convencin, los tokens se escriben en maysculas.
Internamente, PCYacc codifica cada token como un nmero entero,
empezando desde el 257 (ya que del 0 al 255 se utilizan para representar los caracteres
ASCII, y el 256 representa al token de error). Por lo tanto la clusula
%token ID NUM
es equivalente a
# define ID n 1
# define NUM n 2
Evidentemente, los nmeros o cdigos asignados por PCYacc a cada token son siempre
distintos.
No obstante lo anterior, no todos los terminales han de enumerarse en
clusulas % token. En concreto, los terminales se pueden expresar de dos formas:
Si su lexema asociado est formado por un solo carcter, y el analizador
lxico lo devuelve tal cual (su cdigo ASCII), entonces se pueden indicar
directamente en una regla de produccin sin declararlo en una clusula
% token. Ejemplo: '+'.
Cualquier otro terminal debe declararse en la parte de declaraciones, de forma
que las reglas harn referencia a ellos en base a su nombre (no al cdigo, que
tan slo es una representacin interna).
Finalmente, en este rea tambin puede declararse el axioma inicial de la
gramtica de la forma:
%start noTerminal
Si esta clusula se omite, PCYacc considera como axioma inicial el antecedente de la
primera regla gramatical que aparece en el rea de reglas.
103
Gramticas atribuidas
resto de los terminales, al estar formados por un solo carcter, pueden ser manipulados
a travs de su cdigo ASCII. De esta manera, asumiendo que los espacios en blanco 2,
tabuladores y retornos de carro actan simplemente como separadores, disponemos de
dos opciones principales:
%% /* Opcin 1 */
[0 - 9]+ { return NUM; }
[A- Z][A - Z0 -9]* { return ID; }
& \t\n]
[b {;}
. { return yytext[0]; } /* Error sintctico. Le pasa el error al a.si. */
y
%% /* Opcin 2 */
[0 - 9]+ { return NUM; }
[A- Z][A - Z0 -9]* { return ID; }
& \t\n]
[b {;}
(|)|*|+ { return yytext[0]; } /* El a.si. slo recibe tokens vlidos */
. { printf (Error lxico.\n); }
Estas opciones se comportan de manera diferente ante los errores lxicos,
como por ejemplo cuando el usuario teclea una @ donde se esperaba un + o
cualquier otro operador. En la Opcin 1, el analizador sintctico recibe el cdigo ASCII
de la @ como token, debido al patrn .; en este caso el analizador se encuentra, por
tanto, ante un token inesperado, lo que le lleva a abortar el anlisis emitiendo un
mensaje de error sintctico. En la Opcin 2 el analizador lxico protege al sintctico
encargndose l mismo de emitir los mensajes de error ante los lexemas invlidos; en
este caso los mensajes de error se consideran lexicogrficos; adems, el desarrollador
puede decidir si continuar o no el reconocimiento haciendo uso de la funcin halt() de
C.
En los ejemplos que siguen nos decantamos por la Opcin 1, ya que es el
mtodo del mnimo esfuerzo y concentra la gestin de errores en el analizador
sintctico.
Por ltimo, ntese que el programa Lex no incluye la funcin main(), ya que
PCYacc proporciona un modelo dirigido por sintaxis y es al analizador sintctico al que
se debe ceder el control principal, y no al lxico. Por tanto, ser el rea de funciones
del programa Yacc quien incorpore en el main() una llamada al analizador sintctico,
a travs de la funcin yyparse():
%token ID NUM
%%
...
%%
void main ( ){
yyparse( );
2
El espacio en blanco se representar mediante el carcter b&.
104
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
}
4.3.1.3 rea de reglas
ste es el rea ms importante ya que en l se especifican las reglas de
produccin que forman la gramtica cuyo lenguaje queremos reconocer. Cada regla
gramatical tienen la forma:
noTerminal: consecuente ;
donde consecuente representa una secuencia de cero o ms terminales y no
terminales.
El conjunto de reglas de produccin de nuestro ejemplo se escribira en Yacc
tal y como se ilustra en el cuadro 4.5 donde, por convencin, los smbolos no
terminales se escriben en minscula y los terminales en mayscula, las flechas se
sustituyen por : y, adems, cada regla debe acabar en ;
105
Gramticas atribuidas
gramaticales:
e : e + t { printf(Esto es una suma.\n); } ;
e : t { printf(Esto es un trmino.\n); } ;
se pueden expresar en Yacc como:
e : e + t { printf(Esto es una suma.\n); }
| t { printf(Esto es un trmino.\n); }
;
106
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
$$ en funcin de los $i. Por ejemplo, si suponemos que el atributo de todos los
smbolos de nuestra gramtica (terminales y no terminales) es de tipo entero, se tendra:
%%
e : e + t {$$ = $1 + $3;}
| t {$$ = $1;}
;
t : t * f { $$ = $1 * $3; }
| f { $$ = $1; }
;
f : m ( e ) { $$ = $1 * $3; }
| m ID { $$ = $1 * $2; }
| m NUM { $$ = $1 * $2; }
;
m : /* psilon */ { $$ = +1; }
| - { $$ = -1; }
;
Por otro lado, en las reglas y del ejemplo anterior puede apreciarse que
$2 se refiere a los atributos asociados a ID y NUM respectivamente; pero, al ser ID y
NUM terminales, qu valores tienen sus atributos?quin decide qu valores guardar
en ellos?. La respuesta es que el analizador lxico es quien se tiene que encargar de
asignar un valor a los atributos de los terminales. Recordemos que en el apartado
2.4.3.6 se estudi que PCLex proporcionaba la variable global yylval que permita
asociar informacin adicional a un token. Pues bien, mediante esta variable el
analizador lxico puede comunicar atributos al sintctico. De esta manera, cuando el
analizador lxico recibe una cadena como 27+13+25, no slo debe generar la
secuencia de tokens NUM + NUM + NUM, sino que tambin debe cargar el valor
del atributo de cada NUM, y debe de hacerlo antes de retornar el token (antes de hacer
return NUM ;). En nuestro caso asociaremos a cada NUM el valor entero que
representa su lexema, con el objetivo de que las acciones semnticas del sintctico
puedan operar con l. El patrn Lex y la accin lxica asociada son:
[0-9]+ { yylval = atoi(yytext); return NUM; }
La funcin atoi() convierte de texto a entero. Ntese que siempre que se
invoque a atoi(), la conversin ser correcta, ya que el lexema almacenado en yytext
no puede contener ms que dgitos merced al patrn asociado. Adems, se asume que
la variable yylval tambin es entera.
PCYacc necesita alguna informacin adicional para poder realizar una
correcta gestin de atributos, es decir, hemos de informarle del tipo de atributo que
tiene asociado cada smbolo de la gramtica, ya sea terminal o no terminal. Para ello
debe definirse la macro YYSTYPE que indica de qu tipo son todos los atributos; en
otras palabras, PCYacc asume que todos los atributos son de tipo YYSTYPE, por lo
que hay que darle un valor a esta macro. De esta forma, si queremos que todos los
atributos sean de tipo int, bastara con escribir
%{
#define YYSTYPE int
107
Gramticas atribuidas
%}
como seccin de declaraciones globales en el rea de definiciones. De hecho, PCYacc
obliga a incluir en los programas Yacc una declaracin como sta incluso aunque no
se haga uso de los atributos para nada: PCYacc exige asociar atributos siempre y
conocer su tipo.
No obstante, resulta evidente que no todos los smbolos de una gramtica
necesitarn atributos del mismo tipo, siendo muy comn que los nmeros tengan
asociado un atributo entero, y que los identificadores posean un puntero a una tabla de
smbolos que almacene informacin importante sobre l. En este caso, puede declararse
YYSTYPE como una union de C de manera que, aunque todos los smbolos de la
gramtica poseern como atributo dicha union, cada uno utilizar slo el campo que
precise. Aunque este mtodo puede llegar a derrochar un poco de memoria, permite una
gestin uniforme de la pila de anlisis sintctico, lo que conlleva una mayor eficiencia.
Es ms, el mecanismo de usar una union como tipo de YYSTYPE es una prctica tan
comn que PCYacc posee una clusula que permite definir YYSTYPE y declarar la
union en un solo paso. La clusula tiene la forma:
%union {
/* Cuerpo de una union en C */
}
Recordemos que una union en C permite definir registros con parte variante.
Es ms, en una union todo es parte variante, o sea, todas los campos declarados en el
cuerpo de una union comparten el mismo espacio de memoria, de manera que el
tamao de la union es el del campo ms grande que contiene. Siguiendo con nuestro
ejemplo, la unin tendra la forma:
%union {
int numero;
nodo * ptrNodo;
}
Con esto, YYSTYPE se ha convertido en una union (ya no es necesario
incluir el #define YYSTYPE), de manera que cualquier smbolo puede hacer
referencia al campo numero o al campo ptrNodo, pero no a ambos. Por regla general,
cada smbolo gramatical, independientemente de la regla en que aparezca utiliza
siempre el mismo campo del % union; por ello, PCYacc suministra un mecanismo para
indicar cul de los campos es el que se pretende utilizar para cada smbolo. Con esto
adems, nos ahorramos continuas referencias a los campos del %union. El mecanismo
consiste en incluir entre parntesis angulares en la clusula %token del rea de
definiciones el campo que se quiere utilizar para cada terminal de la gramtica; para
indicar el tipo de los atributos de los no terminales, se utilizar la clusula % type que,
por lo dems, es igual a la % token excepto porque en ella se enumeran no terminales
en lugar de terminales. Siguiendo esta notacin, nuestro ejemplo quedara:
%union {
int numero;
108
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
nodo * ptrNodo;
}
%token <numero> NUM
%token <ptrNodo> ID
%type <numero> e t f m
%%
e : e + t {$$ = $1 + $3;}
| t {$$ = $1;}
;
t : t * f { $$ = $1 * $3; }
| f { $$ = $1; }
;
f : m ( e ) { $$ = $1 * $3; }
| m ID { $$ = /*Extraer alguna informacin de (*$2) */; }
| m NUM { $$ = $1 * $2; }
;
m : /* psilon */ { $$ = +1; }
| - { $$ = -1; }
;
Ntese cmo, a pesar de que YYSTYPE es de tipo union, los $$ y $i no
hacen referencia a sus campos, gracias a que las declaraciones % token y % type hacen
estas referencias por nosotros. Es ms, si en algn punto utilizamos el atributo de algn
smbolo y no hemos especificado su tipo en una clusula % token o % type, PCYacc
emitir un mensaje de error durante la metacompilacin. Estas transformaciones son
realizadas por PCYacc por lo que las acciones lxicas, gestionadas por PCLex, cuando
hacen referencia a yylval deben indicar explcitamente el campo con que trabajar. El
programa Lex que implementa el analizador lxico de nuestra gramtica sera:
%% /* Opcin 1 */
[0 - 9]+ { yylval.numero = atoi(yytext); return NUM; }
[A- Z][A - Z0 -9]* { yylval.ptrNodo = ...; return ID;}
& \t\n]
[b {;}
. { return yytext[0]; }
La gestin del atributo asociado a los identificadores de usuario la dejaremos
109
Gramticas atribuidas
para el captulo dedicado a la tabla de smbolos. Por ahora nos centraremos slo en las
constantes numricas. La figura ? muestra el rbol generado para reconocer la cadena
27+13+25; los valores de los atributos aparecen como subndices.
4.3.3 Ambigedad
PCYacc no slo permite expresar de forma fcil una gramtica a reconocer y
generar su analizador sintctico, sino que tambin da herramientas que facilitan la
eliminacin de ambigedades.
La gramtica propuesta en el apartado 4.3.1 es un artificio para obligar a que
el producto y la divisin tenga ms prioridad que la suma y la resta, y que el menos
unario sea el operador de mayor prioridad. Sin embargo, desde el punto de vista
exclusivamente sintctico, el lenguaje generado por dicha gramtica resulta mucho ms
fcil de expresar mediante la gramtica
e : e + e
| e - e
| e * e
| e / e
| - e
| ( e )
| ID
| NUM
;
que, como resulta evidente, es ambigua. Por ejemplo, ante la entrada 27*13+25 se
pueden obtener los rboles sintcticos de la figura 4.13; en el caso a) primero se
multiplica y luego se suma, mientras que en el b) primero se suma y luego se
multiplica. Dado que el producto tiene prioridad sobre la suma, slo el caso a) es vlido
desde el punto de vista semntico, pues de otro modo se obtiene un resultado
110
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
incorrecto.
Figura 4.13. rboles sintcticos que reconocen una sentencia vlida segn una
gramtica ambigua. Ntese cmo semnticamente se producen resultados
diferentes segn el orden de evaluacin. La opcin correcta es la a), ya que el
producto tiene prioridad sobre la suma.
111
Gramticas atribuidas
112
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
obstante, esta solucin presenta dos problemas: 1) la prioridad del menos unario no es
igual a la del producto, sino superior; y 2) el menos unario no es asociativo por la
izquierda, sino por la derecha (aunque esto no dara problemas, ya que no existe
ambigedad en la formacin del rbol asociado a una sentencia como ---5, tal y como
ilustra la figura 4.14).
113
Gramticas atribuidas
Esta ruptura de la ejecucin puede evitarse mediante el correcto uso del token
especial error. Bsicamente, si se produce un error sintctico el analizador generado
por PCYacc desecha parte de la pila de anlisis y parte de la entrada que an le queda
por leer, pero slo hasta llegar a una situacin donde el error se pueda recuperar, lo que
se consigue a travs del token reservado error. Un ejemplo tpico del uso del token
error es el siguiente:
prog : /* psilon */
| prog sent ;
| prog error ;
;
sent : ID = NUM
;
La ltima regla de este trozo de gramtica sirve para indicarle a PCYacc que
entre el no terminal prog y el terminal ; puede aparecer un error sintctico.
Supongamos que tenemos la sentencia a reconocer a = 6; b = 7; c.= 8; d = 6; en la
que se ha colado un punto tras el identificador c; cuando el analizador se encuentre
el token . se da cuenta de que hay un error sintctico y busca en la gramtica todas las
reglas que contienen el smbolo error; a continuacin va eliminando smbolos de la
cima de la pila, hasta que los smbolos ubicados encima de la pila coincidan con los
situados a la izquierda del smbolo error en alguna de las reglas en que ste aparece.
A continuacin va eliminando tokens de la entrada hasta encontrarse con uno que
114
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
coincida con el que hay justamente a la derecha en la regla de error seleccionada. Una
vez hecho esto, inserta el token error en la pila, desplaza y contina con el anlisis
intentando reducir por esa regla de error. Al terminal que hay justo a la derecha de
error en una regla de error se le denomina token de seguridad, ya que permite al
analizador llegar a una condicin segura en el anlisis. Como token de seguridad suele
escogerse el de terminacin de sentencia o de declaracin, como en nuestro caso, en
el que hemos escogido el punto y coma. La figura 4.15 ilustra grficamente el proceso
seguido para la recuperacin del error.
Por ltimo, para que todo funcione de forma adecuada es conveniente
asociarle a la regla de error la invocacin de una macro especial de PCYacc
denominada yyerrok:
prog : /* psilon */
| prog sent ;
| prog error ; { yyerrok; }
;
sent : ID = NUM
;
La ejecucin de esta macro informa al analizador sintctico de que la
recuperacin se ha realizado satisfactoriamente. Si no se invoca, el analizador generado
por PCYacc entra en un estado llamadocondicin de seguridad, y permanecer en ese
estado hasta haber desplazado tres tokens. La condicin de seguridad previene al
analizador de emitir ms mensajes de error (ante nuevos errores sintcticos), y tiene por
objetivo evitar errores en cascada debidos a una incorrecta recuperacin del error. La
invocacin a yyerrok saca al analizador de la condicin de seguridad, de manera que
ste nos informar de todos, absolutamente todos, los errores sintcticos, incluso los
debidos a una incorrecta recuperacin de errores.
A pesar de poder recuperar los errores sintcticos, PCYacc no genera
analizadores que informen correctamente del error, sino que solamente se limitan a
informar de la existencia de stos pero sin especificar su naturaleza. Esta falta se puede
subsanar siguiendo los siguientes pasos:
1. En el rea de funciones de Yacc, colocar la directiva:
#include errorlib.c
y no incluir la funcin yyerror(char * s), que ya est codificada dentro
del fichero errorlib.c.
2. Compilar el programa YACC como:
pcyacc -d -pyaccpar.c fichero.yac
-d crea el fichero yytab.h que tiene una descripcin de la pila del
analizador y el cdigo asociado a cada token.
-pyaccpar.c hace que se utilice un esqueleto para contener al
autmata finito con pila que implementa PCYacc. En lugar de tomar
el esqueleto por defecto, se toma a yaccpar.c que contiene las
115
Gramticas atribuidas
116
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
117
Gramticas atribuidas
118
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
119
Gramticas atribuidas
/* Gramtica */
lista_expr ::= lista_expr expr:e PUNTOYCOMA {: System.out.println("= " + e); :}
| lista_expr error PUNTOYCOMA
| /* Epsilon */
;
expr ::= expr:e1 MAS expr:e2
{: RESULT = new Integer(e1.intValue() + e2.intValue()); :}
| expr:e1 MENOS expr:e2
{: RESULT = new Integer(e1.intValue() - e2.intValue()); :}
| expr:e1 POR expr:e2
{: RESULT = new Integer(e1.intValue() * e2.intValue()); :}
| expr:e1 ENTRE expr:e2
{: RESULT = new Integer(e1.intValue() / e2.intValue()); :}
| expr:e1 MODULO expr:e2
{: RESULT = new Integer(e1.intValue() % e2.intValue()); :}
| NUMERO:n
{: RESULT = n; :}
| MENOS expr:e %prec UMENOS
{: RESULT = new Integer(0 - e.intValue()); :}
| LPAREN expr:e RPAREN
{: RESULT = e; :}
;
Como se ha dejado entrever anteriormente, para que todo esto funcione es
necesario interconectarlo con un analizador lexicogrfico que, ante cada terminal
devuelva un objeto de tipo java_cup.runtime.Symbol. Los objetos de esta clase
poseen un campo value de tipo Object que representa a su atributo, y al que el
analizador lexicogrfico debe asignarle un objeto coherente con la clase especificada
en la declaracin de terminales hecha en Cup.
120
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
esttico ya que no existen objetos accesibles mediante los que referenciar componentes
no estticos. Todo lo que aqu se declare ser accesible a las acciones semnticas.
Tambin es posible realizar declaraciones Java dentro de la propia clase
parser, mediante la declaracin:
parser code {: bloque_java :}
lo cual es muy til para incluir el mtodo main() que arranque nuestra aplicacin.
Las siguientes dos declaraciones sirven para realizar la comunicacin con el
analizador lxicogrfico. La primera es:
init with {: bloque_java :}
y ejecuta el cdigo indicado justo antes de realizar la solicitud del primer token; el
objetivo puede ser abrir un fichero, inicializar estructuras de almacenamiento, etc.
Por ltimo, Cup puede comunicarse con cualquier analizador lxico, ya que
la declaracin:
scan with {: bloque_java :}
permite especificar el bloque de cdigo que devuelve el siguiente token a la entrada.
121
Gramticas atribuidas
4.4.2.5 Gramtica.
El ltimo apartado de un programa Cup es la gramtica a reconocer, expresada
mediante reglas de produccin que deben acabar en punto y coma, y donde el smbolo
::= hace las veces de flecha.
La gramtica puede comenzar con una declaracin que diga cul es el axioma
inicial. Caso de omitirse esta clusula se asume que el axioma inicial es el antecedente
de la primera regla. Dicha declaracin se hace de la forma:
start with noTerminal;
Al igual que en Yacc, es posible cambiar la prioridad y precedencia de una
regla que produzca conflicto desplazar/reducir indicando la clusula:
% prec terminal
junto a ella.
Las reglas de produccin permiten albergar en su interior acciones semnticas,
que son delimitadas por los smbolos {: y :}. En estas acciones puede colocarse
cualquier bloque de cdigo Java, siendo de especial importancia los accesos a los
atributos de los smbolos de la regla. El atributo del antecedente viene dado por la
variable Java RESULT, mientras que los atributos de los smbolos del consecuente son
accesibles mediante los nombres que el usuario haya especificado para cada uno de
122
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
ellos. Este nombre se indica a la derecha del smbolo en cuestin y separado de ste por
dos puntos. Su utilizacin dentro de la accin semntica debe ser coherente con el tipo
asignado al smbolo cuando se lo indic en la lista de smbolos. Debe prestarse especial
atencin cuando se utilicen acciones semnticas intermedias ya que, en tal caso, los
atributos de los smbolos accesibles del consecuente slo podrn usarse en modo
lectura, lo que suele obligar a darles un valor inicial desde Jflex. Un ejemplo de esta
situacin podr estudiarse en el apartado 8.4.6.
Por otro lado, y al igual que ocurra con PCYacc, el smbolo especial error
permite la recuperacin de errores mediante el mtodo panic mode. En el apartado 7.4.4
se ilustra con ms detalle la adecuada gestin de errores en Cup.
123
Gramticas atribuidas
Por otro lado, para que Cup se comunique convenientemente con el analizador
lxico, ste debe implementar la interface java_cup.runtime.Scanner, definida como:
public interface Scanner {
public Symbol next_token() throws java.lang.Exception;
}
Por ltimo, para que nuestra aplicacin funcione es necesario incluir una
funcin main() de Java que construya un objeto de tipo parser, le asocie un objeto de
tipo Scanner (que realizar el anlisis lexicogrfico), y arranque el proceso invocando
a la funcin parser() dentro de una sentencia try-catch. Esto se puede hacer de la
forma:
parser code {:
public static void main(String[] arg){
/* Crea un objeto parser */
parser parserObj = new parser();
/* Asigna el Scanner */
Scanner miAnalizadorLexico =
new Yylex(new InputStreamReader(System.in));
parserObj.setScanner(miAnalizadorLexico);
try{
parserObj.parse();
}catch(Exception x){
System.out.println("Error fatal.");
}
}
:};
suponiendo que Yylex es la clase que implementa al analizador lxico.
124
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
125
Gramticas atribuidas
126
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Captulo 5
JavaCC
5.1 Introduccin
JavaCC (Java Compiler Compiler - Metacompilador en Java) es el principal
metacompilador en JavaCC, tanto por sus posibilidades, como por su mbito de
difusin. Se trata de una herramienta que facilita la construccin de analizadores
lxicos y sintcticos por el mtodo de las funciones recursivas, aunque permite una
notacin relajada muy parecida a la BNF. De esta manera, los analizadores generados
utilizan la tcnica descendente a la hora de obtener el rbol sintctico.
127
JavaCC
128
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
SKIP :{
"b& "
| "\t"
| "\n"
| "\r"
}
TOKEN [IGNORE_CASE] : {
<ID: (["a"-"z"])+>
| <NUM: (["0"-"9"])+>
}
void listaExpr() : {} {
( exprBasica() ";")+
}
void exprBasica() :{}{ LOOKAHEAD(2)
<ID> "(" expr() ")"
| "(" expr() ")"
| "@NEW" <ID>
| <ID> "." <ID>
}
void expr() :{}{
"@ALGO"
| <NUM>
}
Los programas JavaCC se suelen almacenar en ficheros con extensin .jj. As,
el ejemplo anterior se almacenara en el fichero Ejemplo.jj, de manera que la
metacompilacin se hace invocando al fichero por lotes javacc.bat (quien a su vez
invoca a la clase javacc.class del fichero javacc.jar) de la forma:
javacc Ejemplo.jj
lo que producir los ficheros de salida:
Ejemplo.java: es el analizador sintctico.
EjemploTokenManager.java: es el analizador lexicogrfico.
EjemploConstants.java: interface que asocia un cdigo a cada token.
129
JavaCC
Figura 5.1. Proceso de metacompilacin con JavaCC. Partiendo de un solo fichero (Ejemplo.jj),
JavaCC produce 3 ficheros dependientes y otros 4 que son siempre idnticos.
130
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
PARSER_END(NombreClase)
rea de tokens
rea de funciones BNF
El rea de opciones permite especificar algunas directrices que ayuden a
JavaCC a generar analizadores lxico-sintcticos bien ms eficientes, bien ms
adaptados a las necesidades concretas del desarrollador. En el ejemplo se ha indicado
que, por defecto, la gramtica indicada es de tipo LL(1), excepto si, en algn punto, se
indica otra cosa.
Las clusulas PARSER_BEGIN y PARSER_END sirven para indicarle a
JavaCC el nombre de nuestra clase principal, as como para englobar tanto a sta como
a cualesquiera otras que se quieran incluir de apoyo, como pueda ser p.ej. un gestor de
tablas de smbolos. En el ejemplo puede observarse que la clase principal constituye
el analizador sintctico en s ya que la funcin main() crea un objeto de tipo Ejemplo
a la vez que le pasa como parmetro en el constructor la fuente de la que se desea
consumir la entrada: el teclado (System.in).
La clase creada por JavaCC incorporar una funcin por cada no terminal del
rea de reglas. Cada funcin se encargar de consumir la parte de la entrada que
subyace debajo de su no terminal asociado en el rbol sintctico de reconocimiento. Por
tanto, asumiendo que el axioma inicial es listaExpr, una llamada de la forma
miParser.listaExpr() consumir toda la entrada, si sta es aceptable.
Las siguientes dos reas pueden mezclarse, aunque lo ms usual suele ser
indicar primero los tokens y finalmente las reglas en BNF, especialmente por motivos
de claridad en el cdigo.
En el ejemplo se han indicado tokens de dos tipos. Los tokens agrupados bajo
la clusula SKIP son aquellos que sern consumidos sin ser pasados al analizador
sintctico; en nuestro caso son: el espacio, el tabulador, el retorno de carro (CR-Carry
Return) y la alimentacin de lnea (LF-Line Feed).
Los tokens bajo la clusula TOKEN constituyen los tokens normales, aunque
el desarrollador tambin puede indicar este tipo de tokens en las propias reglas BNF,
como ocurre con los patrones "(", ")", ";", etc. La declaracin de cada token se agrupa
entre parntesis angulares y est formada por el nombre del token seguido por el patrn
asociado y separado de ste por dos puntos. Los patrones lexicogrficos se describen
de forma parecida a PCLex. El ejemplo ilustra el reconocimiento de un identificador
formado slo por letras (ya sean maysculas o minsculas merced al modificador
[IGNORE_CASE] de la clusula TOKEN) y de un nmero entero.
La ltima rea del ejemplo ilustra la creacin de tres no terminales y sus reglas
BNF asociadas. Dado que cada no terminal va a ser convertido por JavaCC en una
funcin Java, su declaracin es idntica a la de dicha funcin y est sujeta a las mismas
restricciones que cualquier otra funcin en Java. El cuerpo de cada una de estas
funciones ser construido por JavaCC y tendr como propsito el consumo adecuado
131
JavaCC
5.2.1 Opciones.
Esta seccin comienza con la palabra reservada options seguida de una lista
de una o ms opciones entre llaves. La seccin al completo puede omitirse, ya que no
es obligatoria
Las opciones pueden especificarse tanto en el archivo de la gramtica como
en la lnea de comandos. La misma opcin no debe establecerse ms de una vez,
aunque en el caso de que se especifiquen en la lnea de comandos, stas tienen mayor
prioridad que las especificadas en el archivo.
Los nombres de las opciones se escriben en maysculas, finalizan en ";" y se
separan del valor que se les asocia mediante el signo "=". A continuacin se detallan
brevemente las opciones ms importantes y las funciones que realizan:
LOOKAHEAD: indica la cantidad de tokens que son tenidos en cuenta por el
analizador sintctico antes de tomar una decisin. El valor por defecto es 1 lo
que realizar anlisis LL(1). Cuanto ms pequeo sea el nmero de tokens de
prebsqueda, ms veloz se realizarn los anlisis. Este valor se puede
modificar en determinadas reglas concretas.
CHOICE_AM BIGUITY_CHECK: sirve para que JavaCC proporcione
mensajes de error ms precisos cuando se encuentra con varias opciones con
un prefijo de longitud igual o superior a la de LOOKAHEAD. En estos
casos, JavaCC suele informar de que se ha encontrado un error y aconseja que
se pruebe con un lookahead igual o superior a un cierto nmero n. Si
CHOICE_AMBIGUITY_CHECK vale k, entonces JavaCC intentar
averiguar el lookahead exacto que necesitamos (tan slo a efectos de
informarnos de ello) siempre y cuando ste no supere el valor k. La
utilizacin de esta opcin proporciona informacin ms precisa, pero a costa
132
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
133
JavaCC
134
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
135
JavaCC
< LITERAL_COMA_FLOTANTE:
(["0"-"9"])+ "," (["0"-"9"])* (<EXPONENTE>)? (["f","F","d","D"])?
| "." (["0"-"9"])+ (<EXPONENTE>)? (["f","F","d","D"])?
| (["0"-"9"])+ <EXPONENTE> (["f","F","d","D"])?
| (["0"-"9"])+ (<EXPONENTE>)? ["f","F","d","D"]
>
|
< EXPONENTE: ["e","E"] (["+","-"])? (["0"-"9"])+ >
}
El problema de este ejemplo radica en que el analizador lxico puede
reconocer la cadena E123 como del tipo EXPONENTE, cuando realmente
debiera ser un ID. En situaciones como sta, el token EXPONENTE puede
declararse como (ntese el uso del carcter #):
< #EXPONENT: ["e","E"] (["+","-"])? (["0"-"9"])+ >
lo que le indica a JavaCC que no se trata de un token de pleno derecho, sino
que debe utilizarlo como modelo de uso en aquellos otros patrones que lo
referencien.
*: indica repeticin 0 mas veces de lo que le precede entre parntesis. Ej.:
(ab)* encaja con g, ab, abab, etc. El patrn ab* es incorrecto puesto
que el smbolo * debe estar precedido por un patrn entre parntesis.
+: indica repeticin 1 mas veces de lo que le precede entre parntesis.
?: indica que lo que le precede entre parntesis es opcional.
|: indica opcionalidad. Ej.: (ab | cd) encaja con ab o con cd.
[]: sirve para expresar listas de caracteres. En su interior pueden aparecer:
Caracteres sueltos encerrados entre comillas y separados por comas.
Ej.: [a, b, c] que encajar con los lexemas a, b o c.
Rangos de caracteres formados por dos caracteres sueltos entre
comillas y separados por un guin. Ej.: [a-c] que encajar con
los lexemas a, b o c.
Cualquier combinacin de los anteriores. Ej.: [a-z, A-Z,
, ] que encajar con cualquier letra espaola sin signos
diacrticos (acentos o diresis).
~[]: sirve para expresar una lista de caracteres complementaria a la dada segn lo
visto anteriormente para []. P.ej., el patrn ~[] es una forma de emular al
patrn (.|\n) de PCLex.
JavaCC siempre crea automticamente el token <EOF> que encaja con el
carcter fin de fichero.
Por otro lado, no existe una rea de declaracin de estados lxicos, como en
PCLex o JFlex, sino que stos se utilizan directamente. Un sencillo ejemplo para
reconocer comentarios no anidados al estilo de Java o C sera:
136
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
SKIP : {
"/*" : DentroComentario
}
<DentroComentario> SKIP : {
"*/" : DEFAULT
}
<DentroComentario> MORE : {
<~[]>
}
5.2.2.2 Elementos accesibles en una accin lxica
El desarrollador puede asociar acciones lxicas a un patrn que sern
ejecutadas por el analizador lxico cada vez que encuentre un lexema acorde. En estas
acciones se dispone de algunas variables tiles, como son:
image: variable de tipo StringBuffer que almacena el lexema actual. Una copia
de dicho lexema se almacena tambin en el campo token de la variable
matchedToken. Si el ltimo token aceptado fue de tipo M ORE, entonces
image acumular el lexema actual a lo que ya contuviese. P.ej. si tenemos:
<DEFAULT> MORE : {
"a" { ;} : S1
}
<S1> MORE : {
"b" {
int long = image.length()-1;
image.setCharAt(long, image.charAt(long).toUpperCase());
} : S2
}
<S2> TOKEN : {
"cd" { ; } : DEFAULT
}
ante la entrada abcd, la variable image tendr los valores a, ab, aB
y aBcd en los puntos , , y del cdigo respectivamente.
lenghtOfM atch: variable de tipo entero que indica la longitud del ltimo lexema
recuperado. Siguiendo el ejemplo anterior, esta variable tomara los valores
1, 1, 1 y 2 en los puntos , , y del cdigo respectivamente. La longitud
completa de image puede calcularse a travs del mtodo length() de la clase
StringBuffer.
curLexState: variable de tipo entero que indica el estado lxico actual. Los
estados lxicos pueden ser manipulados directamente a travs del nombre que
el desarrollador les halla dado, ya que JavaCC inicializa las correspondientes
constantes Java en uno de los ficheros que genera (EjemploConstants.java
segn el ejemplo del apartado 5.1.2).
matchedToken: variable de tipo Token que almacena el token actual. Siguiendo
137
JavaCC
138
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
139
JavaCC
System.out.println("Vd. ha escrito"+t.image);
}
}
SKIP : {
""
| "\t"
| "\n"
| "\r"
}
TOKEN [IGNORE_CASE] : {
<ID: (["a"-"z"])+>
}
De esta manera, la funcin main() se incluye directamente en el token
manager mediante la clusula TOKEN_MGR_DECLS, y en ella construimos un
canal de entrada del tipo esperado por el analizador lxico, a quien se lo pasamos en
su constructor. Los canales de entrada de los que puede leer el token manager son del
tipo CharStream; JavaCC proporciona la clase SimpleCharStream que hereda de
CharStream para facilitar la creacin de canales aceptables para ser analizados
lexicogrficamente. SimpleCharStream es un envoltorio o estrato para las clases
InputStream y Reader poseyendo constructores que toman como parmetros objetos
de cualesquiera de estos dos tipos.
Una vez hecho esto, los tokens se van recuperando de uno en uno visualizando
por pantalla el correspondiente mensaje. El mismo efecto se podra haber obtenido
escribiendo:
options{ BUILD_PARSER=false; }
PARSER_BEGIN(Ejemplo) public class Ejemplo{} PARSER_END(Ejemplo)
TOKEN_MGR_DECLS : {
public static void main(String args[]) throws ParseException {
new EjemploTokenManager(new SimpleCharStream(System.in)).getNextToken();
}
}
SKIP : {
""
| "\t"
| "\n"
| "\r"
}
SKIP [IGNORE_CASE] : {
<ID: (["a"-"z"])+> { System.out.println("Vd. ha escrito"+image); }
}
mucho ms compacto y donde se han resaltado las modificaciones con respecto al
equivalente anterior.
140
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
141
JavaCC
}
void term():{}{
fact() ( (*|/) fact() )*
}
void fact():{}{
<NUMERO>
| <ID>
| ( expr() )
}
El apartado codigoJava i permite ubicar declaraciones y sentencias Java al
comienzo de la implementacin de una funcin no terminal. Entre otras cosas, esto nos
puede permitir obtener una traza de las acciones que toma nuestro analizador sintctico
para reconocer una sentencia. Por ejemplo, el programa:
void expr():{ System.out.println(Reconociendo una expresin); }{
term() ( (+|-) term() )*
}
void term():{ System.out.println(Reconociendo un trmino); }{
fact() ( (*|/) fact() )*
}
void fact():{ System.out.println(Reconociendo un factor); }{
<NUMERO>
| <ID>
| ( expr() )
}
ante la entrada 23+5*(8-5) producira la salida:
Reconociendo una expresin
Reconociendo un trmino
Reconociendo un factor
Reconociendo un trmino
Reconociendo un factor
Reconociendo un factor
Reconociendo una expresin
Reconociendo un trmino
Reconociendo un factor
Reconociendo un trmino
Reconociendo un factor
142
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
lo que coincide con el recorrido en preorden del rbol sintctico que reconoce dicha
sentencia, y que se muestra en la figura 5.2.
Ntese que cuando se utiliza notacin BNF, el nmero de smbolos bajo una
misma regla puede variar si en sta se emplea la repeticin. P.ej., siguiendo con la
gramtica anterior, la regla de la expr puede dar lugar a tantos hijos como se quiera en
el rbol sintctico; sirva como demostracin el rbol asociado al reconocimiento de
23+5+7+8+5", que se ilustra en la figura 5.3, y cuya corroboracin viene dada por la
salida del analizador sintctico:
Reconociendo una expresin
Reconociendo un trmino
Reconociendo un factor
Reconociendo un trmino
Reconociendo un factor
Reconociendo un trmino
Reconociendo un factor
Reconociendo un trmino
Reconociendo un factor
Reconociendo un trmino
Reconociendo un factor
143
JavaCC
programacin contienen una mayora de expresiones LL(1), y tan slo unas pocas reglas para
las cuales puede ser necesario una prebsqueda de 2, o 3 a lo sumo. En estos casos, se obtendr
un analizador sintctico ms eficiente si se deja la prebsqueda por defecto (a 1), y se le indica
a JavaCC en qu casos concretos debe emplear una prebsqueda superior. Esto se consigue con
la prebsqueda o lookahead local, que tiene la forma:
LOOKAHEAD(n) patron1
y que es considerado en s mismo un patrn tambin. El significado de esta clusula es muy
sencillo: le indica al analizador sintctico que, antes de decidirse a entrar por el patron1 se
asegure de que ste es el correcto tomando n tokens. De esta manera, la gramtica anterior se
puede dejar como LL(1) e indicar la prebsqueda local de la forma:
void expr():{}{
LOOKAHEAD(2) <ID>
| LOOKAHEAD(2) <ID> "." expr()
| <ID> "()"
}
Adems, el propio JavaCC es capaz de decidir si la prebsqueda especificada por el
desarrollador es adecuada o no, informando de ello en caso negativo tanto por exceso como por
defecto.
144
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
145
JavaCC
}
/*
term ::= fact ( ( "*" | "/" ) fact )*
*/
int term():{
int acum1=0,
acum2=0;
}{
acum1=fact() ( ("*" acum2=fact() {acum1*=acum2;} )
| ("/" acum2=fact() {acum1/=acum2;} )
)*
{ return acum1; }
}
/*
fact ::= NUMERO | "(" expr ")"
*/
int fact():{
int acum=0;
}{
acum=numero() { return acum; }
| "(" acum=expr() ")" { return acum; }
}
int numero(): {}{
<NUMERO> { return Integer.parseInt(token.image); }
}
146
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
147
JavaCC
}
}
Ntese que la clusula try-catch de JavaCC puede capturar cualquier
excepcin, y no slo las de tipo ParseException, por lo que el desarrollador puede
crear sus propias excepciones y elevarlas dnde y cuando se produzcan. Por ejemplo,
puede resultar til el crearse la excepcin TablaDeSimbolosException.
El cdigo de recuperacin suele trabajar en panic mode, consumiendo toda la
entrada hasta llegar a un token de seguridad. El siguiente ejemplo ilustra cmo
recuperar errores en sentencias acabadas en punto y coma.
void sentenciaFinalizada() : {} {
try { ( sentencia() <PUNTOYCOMA> )
catch (ParseException e) {
System.out.println(e.toString());
Token t;
do {
t = getNextToken();
} while (t.kind != PUNTOYCOMA);
}
}
148
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
( exprFinalizada() )+
}
/*
exprFinalizada ::= expr ";"
*/
void exprFinalizada():{
int resultado;
}{
try{
resultado=expr() <PUNTOYCOMA> {System.out.println("="+resultado); }
}catch(ParseException x){
System.out.println(x.toString());
Token t;
do {
t = getNextToken();
} while (t.kind != PUNTOYCOMA);
}
}
/*
expr ::= term ( ( "+" | "-" ) term )*
*/
int expr():{
int acum1=0,
acum2=0;
}{
acum1=term() ( ("+" acum2=term() {acum1+=acum2;} )
| ("-" acum2=term() {acum1-=acum2;} )
)*
{ return acum1; }
}
/*
term ::= fact ( ( "*" | "/" ) fact )*
*/
int term():{
int acum1=0,
acum2=0;
}{
acum1=fact() ( ("*" acum2=fact() {acum1*=acum2;} )
| ("/" acum2=fact() {acum1/=acum2;} )
)*
{ return acum1; }
}
/*
fact ::= NUMERO | "(" expr ")"
*/
int fact():{
int acum=0,
uMenos=1;
}{
149
JavaCC
5.6.1 Caractersticas
Por defecto, JJTree genera cdigo para construir el rbol de nodos del parse
para cada no terminal de nuestro lenguaje. Este funcionamiento puede modificarse de
manera que ciertos no terminales no generen nodos, o generar un nodo a partir de una
expansin de una regla de produccin.
JJTree define una interfaz de Java tipo Node, el cual todos los nodos del rbol
parser deben implementar. La interfaz proporciona mtodos para operaciones como
inicializacin del tipo del padre de un nodo generado, as como adicin y recuperacin
de hijos.
JJTree puede operar de dos formas distintas: simple o multi (para buscar
trminos mejores). En modo simple, cada nodo del rbol parser es del tipo
SimpleNode; en modo multi, el tipo de los nodos del rbol parse se deriva del nombre
del nodo. Si no se proporciona implementaciones para la clase nodo, JJTree generar
implementaciones basadas en la clase SimpleNode, pudiendo modificar las
implementaciones de manera que puedan sernos de utilidad.
150
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
manera:
jjtree ejemplo1.jjt
Todos los archivos que se generan de forma automtica se pueden consultar
en la seccin Ficheros Fuente.
Supongamos que creamos un archivo llamado ejemplo1.jjt con las siguientes
especificaciones:
PARSER_BEGIN(ejemplo1)
class ejemplo1 {
public static void main(String args[]) {
System.out.println("Entrada teclado");
ejemplo1 ins = new ejemplo1(System.in);
try {
SimpleNode nodo = ins.Inicio();
nodo.dump("");
} catch (Exception e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
}
}
PARSER_END(ejemplo1)
SKIP :{
""
| "\t"
| "\n"
| "\r"
| <"//" (~["\n","\r"])* ("\n"|"\r"|"\r\n")>
| <"/*" (~["*"])* "*" (~["/"] (~["*"])* "*")* "/">
}
TOKEN : /* LITERALES */
{
<ENTERO:
<DECIMAL>
| <HEX>
| <OCTAL>
>
|
< #DECIMAL: ["1"-"9"] (["0"-"9"])* >
|
< #HEX: "0" ["x","X"] (["0"-"9","a"-"f","A"-"F"])+ >
|
< #OCTAL: "0" (["0"-"7"])* >
}
TOKEN : /* IDENTIFICADORES */
{
151
JavaCC
SimpleNode Inicio() : {}
{
Expresion() ";"
{ return jjtThis; }
}
void Expresion() : {}
{
ExpSuma()
}
void ExpSuma() : {}
{
ExpMult() ( ( "+" | "-" ) ExpMult() )*
}
void ExpMult() : {}
{
ExpUnaria() ( ( "*" | "/" | "%" ) ExpUnaria() )*
}
void ExpUnaria() : {}
{
"(" Expresion() ")" | Identificador() | <ENTERO>
}
void Identificador() : {}
{
<IDENTIFICADOR>
}
La sentencia e.printStackTrace(); me indica que obtengo el contenido de la
pila cuando ha ocurrido un error. Una traza de ejemplo se muestra a continuacin; en
este caso, el error ocurrido es el acceso a una zona de memoria que est a null:
...
java.lang.NullPointerException
at ejemplo4.Identificador(ejemplo4.java:228)
at ejemplo4.ExpUnaria(ejemplo4.java:200)
at ejemplo4.ExpMult(ejemplo4.java:139)
at ejemplo4.ExpSuma(ejemplo4.java:84)
at ejemplo4.Expresion(ejemplo4.java:75)
at ejemplo4.ExpUnaria(ejemplo4.java:196)
at ejemplo4.ExpMult(ejemplo4.java:139)
at ejemplo4.ExpSuma(ejemplo4.java:84)
at ejemplo4.Expresion(ejemplo4.java:75)
at ejemplo4.Inicio(ejemplo4.java:45)
at ejemplo4.Main(ejemplo4.java:30)
152
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
153
JavaCC
154
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
{
ExpSuma()
}
void ExpSuma() #void : {}
{
(ExpMult() ( ( "+" | "-" ) ExpMult() )*)#Suma(>1)
}
void ExpMult() #void: {}
{
(ExpUnaria()(("*"|"/"|"%")ExpUnaria())*)#Mult(>1)
}
void ExpUnaria() #void : {}
{
"(" Expresion() ")" | Identificador() | <LITERAL_ENTERO>
}
Y el archivo ejemplo1bisTreeConstans quedara:
public interface ejemplo1bisTreeConstants {
public int JJTINICIO = 0;
public int JJTVOID = 1;
public int JJTSUMA = 2;
public int JJTMULT = 3;
public int JJTIDENTIFICADOR = 4;
public String[] jjtNodeName = {
"Inicio",
"void",
"Suma",
"Mult",
"Identificador",
};
}
Alimentando de nuevo la entrada con la misma cadena, obtenemos la siguiente
salida: (traza de la pila)
Inicio
Mult
Suma
Identificador
Identificador
Suma
Identificador
Identificador
Observamos que aquellos no-terminales a los que hemos renombrado como
#void, no aparecen en la salida correspondiente a la pila. Esto es muy til cuando
queremos utilizar slo los nodos que realmente aportan informacin al rbol, como ya
se ha comentado anteriormente.
155
JavaCC
156
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
157
JavaCC
158
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
}
De esta forma es ms fcil el acceso a los campos y mtodos del nodo al que
se quiere acceder. Este identificador devuelve siempre el mbito al que pertenece el
nodo.
Un mbito es la unidad que se expande y que le precede de forma inmediata
al nodo con especificacin de atributos (decorated node); por ejemplo, puede ser una
expresin entre parntesis. Cuando la signatura de la produccin se decora (puede tener
al nodo por defecto de forma implcita), el mbito es la parte derecha de la produccin,
incluyendo el bloque de la declaracin.
Tambin se puede usar el mtodo jjThis pasndole como parmetro la parte
izquierda de una expansin de referencia. Un ejemplo basado en la gramtica que estoy
usando es el siguiente:
void ExpUnaria() : {}
{
"(" Expresion() ")" | Identificador() | <ENTERO>
}
159
JavaCC
pila. Se puede acceder a los hijos del padre mediante el mtodo jjGetChild (). Estos
mtodos de acceso a los atributos de cada nodo se han implementado de forma
automtica, como ms arriba se explica.
Otras acciones de usuario slo pueden acceder a los hijos en la pila, debido
a que an no son parte del padre. No sirven los mtodos que impliquen accesos al nodo
(tales como jjGetChild ()).
Un nodo condicional que tenga un descriptor de nodo y sea evaluado a false,
no ser aadido a la pila, ni se le aadir ningn hijo a su estructura. El usuario final,
dentro del mbito de un nodo condicional, puede precisar si el nodo ha sido creado o
no mediante la llamada al mtodo nodeCreated () (implementado en
SimpleNode.java).Devuelve true si la condicin del nodo se satisface, si el nodo ha
sido creado y si ha sido almacenado en la pila. Devuelve false en otro caso.
160
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
public SimpleNode(int i) {
id = i;
}
public SimpleNode(ejemplo3 p, int i) {
this(i);
parser = p;
}
161
JavaCC
}
public String getName() {
return this.name;
}
public void setFirstToken(Token t) {
this.first=t;
}
public Token getFirstToken() {
return this.first;
}
public void setLastToken(Token t) {
this.last=t;
}
public Token getLastToken() {
return this.last;
}
public String toString() {
return ejemplo3TreeConstants.jjtNodeName[id]+":"+name;
}
public String toString(String prefix) { return prefix + toString();
}
/* Override this method if you want to customize how the node dumps
out its children. */
public void dump(String prefix) {
System.out.println(toString(prefix));
if (children != null) {
for (int i = 0; i < children.length; ++i) {
SimpleNode n = (SimpleNode)children[i];
if (n != null) {
n.dump(prefix + " ");
}
}
}
}
}
Aquellas variables y mtodos que se presentan en negrita son los que
necesitamos definir (o redefinir) para que javac reconozca todos aquellos mtodos que
hemos definido en el fichero principal .jjt
El resto de los mtodos son generados automticamente por javacc, pudiendo
modificarlos siempre y cuando seamos conscientes de las modificaciones llevadas a
cabo. Cualquier acceso a un nodo o cualquier operacin incorrecta llevar a la
generacin de errores. Si no los modificamos, tenemos la seguridad de que dichos
mtodos realizan las acciones que les son asignadas por defecto.Otro uso que se le
puede dar es almacenar el parser objeto en el nodo, de manera que se puede
proporcionar el estado que debe ser compartido por todos los nodos generados por el
parser. Por ejemplo, el parser podra mantener una tabla de smbolos.
162
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
163
JavaCC
164
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
165
JavaCC
Esta interfaz es comn para todos los archivos .jjt que se lancen mediante el
ejecutable jjtree. El contenido es:
public interface Node {
/* Este mtodo se llama despus de que el nodo haya pasado a ser el nodo
actual. Indica que se le pueden aadir los nodos hijo*/
public void jjtOpen ();
/* Este mtodo se llama despus de haber aadido todos los nodos hijo*/
public void jjtClose ();
166
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
javac archivo.java
java archivo
2. Si el fichero tiene extensin .jjt:
jjtree archivo.jjt
javacc archivo.jj
javac archivo.java
java archivo
Los archivos generados al ejecutar jjtree <archivo.jjt> son:
archivoTreeConstants.java
JJTarchivoState.java
Node.java
SimpleNode.java
Si el fuente est en modo multi, se generar un fichero .java por cada
no-terminal no declarado como void, es decir, que est decorado. Los nombres de los
ficheros sern los mismos que los nombres de los no-terminales.
Los archivos generados al ejecutar javacc <archivo.jj> son:
archivo.java
archivoConstants.java
archivoTokenManager.java
TokenMgrError.java
ParseException.java
Token.java
SimpleCharStream.java
El siguiente paso es la modificacin del fichero SimpleNode.java con los
mtodos que nos sean de utilidad, esto es, que hemos invocado en el fuente .jjt.
De esta manera, no se generarn errores tipo "method not found" o "symbol
not found" a la hora de compilar el fichero .java generado automticamente por javacc.
Hemos de ejecutar el comando javac en el directorio contenedor o, si
queremos ahorrarnos tiempo, actualizar el path con la direccin en la que se encuentra
javac.bat y java.bat
La linea de comandos a ejecutar, en el caso de encontrarnos en el director
contenedor de la javac es:
C:\jdk1.5.0_01\bin> javac -classpath <camino de los ficheros fuente> ejemplo.java
C:\jdk1.5.0_01\bin> java -classpath <camino de los ficheros fuente> ejemplo
Nos pedir la introduccin de la sentencia(s) a reconocer, si la entrada
seleccionada es la estndar, o pasarle el fichero en el que se encuentra el cdigo a
reconocer mediante el comando
C:\jdk1.5.0_01\bin> java -classpath <path de los ficheros fuente> ejemplo > fichero
167
JavaCC
ExpM ult ::= ( ExpU naria ( ( "*" | "/" | "% " ) ExpU naria )*
)
| Identificador
| Entero
168
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
169
JavaCC
170
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Captulo 6
Tabla de smbolos
171
Tabla de smbolos
172
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Figura 6.1. Accesos a la tabla de smbolos por parte de las distintas fases de un compilador
En general, conforme avanza la etapa de anlisis y van apareciendo nuevas
declaraciones de identificadores, el analizador lxico o el analizador sintctico segn
la estrategia que sigamos, insertar nuevas entradas en la tabla de smbolos, evitando
siempre la existencia de entradas repetidas. El analizador semntico efecta las
comprobaciones sensibles al contexto gracias a la tabla de smbolos y, ya en la etapa
de sntesis, el generador de cdigo intermedio usa las direcciones de memoria asociadas
a cada identificador para generar un programa equivalente al de entrada. Por regla
general el optimizador de cdigo no necesita hacer uso de ella, aunque nada impide que
pueda accederla.
Aunque la eficiencia en los accesos y manipulacin de la tabla de smbolos
173
Tabla de smbolos
son primordiales para hacer un buen compilador, suele ser conveniente realizar un
desarrollo progresivo del mismo. As, en las fases tempranas de la construccin del
compilador, dada su complejidad es mejor incluir una tabla de smbolos lo ms sencilla
posible (por ejemplo, basada en una lista simplemente encadenada) para evitar errores
que se difundan por el resto de fases del compilador. Una vez que las distintas fases
funcionen correctamente puede concentrarse todo el esfuerzo en optimizar la gestin
de la tabla de smbolos. Para que este proceso no influya en las fases ya desarrolladas
del compilador es necesario dotar a la tabla de smbolos de una interfaz claramente
definida e invariable desde el principio: en la optimizacin de la tabla de smbolos se
modifica su implementacin, pero se mantiene su interfaz. Todo este proceso puede
verse en la figura 6.1.
En esta figura se menciona una distincin importante entre la construccin de
compiladores para lenguajes que obligan al programador a declarar todas las variables
(C, Modula-2, Pascal, Java, etc.), y lenguajes en los que las variables se pueden usar
directamente sin necesidad de declararlas (BASIC, Logo, etc.). Cuando un lenguaje
posee rea de declaraciones y rea de sentencias, la aparicin de un identificador tiene
una semntica bien diferente segn el rea en que se encuentre:
Si aparece en el rea de declaraciones, entonces hay que insertar el
identificador en la tabla de smbolos, junto con la informacin que de l se
conozca. Si el identificador ya se encontraba en la tabla, entonces se ha
producido una redeclaracin y debe emitirse un mensaje de error semntico.
Si aparece en el rea de sentencias, entonces hay que buscar el identificador
en la tabla de smbolos para controlar que el uso que de l se est haciendo sea
coherente con su tipo. Si el identificador no se encuentra en la tabla, entonces
es que no ha sido previamente declarado, lo que suele considerarse un error
semntico del que hay que informar al programador.
En estos casos, el analizador lexicogrfico, cuando se encuentra un
identificador desconoce en qu rea lo ha encontrado, por lo que no resulta fcil incluir
una accin lxica que discrimine entre realizar una insercin o una bsqueda; as,
deber ser el analizador sintctico quien haga estas operaciones. As, el atributo del
token identificador suele ser su propio nombre.
Sin embargo, hay unos pocos lenguajes de programacin en los que no es
necesario declarar las variables, bien porque slo existe un nico tipo de datos (como
en nuestro ejemplo de la calculadora donde todo se consideran valores enteros) o bien
porque el propio nombre de la variable sirve para discernir su tipo (en BASIC las
variables que acaban en $ son de tipo cadena de caracteres). En estos casos, el propio
analizador lexicogrfico puede insertar el identificador en la tabla de smbolos la
primera vez que aparezca; antes de insertarlo realiza una operacin de bsqueda por si
ya exista. Y sea cual sea el caso, se obtiene la entrada o ndice de la tabla en la que se
encontraba o se ha incluido; dicho ndice ser el valor que el analizador lxico pasa al
sintctico como atributo de cada identificador.
174
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
una estructura de lista no ordenada simplemente encadenada (ver figura 6.2), ya que
la claridad en nuestro desarrollo prima sobre la eficiencia. En los desarrollos con Java
haremos uso de la clase HashMap.
La figura 6.3 muestra cmo se debe actualizar la tabla de smbolos para
reflejar las asignaciones propuestas. As, en la sentencia una vez evaluada la
expresin 7*3 al valor 21 se inspeccionar la tabla de smbolos en busca del
identificador a. Dado que ste no est, el analizador lxico lo crear inicializndolo a
0; a continuacin la regla asociada a la asignacin modificar la entrada de la a para
175
Tabla de smbolos
Las acciones semnticas usarn este puntero para acceder al valor de cada
variable: si el identificador est a la izquierda del token de asignacin, entonces se
machacar el valor; y si forma parte de una expresin, sta se evaluar al valor de la
variable.
Por otro lado, el atributo del token NUM sigue siendo de tipo int. Dado que
se necesitan atributos de tipos distintos (para NUM y para ID), habra que declarar un
% union en Yacc, de la forma:
%union {
176
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
int numero;
simbolo * ptrSimbolo;
}
y los terminales y no terminales de la forma:
%token <numero> NUM
%token <ptrSimbolo> ID
%type <numero> expr
Para finalizar, podemos optar por permitir asignaciones mltiples o no. Un
ejemplo de asignacin mltiple es:
a := b := c := 16;
que asigna el valor 16 a las variables c, b y a en este orden. Para hacer esto,
consideraremos que las asignaciones vienen dadas por las siguientes reglas:
asig : ID ASIG expr
| ID ASIG asig
;
La figura 6.4 ilustra el rbol sintctico que reconoce este ejemplo. Como
puede verse en l, el no terminal asig tambin debe tener asociado como atributo el
campo numero del % union, con objeto de ir propagando el valor de la expresin y
poder realizar las asignaciones a medida que se asciende en el rbol sintctico.
La tabla de smbolos viene dada por el cdigo siguiente, almacenado en el
fichero TabSimb.c:
1 #include <stdlib.h>
2 #include <stdio.h>
3 typedef struct _simbolo {
4 struct _simbolo * sig;
5 char nombre [20];
6 int valor;
177
Tabla de smbolos
8 } simbolo;
9 simbolo * crear() {
10 return NULL;
11 };
12 void insertar(simbolo **pT, simbolo *s) {
13 s->sig = (*pT);
14 (*pT) = s;
15 };
16 simbolo * buscar(simbolo * t, char nombre[20]){
17 while ( (t != NULL) && (strcmp(nombre, t->nombre)) )
18 t = t->sig;
19 return (t);
20 };
21 void imprimir(simbolo * t) {
22 while (t != NULL) {
23 printf("%s\n", t->nombre);
24 t = t->sig;
25 }
26 };
El programa Lex resulta sumamente sencillo. Tan slo debe reconocer los
lexemas e insertar en la tabla de smbolos la primera vez que aparezca cada
identificador. El siguiente fichero Calcul.lex ilustra cmo hacerlo:
1 %%
2 [0-9]+ {
3 yylval.numero = atoi(yytext);
4 return NUMERO;
5 }
6 ":=" { return ASIG; }
7 "PRINT" { return PRINT; }
8 [a-zA-Z][a-zA-Z0-9]* {
9 yylval.ptrSimbolo = buscar(t,yytext);
10 if (yylval.ptrSimbolo == NULL) {
11 yylval.ptrSimbolo=(simbolo *) malloc(sizeof(simbolo));
12 strcpy(yylval.ptrSimbolo->nombre, yytext);
13 yylval.ptrSimbolo->valor=0;
14 insertar(&t, yylval.ptrSimbolo);
15 }
16 return ID;
17 }
18 & \t\n]+
[b {;}
19 . { return yytext[0]; }
El programa Yacc tan slo difiere de la calculadora tradicional en los accesos
a la tabla de smbolos para obtener y alterar el valor de cada variable. El siguiente
cdigo se supone almacenado en el fichero Calcuy.yac:
1 %{
2 #include "TabSimb.c"
3 simbolo * t;
178
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
4 %}
5 %union {
6 int numero;
7 simbolo * ptrSimbolo;
8 }
9 %token <numero> NUMERO
10 %token <ptrSimbolo> ID
11 %token ASIG PRINT
12 %type <numero> expr asig
13 %left '+' '-'
14 %left '*' '/'
15 %right MENOS_UNARIO
16 %%
17 prog : prog asig ';' { printf("Asignacion(es) efectuada(s).\n"); }
18 | prog PRINT expr ';' { printf("%d\n",$3); }
19 | prog error ';' { yyerrok; }
20 | /* psilon */
21 ;
22 asig : ID ASIG expr {
23 $$ = $3;
24 $1->valor = $3;
25 }
26 | ID ASIG asig {
27 $$ = $3;
28 $1->valor = $3;
29 }
30 ;
31 expr : expr '+' expr {$$ = $1 + $3;}
32 | expr '-' expr {$$ = $1 - $3;}
33 | expr '*' expr {$$ = $1 * $3;}
34 | expr '/' expr {$$ = $1 / $3;}
35 | '(' expr ')' {$$ = $2;}
36 | '-' expr %prec MENOS_UNARIO {$$ = - $2;}
37 | ID {$$ = $1->valor; }
38 | NUMERO {$$ = $1;}
39 ;
40 %%
41 #include "Calcul.c"
42 #include "errorlib.c"
43 void main()
44 { t = crear();
45 yyparse ();
46 imprimir(t);
47 }
179
Tabla de smbolos
de dispersin y proporciona al exterior tan slo las operaciones del punto 6.4.1. Como
clave de la tabla usaremos un String (el nombre del smbolo) y como dato utilizaremos
un objeto de tipo Simbolo que, a su vez, contiene el nombre y el valor de cada variable.
El fichero Simbolo.java es:
1 class Simbolo{
2 String nombre;
3 Integer valor;
4 public Simbolo(String nombre, Integer valor){
5 this.nombre = nombre;
6 this.valor = valor;
7 }
8 }
Y TablaSimbolos.java quedara:
1 import java.util.*;
2 public class TablaSimbolos{
3 HashMap t;
4 public TablaSimbolos(){
5 t = new HashMap();
6 }
7 public Simbolo insertar(String nombre){
8 Simbolo s = new Simbolo(nombre, new Integer(0));
9 t.put(nombre, s);
10 return s;
11 }
12 public Simbolo buscar(String nombre){
13 return (Simbolo)(t.get(nombre));
14 }
15 public void imprimir(){
16 Iterator it = t.values().iterator();
17 while(it.hasNext()){
18 Simbolo s = (Simbolo)it.next();
19 System.out.println(s.nombre + ": "+ s.valor);
20 }
21 }
22 }
La tabla de smbolos debera ser vista tanto por el analizador sintctico como
por el lxico, con objeto de poder acceder a sus elementos. Aunque en este ejemplo no
es estrictamente necesario, haremos que la tabla de smbolos sea creada por el
programa principal como dato propio del analizador sintctico (ya que estamos en un
anlisis dirigido por sintaxis) y se pasar al analizador lxico como un parmetro ms
en el momento de su construccin. De esta manera el fichero Calcul.jflex quedara:
1 import java_cup.runtime.*;
2 import java.io.*;
3 %%
4 %{
5 private TablaSimbolos tabla;
6 public Yylex(Reader in, TablaSimbolos t){
180
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
7 this(in);
8 this.tabla = t;
9 }
10 %}
11 %unicode
12 %cup
13 %line
14 %column
15 %%
16 "+" { return new Symbol(sym.MAS); }
17 "-" { return new Symbol(sym.MENOS); }
18 "*" { return new Symbol(sym.POR); }
19 "/" { return new Symbol(sym.ENTRE); }
20 ";" { return new Symbol(sym.PUNTOYCOMA); }
21 "(" { return new Symbol(sym.LPAREN); }
22 ")" { return new Symbol(sym.RPAREN); }
23 ":=" { return new Symbol(sym.ASIG); }
24 "PRINT" { return new Symbol(sym.PRINT); }
25 [:jletter:][:jletterdigit:]* {
26 Simbolo s;
27 if ((s = tabla.buscar(yytext())) == null)
28 s = tabla.insertar(yytext());
29 return new Symbol(sym.ID, s);
30 }
31 [:digit:]+ { return new Symbol(sym.NUMERO, new Integer(yytext())); }
32 & \t\r\n]+
[b {;}
33 . { System.out.println("Error lxico."+yytext()+"-"); }
Las acciones sobre la tabla de smbolos se han destacado en el listado del
fichero Calcuy.cup que sera:
1 import java_cup.runtime.*;
2 import java.io.*;
3 parser code {:
4 static TablaSimbolos tabla = new TablaSimbolos();
5 public static void main(String[] arg){
6 parser parserObj = new parser();
7 Yylex miAnalizadorLexico =
8 new Yylex(new InputStreamReader(System.in), tabla);
9 parserObj.setScanner(miAnalizadorLexico);
10 try{
11 parserObj.parse();
12 tabla.imprimir();
13 }catch(Exception x){
14 x.printStackTrace();
15 System.out.println("Error fatal.\n");
16 }
17 }
18 :};
19 terminal PUNTOYCOMA, MAS, MENOS, POR, ENTRE;
181
Tabla de smbolos
182
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
183
Tabla de smbolos
35 void sentFinalizada():{
36 int resultado;
37 Vector v = new Vector();
38 Simbolo s;
39 }{
40 try{
41 <PRINT> resultado=expr() <PUNTOYCOMA>
42 { System.out.println("="+resultado); }
43 |
44 ( LOOKAHEAD(2)
45 s=id() ":=" { v.add(s); } )+ resultado=expr() <PUNTOYCOMA>
46 {
47 Integer valor = new Integer(resultado);
48 Iterator it = v.iterator();
49 while(it.hasNext())
50 ((Simbolo)it.next()).valor = valor;
51 System.out.println("Asignacion(es) efectuada(s).");
52 }
53 }catch(ParseException x){
54 System.out.println(x.toString());
55 Token t;
56 do {
57 t = getNextToken();
58 } while (t.kind != PUNTOYCOMA);
59 }
60 }
61 /*
62 expr ::= term ( ( "+" | "-" ) term )*
63 */
64 int expr():{
65 int acum1=0,
66 acum2=0;
67 }{
68 acum1=term() ( ("+" acum2=term() {acum1+=acum2;} )
69 | ("-" acum2=term() {acum1-=acum2;} )
70 )*
71 { return acum1; }
72 }
73 /*
74 term ::= fact ( ( "*" | "/" ) fact )*
75 */
76 int term():{
77 int acum1=0,
78 acum2=0;
79 }{
80 acum1=fact() ( ("*" acum2=fact() {acum1*=acum2;} )
81 | ("/" acum2=fact() {acum1/=acum2;} )
82 )*
83 { return acum1; }
184
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
84 }
85 /*
86 fact ::= (-)* ( ID | NUMERO | "(" expr ")" )
87 */
88 int fact():{
89 int acum=0,
90 signo=1;
91 Simbolo s;
92 }{
93 ("-" { signo *= -1; } )*
94 ( s=id() { acum = s.valor.intValue(); }
95 | acum=numero()
96 | "(" acum=expr() ")"
97 )
98 { return signo*acum; }
99 }
100 Simbolo id():{}{
101 <ID> {
102 Simbolo s;
103 if ((s = tabla.buscar(token.image)) == null)
104 s = tabla.insertar(token.image);
105 return s;
106 }
107 }
108 int numero():{}{
109 <NUMERO> { return Integer.parseInt(token.image); }
110 }
111 SKIP : {
112 <ILEGAL: (~[])> { System.out.println("Carcter: "+image+" no esperado.");}
113 }
Como se ha comentado anteriormente, la necesidad de realizar todas las
asignaciones de golpe en una asignacin mltiple se podra evitar introduciendo la
regla:
/*
asig ::= ( ID := )+ expr
*/
int asig():{
int resultado;
Simbolo s;
}{
( LOOKAHEAD(4)
s=id() := resultado=asig() { s.valor = new Integer(resultado); }
| s=id() := resultado=expr() { s.valor = new Integer(resultado); }
)
{ return resultado; }
}
Y la regla
id() ":=" )+ expr() <PUNTOYCOMA>
185
Tabla de smbolos
se sustituye por
asig() <PUNTOYCOMA> { System.out.println("Asignacion(es) efectuada(s)."); }
186
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Captulo 7
Gestin de tipos
187
Gestin de tipos
188
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
189
Gestin de tipos
int edad;
} e1, e2;
y
struct{
char nombre[20];
char apellidos[35];
int edad;
} c1, c2;
donde los tipos de las variables e1, e2, c1 y c2 carece de nombre. Por regla
general, en estas situaciones, un lenguaje con compatibilidad de tipos nominal crea un
tipo con un nombre interno para cada declaracin annima, lo que en el ejemplo se
traduce en la creacin de dos tipos, uno al que pertenecern e1 y e2 (y que, por tanto,
sern compatibles nominalmente), y otro al que pertenecern c1 y c2 (que tambin
sern compatibles nominalmente).
Por otro lado, la compatibilidad funcional ni siquiera requiere que las
expresiones posean la misma estructura. Dos expresiones e a y e b de tipos A y B
respectivamente son funcionalmente compatibles si A y B poseen diferente estructura
pero pueden usarse indistintamente en algn contexto. El caso ms usual consiste en
poder sumar valores enteros y decimales. Por regla general, los enteros se suelen
representar en complemento a dos, mientras que los nmeros decimales se representan
en coma flotante. Aunque se utilice el mismo nmero de octetos para representar a
ambos, su estructura es esencialmente diferente, pero la mayora de lenguajes de
programacin permite realizar operaciones aritmticas entre ellos. En estos lenguajes
existe una compatibilidad funcional entre el tipo entero y el decimal.
La compatibilidad funcional requiere que el compilador detecte las situaciones
en que se mezclan correctamente expresiones de tipos con diferente estructura, al
objeto de generar cdigo que realice la conversin del tipo de la expresin menos
precisa a la de mayor precisin. Por ejemplo, al sumar enteros con decimales, los
enteros se deben transformar a decimales y, slo entonces, realizar la operacin. A esta
operacin automtica se la denomina promocin (del ingls promotion).
En los lenguajes orientados a objeto tambin aparece la compatibilidad por
herencia: si una clase B hereda de otra A (B es un A), por regla general puede
emplearse un objeto de clase B en cualquier situacin en la que se espere un objeto de
tipo A, ya que B es un A y por tanto cumple con todos los requisitos funcionales para
suplantarlo.
190
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
191
Gestin de tipos
192
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
PRINT c + 5
20
PRINT b
Camarada Trotski
PRINT b+7
No se pueden sumar cadenas y enteros.
PRINT b+, fundador de la URSS.
Camarada Trotski, fundador de la URSS.
PRINT A_ENTERO(23)*4
92
7.3.2.2 Definicin de la tabla de smbolos
En este paso se define la estructura de la tabla de smbolos, si es que esta es
necesaria para el problema, as como su interfaz.
En este caso s necesitaremos la tabla de smbolos, ya que en ella se
almacenarn los valores de los identificadores. Tambin ser necesario guardar el tipo
de cada variable con objeto de controlar que su utilizacin sea coherente con las
intenciones de su declaracin (implcita en la primera asignacin). De esta forma, el
tipo de una variable se guardar en la tabla de smbolos mediante un carcter con el
siguiente significado:
c: tipo cadena de caracteres.
e: tipo entero.
i: tipo indefinido. Este tipo se usar en caso de que en la primera asignacin
se le intente dar a una variable el valor resultante de una expresin errnea.
Parece claro que, dado que existen variables de tipos diferentes, una entrada
de la tabla de smbolos debe ser capaz de guardar tanto una cadena como un entero, en
funcin del tipo de la variable que representa. En lenguaje C esto se soluciona mediante
una estructura union, mientras que en Java se tendr un manejador a Object que, en
funcin del tipo de la variable, apuntar a un Integer o a un String. La figura 7.2
muestra grficamente la estructura de la tabla de smbolos.
Figura 7.2. Situacin de la tabla de smbolos tras introducir en la calculadora las sentencias del
punto 7.3.2.1
193
Gestin de tipos
Figura 7.3. La entrada de la tabla marcada en rojo acaba de ser insertada por el analizador
lxico que, como desconoce el tipo de la variable recin insertada c, la pone como indefinida:
i
194
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
195
Gestin de tipos
196
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
| A_CADENA ( expr )
toman, respectivamente, expresiones de tipo cadena y entero y devuelven enteros y
cadenas, tambin respectivamente. En cualquier otro caso se produce una expresin
indefinida. La funcin A_CADENA() convierte el valor de su parmetro en una ristra
de caracteres que son dgitos. La funcin A_ENTERO() convierte su parmetro en un
nmero, siempre y cundo la cadena consista en una secuencia de dgitos; en caso
contrario tambin se genera una expresin indefinida.
La regla:
expr : ID
es trivial, ya que el analizador sintctico recibe del lxico la entrada de la tabla de
smbolos en la que reside la variable. Por tanto, dado que el atributo de una expresin
comparte parte de la estructura de un nodo de la tabla de smbolos, en esta accin
semntica lo nico que hay que hacer es copiar dicho trozo (ver figura 7.4).
La semntica de la asignacin dada por la regla:
asig : ID ASIG expr
es un poco ms compleja. Recordemos que el tipo de una variable viene dado por el
tipo de lo que se le asigna por primera vez. Por ejemplo, si en laprimera asignacin a
la variable a se le asigna el valor 7, entonces su tipo es entero durante toda la ejecucin
del programa; si se le asigna Ana, entonces ser de tipo cadena. Por tanto, la
asignacin debe:
1. Si el tipo del l-valor es indefinido, se le asigna el valor y el tipo del r-
valor, sea ste el que sea.
2. Si el tipo del l-valor est definido, entonces:
2.1. Si coinciden los tipos del l-valor y del r-valor entonces se asigna al
l-valor el valor del r-valor.
2.2. Si no coinciden los tipos, se emite un mensaje de error y el l-valor
se deja intacto.
En esta explicacin queda implcito qu hacer cuando se intenta asignar a una
variable una expresin indefinida: si la variable an es indefinida se deja tal cual; si no,
la variable no pasa a valer indefinido, sino que se queda como est. Si ante una
asignacin indefinida se nos ocurriera poner la variable en estado indefinido, la
secuencia:
a=7 U Funciona
a = 7 + hola X Falla (y cambia el tipo de a a indefinido)
a = mal U Funciona (pero no debera)
cambiara el valor y el tipo de a, lo cual es incorrecto con respecto a la semntica de
nuestro lenguaje.
Una vez asignado el tipo a una variable ste siempre es el mismo; si durante
la ejecucin se le asigna una expresin errnea, el tipo de la variable seguir siendo el
mismo y su valor tampoco cambiar. Este comportamiento podra modificarse
aadiendo una variable booleana a la tabla de smbolos que me diga si el valor de la
variable es conocido o no, de tal manera que una asignacin errnea pondra esta
197
Gestin de tipos
198
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
199
Gestin de tipos
evitarlas en la medida de sus posibilidades. De esta forma, ante una entrada como c
:= 4 * p * k * 5, cuyo rbol se presenta en la figura 7.8.a) slo se muestra un
mensaje de error. Ello se debe a que los mensajes de error al sumar o multiplicar se
muestran slo cuando cada parmetro es correcto por s slo, pero operado con el
adyacente produce un error. No obstante, una entrada como c := 4 + p + k * 5,
cuyo rbol se presenta en la figura 7.8.b), posee dos errores de tipos: 1) la suma de 4
ms p; y 2) el producto de k por 5; nuestro intrprete informa de los dos, ya que
se producen en ramas diferentes del rbol.
200
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
201
Gestin de tipos
202
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
125 %%
126 #include "TipSimpl.c"
127 void main(){
128 yyparse();
129 imprimir(t);
130 }
131 void yyerror(char * s){
132 printf("%s\n",s);
133 }
203
Gestin de tipos
24 Iterator it = t.values().iterator();
25 while(it.hasNext()){
26 Simbolo s = (Simbolo)it.next();
27 System.out.print(s.nombre + ": ");
28 if (s.valor == null) System.out.print("i: Indefinido");
29 else if (s.valor instanceof Integer) System.out.print("e: "+s.valor.toString());
30 else if (s.valor instanceof String) System.out.print("c: "+s.valor);
31 }
32 }
33 }
El fichero TipSimb.jflex queda:
1 import java_cup.runtime.*;
2 import java.io.*;
3 %%
4 %{
5 private TablaSimbolos tabla;
6 public Yylex(Reader in, TablaSimbolos t){
7 this(in);
8 this.tabla = t;
9 }
10 %}
11 %unicode
12 %cup
13 %line
14 %column
15 %%
16 "+" { return new Symbol(sym.MAS); }
17 "*" { return new Symbol(sym.POR); }
18 "(" { return new Symbol(sym.LPAREN); }
19 ")" { return new Symbol(sym.RPAREN); }
20 "\n" { return new Symbol(sym.RETORNODECARRO); }
21 ":=" { return new Symbol(sym.ASIG); }
22 \"[^\"]*\" { return new Symbol(sym.CADENA,yytext().substring(1,yytext().length()-1));}
23 [:digit:]+ { return new Symbol(sym.NUMERO, new Integer(yytext())); }
24 "PRINT" { return new Symbol(sym.IMPRIMIR); }
25 "A_CADENA" { return new Symbol(sym.A_CADENA); }
26 "A_ENTERO" { return new Symbol(sym.A_ENTERO); }
27 [:jletter:][:jletterdigit:]* {
28 Simbolo s;
29 if ((s = tabla.buscar(yytext())) == null)
30 s = tabla.insertar(yytext());
31 return new Symbol(sym.ID, s);
32 }
33 & \t\r]+ {;}
[b
34 . { System.out.println("Error lxico."+yytext()+"-"); }
Y por ltimo, TipSimp.cup es:
1 import java_cup.runtime.*;
2 import java.io.*;
204
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
3 parser code {:
4 static TablaSimbolos tabla = new TablaSimbolos();
5 public static void main(String[] arg){
6 parser parserObj = new parser();
7 Yylex miAnalizadorLexico =
8 new Yylex(new InputStreamReader(System.in), tabla);
9 parserObj.setScanner(miAnalizadorLexico);
10 try{
11 parserObj.parse();
12 tabla.imprimir();
13 }catch(Exception x){
14 x.printStackTrace();
15 System.out.println("Error fatal.\n");
16 }
17 }
18 :};
19 action code {:
20 private static char tipo(Object o){
21 if (o == null) return 'i';
22 else if (o instanceof Integer) return 'e';
23 else return 'c';
24 }
25 :}
26 terminal RETORNODECARRO, MAS, POR;
27 terminal IMPRIMIR, ASIG, LPAREN, RPAREN, A_ENTERO, A_CADENA;
28 terminal Simbolo ID;
29 terminal Integer NUMERO;
30 terminal String CADENA;
31 non terminal asig, prog;
32 non terminal Object expr;
33 precedence left MAS;
34 precedence left POR;
35 /* Gramtica */
36 prog ::= /* psilon */
37 | prog asig RETORNODECARRO
38 | prog IMPRIMIR expr:e RETORNODECARRO {:
39 if (tipo(e) == 'i')
40 System.out.println("Indefinido.");
41 else
42 System.out.println(e.toString());
43 :}
44 | prog error RETORNODECARRO
45 ;
46 asig ::= ID:s ASIG expr:e {:
47 if (tipo(e) == 'i')
48 System.out.println("Asignacion no efectuada.");
49 else
50 if ((s.valor == null) || (tipo(s.valor) == tipo(e)))
51 s.valor = e;
205
Gestin de tipos
52 else
53 System.err.println("Asignacion de tipos incompatibles.");
54 :}
55 ;
56 expr ::= expr:e1 MAS expr:e2 {:
57 if((tipo(e1) == 'c') && (tipo(e2) == 'c'))
58 RESULT = e1.toString() + e2.toString();
59 else if((tipo(e1) == 'e') && (tipo(e2) == 'e'))
60 RESULT = new Integer( ((Integer)e1).intValue()
61 + ((Integer)e2).intValue());
62 else {
63 RESULT = null;
64 if ((tipo(e1) != 'i') && (tipo(e2) != 'i'))
65 System.err.println("No se pueden sumar cadenas y enteros.");
66 }
67 :}
68 | expr:e1 POR expr:e2 {:
69 if((tipo(e1) == 'c') || (tipo(e2) == 'c')) {
70 RESULT = null;
71 System.err.println("Una cadena no se puede multiplicar.");
72 } else if((tipo(e1) == 'e') && (tipo(e2) == 'e'))
73 RESULT = new Integer( ((Integer)e1).intValue()
74 * ((Integer)e2).intValue());
75 else
76 RESULT = null;
77 :}
78 | A_ENTERO LPAREN expr:e RPAREN {:
79 if (tipo(e) != 'c') {
80 RESULT = null;
81 System.err.println("Error de conversin. Se requiere una cadena.");
82 } else try {
83 RESULT = new Integer(Integer.parseInt(e.toString()));
84 } catch (Exception x){
85 RESULT = null;
86 System.err.println("La cadena a convertir slo puede tener dgitos.");
87 }
88 :}
89 | A_CADENA LPAREN expr:e RPAREN {:
90 if (tipo(e) != 'e') {
91 RESULT = null;
92 System.err.println("Error de conversin. Se requiere un entero.");
93 } else
94 RESULT = e.toString();
95 :}
96 | ID:s {:
97 RESULT = s.valor;
98 if (tipo(s.valor) == 'i')
99 System.err.println("Tipo de "+ s.nombre +" no definido.");
100 :}
206
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
207
Gestin de tipos
25 s.valor = e;
26 else
27 System.err.println("Asignacion de tipos incompatibles.");
28 }
29 private static Object usarMAS(Object e1, Object e2){
30 if((tipo(e1) == 'c') && (tipo(e2) == 'c'))
31 return e1.toString() + e2.toString();
32 else if((tipo(e1) == 'e') && (tipo(e2) == 'e'))
33 return new Integer(((Integer)e1).intValue() + ((Integer)e2).intValue());
34 else {
35 if ((tipo(e1) != 'i') && (tipo(e2) != 'i'))
36 System.err.println("No se pueden sumar cadenas y enteros.");
37 return null;
38 }
39 }
40 private static Object usarPOR(Object e1, Object e2){
41 if((tipo(e1) == 'c') || (tipo(e2) == 'c')) {
42 System.err.println("Una cadena no se puede multiplicar.");
43 return null;
44 } else if((tipo(e1) == 'e') && (tipo(e2) == 'e'))
45 return new Integer(((Integer)e1).intValue() * ((Integer)e2).intValue());
46 else
47 return null;
48 }
49 private static Object usarID(Simbolo s){
50 if (tipo(s.valor) == 'i')
51 System.err.println("Tipo de "+ s.nombre +" no definido.");
52 return s.valor;
53 }
54 private static Object usarA_CADENA(Object e){
55 if (tipo(e) != 'e') {
56 System.err.println("Error de conversin. Se requiere un entero.");
57 return null;
58 } else
59 return e.toString();
60 }
61 private static Object usarA_ENTERO(Object e){
62 if (tipo(e) != 'c') {
63 System.err.println("Error de conversin. Se requiere una cadena.");
64 return null;
65 } else try {
66 return new Integer(Integer.parseInt(e.toString()));
67 } catch (Exception x){
68 System.err.println("La cadena a convertir slo puede tener dgitos.");
69 return null;
70 }
71 }
72 }
73 PARSER_END(Calculadora)
208
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
74 SKIP : {
75 "b& "
76 | "\t"
77 | "\r"
78 }
79 TOKEN [IGNORE_CASE] :
80 {
81 <IMPRIMIR: "PRINT">
82 | <A_CADENA: "A_CADENA">
83 | <A_ENTERO: "A_ENTERO">
84 | <ID: ["A"-"Z"](["A"-"Z", "0"-"9"])*>
85 | <NUMERO: (["0"-"9"])+>
86 | <CADENA: "\""(~["\""])*"\"">
87 | <RETORNODECARRO: "\n">
88 }
89 /*
90 gramatica ::= ( sentFinalizada )*
91 */
92 void gramatica():{}{
93 (sentFinalizada())*
94 }
95 /*
96 sentFinalizada ::= IMPRIMIR expr '\n' | ID ASIG expr '\n' | error '\n'
97 */
98 void sentFinalizada():{
99 Simbolo s;
100 Object e;
101 }{ try {
102 <IMPRIMIR> e=expr() <RETORNODECARRO> { usarIMPRIMIR(e); }
103 | s=id() ":=" e=expr() <RETORNODECARRO> { usarASIG(s, e); }
104 }catch(ParseException x){
105 System.out.println(x.toString());
106 Token t;
107 do {
108 t = getNextToken();
109 } while (t.kind != RETORNODECARRO);
110 }
111 }
112 /*
113 expr ::= term ('+' term)*
114 */
115 Object expr():{
116 Object t1, t2;
117 }{
118 t1=term() ( "+" t2=term() { t1=usarMAS(t1, t2); } )* { return t1; }
119 }
120 /*
121 term ::= fact ('*' fact)*
122 */
209
Gestin de tipos
210
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
211
Gestin de tipos
Punteros
Arrays de dimensiones desconocidas.
Funciones sin parmetros.
El hecho de desconocer las dimensiones de los arrays y de obviar los
parmetros de las funciones nos permitir centrarnos exclusivamente en la gestin de
tipos. Por otro lado, admitiremos los tipos bsicos:
Lgico o booleano.
Entero.
Real.
Carcter.
En total tenemos cuatro tipos primitivos y tres constructores de tipos. Es
importante observar que por cada tipo primitivo es necesario suministrar al
programador algn mecanismo con el que pueda definir constantes de cada uno de esos
tipos: constantes booleanas, enteras, reales y de carcter.
En lo que sigue construiremos la gestin de tipos de un compilador, lo que
quiere decir que dejaremos a un lado el ejemplo de la calculadora y nos centraremos
en reconocer declaraciones y expresiones vlidas. Para comprobar que nuestro
compilador est funcionando bien, cada vez que el programador introduzca una
expresin se nos deber mostrar un mensaje donde, en lenguaje natural, se informe del
tipo de dicha expresin. Lo siguiente es un ejemplo de entrada:
a: POINTER TO BOOLEAN;
a^;
Es un boolean.
a;
Es un puntero a un boolean.
beta : POINTER TO ARRAY[] OF POINTER TO PROCEDURE(): INTEGER;
beta^;
Es un array de un puntero a una funcin que devuelve un entero.
beta[2];
Esperaba un array.
beta();
Esperaba una funcin.
true;
Es un boolean.
212
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
prog : /* psilon */
| prog decl ';'
| prog expr ';'
| prog error ';'
|
;
decl : ID ',' decl
| ID ':' tipo
;
tipo : INTEGER
| REAL
| CHAR
| BOOLEAN
| POINTER TO tipo
| ARRAY '[' ']' OF tipo
| PROCEDURE '(' ')' ':' tipo
;
expr : ID
| NUM
| NUMREAL
| CARACTER
| CTELOGICA
| expr '^'
| expr'['NUM']'
| expr'('')'
;
Esta gramtica expresada en notacin EBNF de JavaCC queda:
prog():{}{
( bloque() )*
}
bloque():{}{
LOOKAHEAD(2)
decl() <PUNTOYCOMA>
| expr() <PUNTOYCOMA>
| /* Gestin del error */
}
decl():{}{
LOOKAHEAD(2)
<ID> : tipo()
| <ID> , decl()
}
tipo():{}{
( <POINTER> <TO>
| <ARRAY> [ ] <OF>
| <PROCEDURE> ( ) :
)* (
<INTEGER>
| <REAL>
| <CHAR>
213
Gestin de tipos
| <BOOLEAN>
)
}
expr():{}{
( <ID> (
^
| [ <NUM> ]
| ( )
)*
| <NUM>
| <NUMREAL>
| <CARACTER>
| <CTELOGICA>
)
}
en la que puede observarse que se ha utilizado recursin a derecha para facilitar el
proceso de propagacin de atributos, como se ver posteriormente. Adems, esta
gramtica no permite construcciones de la forma 8^, mientras que la dada a travs de
reglas de produccin s; por tanto, en una se detectar como error sintctico y en otra
como error semntico de tipos.
214
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
215
Gestin de tipos
216
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
indefinido.
Segn esta codificacin, los cdigos 111, 001, 010, 011 y 000 slo pueden
encontrarse en la base de la pila; es ms, los cdigos 100, 101 y 110 nunca podrn estar
en la base dado que requieren otro tipo por debajo sobre el que aplicarse. Este hecho
nos permite utilizar los mismos cdigos para representar tanto tipos primitivos como
constructores de tipo: si se halla en la base de la pila es que se refiere a un tipo
primitivo y en caso contrario lo que se referencia es un constructor. Si adems
representamos los tipos mediante un puntero a entero, el tipo indefinido vendra dado
por un puntero a null, con lo que la codificacin sera:
Tipos y constructores Cdigo de bits
POINTER TO p - 01
ARRAY [] OF a - 10
PROCEDURE() : f - 11
BOOLEAN b- 00
INTEGER i- 01
REAL r- 10
CHAR c- 11
donde las lneas con el mismo color tienen asociado el mismo cdigo de bits. Ntese
cmo, de nuevo, no es posible usar el cdigo 00 para representar a un constructor de
tipo siempre que no se quiera guardar la longitud del tipo y los bits no utilizados se
rellenen a 0.
No puede existir confusin, ya que lo que est en la base de la pila slo puede
ser un tipo primitivo; en la figura 7.11 se muestra cmo quedara el tipo de alfa
codificado con un entero de 16 bits.
217
Gestin de tipos
permite entrar por ella justo al comienzo del anlisis, como paso base de la recursin
de las reglas de prog sobre s misma.
En las reglas de decl y de tipo, hacemos la gramtica recursiva a la derecha.
As, las reglas de tipo permiten construir la pila desde la base hasta la cima, ejecutando
tan slo operaciones apilar. Por otro lado, las reglas decl permiten asignar el tipo a
cada uno de los identificadores declarados, tambin de derecha a izquierda, tal y como
se ilustra en la figura 7.12. Como puede apreciarse en ella, cuando el analizador
sintctico se encuentra un tipo primitivo construye una pila con un slo carcter (el
correspondiente al tipo encontrado) y lo asigna como atributo del no terminal tipo. A
medida que se reduce a nuevos no terminales tipo en base a las reglas recursivas a la
derecha, se toma el atributo del tipo del consecuente como punto de partida para
construir el atributo del antecedente; basta con aadir a ste el carcter correspondiente
al constructor de tipo de que se trate (marcados en gris en la figura 7.12). Una vez
construido el tipo completo, el analizador sintctico debe reducir por la regla:
decl : ID ':' tipo
de tal manera que debe crearse una entrada en la tabla de smbolos para la variable
indicada como atributo de ID (c segn la figura 7.12); el tipo de esta variable vendr
dado por el atributo de tipo. Por supuesto es necesario comprobar que no se ha
realizado una redeclaracin de variables. Dado que es posible realizar declaraciones de
varias variables simultneamente, el resto de variables (b y a segun la figura 7.12) se
reconocen mediante la reduccin de la regla:
decl : ID ',' decl
218
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Obviamente el proceso debe ser muy similar al de la regla anterior, esto es, crear una
entrada en la tabla de smbolos para la variable del atributo de ID y asignarle una pila
de tipos. Claro est, para ello es necesario disponer de la pila de tipos como atributo del
no terminal decl, por lo que dicha pila debe ser propagada en ambas reglas de
produccin desde el no terminal del consecuente al antecedente, o sea, a decl. En
conclusin, tanto tipo como decl tienen como atributo una pila de tipos.
Pasemos ahora a estudiar las reglas de las expresiones. El objetivo es que una
expresin tenga tambin asociada una pila de tipos de manera que, una vez completado
el anlisis de la expresin completa, su pila de tipos se pase como parmetro a una
funcin que traslade su contenido a una frase en lenguaje natural.
Las expresiones pueden dividirse en tres bloques: 1) primitivas, formadas por
una constante de cualquier tipo; 2) simples, formadas por una variable; y 3) complejas,
formadas por una expresin simple con modificadores de tipo a su derecha.
Las expresiones del tipo 1 tienen asociada una pila de tipos con un nico
carcter, que se corresponder con el tipo del literal reconocido:
i para NUM .
r para NUMREAL.
c para CARACTER.
b para CTELOGICA.
Las expresiones del tipo 2 tienen una pila de tipos exactamente igual que la
de la variable a que representan. Si la variable no ha sido declarada entonces la pila de
tipos tendr nicamente una u que representa al tipo indefinido (no se puede usar la
letra i porque se confundira con la i de INTEGER).
219
Gestin de tipos
Las expresiones del tipo 3 parten de una expresin (que tiene una pila de tipos
asociada) junto con un modificador de tipo. En este caso hay que controlar que el tipo
principal de la expresin (representado por la cima de su pila de tipos), es compatible
con el modificador de tipo, tal y como se vio al final del apartado 7.4.2.2. Por ejemplo,
si encontramos expr^, expr[NUM] o expr() hay que controlar que el tipo
principal de alfa sea p, a o f respectivamente. Suponiendo que la variable a ha sido
declarada tal y como aparece en la figura 7.12, la figura muestra cmo vara la pila de
tipos durante el reconocimiento de la expresin a^[3].
En el momento en que reduce por la regla:
expr : ID
se busca en la tabla de smbolos a la variable que viene dada como atributo de ID; una
vez encontrada, el atributo de expr ser una copia de la pila de tipos de dicha variable.
A medida que se van aplicando las reglas:
expr : expr '^'
| expr'['NUM']'
| expr'('')'
;
se va comprobando que el modificador de la regla aplicada sea compatible con el tipo
de la cima de la pila de la expresin a que se aplica. Si es compatible, entonces el tipo
de la expresin resultante (el antecedente) ser el mismo que la del consecuente pero
sin la cima. Si no se debe emitir un mensaje de error y la expresin resultante ser de
tipo indefinido.
En resumen, los constructores de tipo construyen la pila metiendo caracteres
en ella, mientras que los modificadores de tipo destruyen la pila sacando caracteres por
la cima.
220
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
221
Gestin de tipos
222
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
223
Gestin de tipos
224
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
2 String nombre;
3 Stack tipo;
4 public Simbolo(String nombre, Stack tipo){
5 this.nombre = nombre;
6 this.tipo = tipo;
7 }
8 public static String tipoToString(Stack st){
9 String retorno = "";
10 for(int cont = st.size()-1; cont>=0; cont--)
11 switch(((Character)st.elementAt(cont)).charValue()) {
12 case('i'): { retorno += "un entero."; break; }
13 case('r'): { retorno += "un real."; break; }
14 case('b'): { retorno += "un booleano."; break; }
15 case('c'): { retorno += "un caracter."; break; }
16 case('p'): { retorno += "un puntero a "; break; }
17 case('a'): { retorno += "un array de "; break; }
18 case('f'): { retorno += "una funcion que devuelve "; break; }
19 case('u'): { retorno += "indefinido."; break; }
20 };
21 return retorno;
22 }
23 }
La tabla de smbolos es muy parecida a la de ejercicios anteriores, excepto por
el hecho de que la funcin insertar() tambin acepta un objeto de tipo Stack. Esto se
debe a que en el momento de la insercin de un smbolo tambin se conoce su tipo,
dado que es Cup quien realiza las inserciones. As, la clase TablaSimbolos queda:
1 import java.util.*;
2 public class TablaSimbolos{
3 HashMap t;
4 public TablaSimbolos(){
5 t = new HashMap();
6 }
7 public Simbolo insertar(String nombre, Stack st){
8 Simbolo s = new Simbolo(nombre, st);
9 t.put(nombre, s);
10 return s;
11 }
12 public Simbolo buscar(String nombre){
13 return (Simbolo)(t.get(nombre));
14 }
15 public void imprimir(){
16 Iterator it = t.values().iterator();
17 while(it.hasNext()){
18 Simbolo s = (Simbolo)it.next();
19 System.out.println(s.nombre + ": " + Simbolo.tipoToString(s.tipo));
20 }
21 }
22 }
225
Gestin de tipos
226
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
de tipo Yylex como variable de instancia; esto se hace con el objetivo de poder
referenciar las funciones linea() y columna() del analizador lxico en un mensaje de
error personalizado. Los mensajes de error se pueden personalizar reescribiendo la
funcin syntax_error() que toma como parmetro el token actual que ha provocado el
error. En nuestro caso se ha reescrito esta funcin en las lneas 20 a 23. En stas se
llama, a su vez, a la funcin report_error() que se encarga de sacar un mensaje por la
salida de error (System.err) y que, en caso necesario, tambin puede ser reescrita por
el desarrollador. De hecho, la salida de error se ha utilizado en todos los mensajes de
error semntico que se visualizan: variable no declarada, variable redeclarada,
esperaba un puntero, etc.
Por otro lado, y como ya se ha indicado, se ha utilizado un objeto de tipo
Stack para almacenar la pila de tipos. Ntese cmo, por regla general, se utiliza
comparticin estructural, esto es, no se generan copias de la pila de tipos. Por ejemplo,
durante una declaracin, la pila de tipos se construye al encontrar un tipo primitivo y
a medida que se encuentran constructores de tipos se van aadiendo elementos a la pila
original, sin cear copias de la misma (se usa asignacin pura y no la funcin clone()).
El nico caso en el que es necesario trabajar con una copia de una pila de tipos es
cuando se recupera el tipo de un identificador como expresin base sobre la que aplicar
modificadores posteriores (vase la lnea 82); si no se hiciera una copia clone() en este
caso, cada vez que se aplicara un modificador de tipo se estara cambiando el tipo del
identificador (y de todos los que se declararon junto a l).
El fichero TipCompl.cup queda:
1 import java_cup.runtime.*;
2 import java.io.*;
3 import java.util.*;
4 parser code {:
5 static TablaSimbolos tabla = new TablaSimbolos();
6 Yylex miAnalizadorLexico;
7 public static void main(String[] arg){
8 parser parserObj = new parser();
9 parserObj.miAnalizadorLexico =
10 new Yylex(new InputStreamReader(System.in));
11 parserObj.setScanner(parserObj.miAnalizadorLexico);
12 try{
13 parserObj.parse();
14 tabla.imprimir();
15 }catch(Exception x){
16 x.printStackTrace();
17 System.err.println("Error fatal.");
18 }
19 }
20 public void syntax_error(Symbol cur_token){
21 report_error("Error de sintaxis: linea "+miAnalizadorLexico.linea()+
22 ", columna "+miAnalizadorLexico.columna(), null);
23 }
227
Gestin de tipos
24 :};
25 terminal INTEGER, REAL, CHAR, BOOLEAN, POINTER, TO;
26 terminal ARRAY, OF, PROCEDURE;
27 terminal NUM, CARACTER, NUMREAL, CTELOGICA;
28 terminal RPAREN, LPAREN, RCORCH, LCORCH, CIRCUN;
29 terminal DOSPUN, PUNTOYCOMA, COMA;
30 terminal String ID;
31 non terminal Stack tipo, expr, decl;
32 non terminal prog;
33 /* Gramtica */
34 start with prog;
35 prog ::= /*Epsilon */
36 | prog decl:d PUNTOYCOMA
37 {: System.out.println(Simbolo.tipoToString(d)); :}
38 | prog expr:e PUNTOYCOMA
39 {: System.out.println(Simbolo.tipoToString(e)); :}
40 | prog error PUNTOYCOMA
41 {: ; :}
42 ;
43 decl ::= ID:id COMA decl:t {:
44 RESULT = t;
45 if(parser.tabla.buscar(id)!= null)
46 System.err.println(id + " redeclarada.");
47 else
48 parser.tabla.insertar(id, t);
49 :}
50 | ID:id DOSPUN tipo:t {:
51 RESULT = t;
52 if(parser.tabla.buscar(id)!= null)
53 System.err.println(id + " redeclarada.");
54 else
55 parser.tabla.insertar(id, t);
56 :}
57 ;
58 tipo ::= INTEGER {: RESULT = new Stack(); RESULT.push(new Character('i')); :}
59 | REAL {: RESULT = new Stack(); RESULT.push(new Character('r')); :}
60 | CHAR {: RESULT =new Stack(); RESULT.push(new Character('c')); :}
61 | BOOLEAN {: RESULT =new Stack(); RESULT.push(new Character('b')); :}
62 | POINTER TO tipo:t {:
63 RESULT = t;
64 RESULT.push(new Character('p'));
65 :}
66 | ARRAY LCORCH RCORCH OF tipo:t {:
67 RESULT = t;
68 RESULT.push(new Character('a'));
69 :}
70 | PROCEDURE RPAREN LPAREN DOSPUN tipo:t {:
71 RESULT = t;
72 RESULT.push(new Character('f'));
228
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
73 :}
74 ;
75 expr ::= ID:id {:
76 Simbolo s;
77 if ((s = parser.tabla.buscar(id)) == null) {
78 RESULT = new Stack();
79 RESULT.push(new Character('u'));
80 System.err.println(id + " no declarada.");
81 } else
82 RESULT = (Stack)s.tipo.clone();
83 :}
84 | NUM {: RESULT = new Stack(); RESULT.push(new Character('i')); :}
85 | NUMREAL {: RESULT = new Stack(); RESULT.push(new Character('r')); :}
86 | CARACTER {: RESULT =new Stack(); RESULT.push(new Character('c')); :}
87 | CTELOGICA{: RESULT = new Stack(); RESULT.push(new Character('b')); :}
88 | expr:t CIRCUN {:
89 if(((Character)t.peek()).charValue() != 'p') {
90 RESULT = new Stack(); RESULT.push(new Character('u'));
91 if(((Character)t.peek()).charValue() != 'u')
92 System.err.println("Esperaba un puntero.");
93 } else {
94 RESULT = t;
95 RESULT.pop();
96 }
97 :}
98 | expr:t LCORCH NUM RCORCH {:
99 if(((Character)t.peek()).charValue() != 'a') {
100 RESULT = new Stack(); RESULT.push(new Character('u'));
101 if(((Character)t.peek()).charValue() != 'u')
102 System.err.println("Esperaba un array.");
103 } else {
104 RESULT = t;
105 RESULT.pop();
106 }
107 :}
108 | expr:t LPAREN RPAREN {:
109 if(((Character)t.peek()).charValue() != 'p') {
110 RESULT = new Stack(); RESULT.push(new Character('u'));
111 if(((Character)t.peek()).charValue() != 'u')
112 System.err.println("Esperaba una funcin.");
113 } else {
114 RESULT = t;
115 RESULT.pop();
116 }
117 :}
118 ;
229
Gestin de tipos
230
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
38 System.err.println("Esperaba "+mensaje+".");
39 return nuevaPila('u');
40 } else {
41 st.pop();
42 return st;
43 }
44 }
45 }
46 PARSER_END(TiposComplejos)
47 SKIP : {
48 "b& "
49 | "\t"
50 | "\n"
51 | "\r"
52 }
53 TOKEN [IGNORE_CASE] :
54 {
55 <POINTER: "POINTER">
56 | <TO: "TO">
57 | <ARRAY: "ARRAY">
58 | <OF: "OF">
59 | <PROCEDURE: "PROCEDURE">
60 | <INTEGER: "INTEGER">
61 | <REAL: "REAL">
62 | <CHAR: "CHAR">
63 | <BOOLEAN: "BOOLEAN">
64 | <NUM: (["0"-"9"])+>
65 | <NUMREAL: (["0"-"9"])+"."(["0"-"9"])+>
66 | <CARACTER: ("\'"~[]"\'")>
67 | <CTELOGICA: ("TRUE"|"FALSE")>
68 | <ID: ["A"-"Z"](["A"-"Z", "0"-"9"])*>
69 | <PUNTOYCOMA: ";">
70 }
71 /*
72 prog ::= ( bloque )*
73 */
74 void prog():{}{
75 ( bloque() )*
76 }
77 /*
78 bloque ::= decl ';' | expr ';' | error ';'
79 */
80 void bloque():{
81 Stack st;
82 }{
83 try {
84 LOOKAHEAD(2)
85 st=decl() <PUNTOYCOMA> { System.out.println(Simbolo.tipoToString(st)); }
86 | st=expr() <PUNTOYCOMA> { System.out.println(Simbolo.tipoToString(st)); }
231
Gestin de tipos
87 }catch(ParseException x){
88 System.err.println(x.toString());
89 Token t;
90 do {
91 t = getNextToken();
92 } while (t.kind != PUNTOYCOMA);
93 }
94 }
95 /*
96 decl ::= ID ( ',' ID )* ':' tipo
97 */
98 Stack decl():{
99 String nombre;
100 Stack t;
101 }{
102 LOOKAHEAD(2)
103 nombre=id() ":" t=tipo() { return declaracion(nombre, t); }
104 | nombre=id() "," t=decl() { return declaracion(nombre, t); }
105 }
106 /*
107 tipo ::= ( POINTER TO | ARRAY '[' ']' OF | PROCEDURE '(' ')' ':' )*
108 ( INTEGER | REAL | CHAR | BOOLEAN )
109 */
110 Stack tipo():{
111 Stack st = new Stack();
112 }{
113 ( <POINTER> <TO> { st.push(new Character('p')); }
114 | <ARRAY> "[" "]" <OF> { st.push(new Character('a')); }
115 | <PROCEDURE> "(" ")" ":" { st.push(new Character('f')); }
116 )* (
117 <INTEGER> { st.push(new Character('i')); return invertir(st); }
118 | <REAL> { st.push(new Character('r')); return invertir(st); }
119 | <CHAR> { st.push(new Character('c')); return invertir(st); }
120 | <BOOLEAN> { st.push(new Character('b')); return invertir(st); }
121 )
122 }
123 /*
124 expr ::= NUM | NUMREAL | CARACTER | CTELOGICA | ID ( '^' | '[' NUM ']' | '(' ')' )*
125 */
126 Stack expr():{
127 String nombre;
128 Stack st;
129 }{
130 ( nombre=id() { st = utilizacion(nombre); }
131 (
132 "^" { st = comprobar(st, "un puntero", 'p'); }
133 | "[" <NUM> "]" { st = comprobar(st, "un array", 'a'); }
134 | "(" ")" { st = comprobar(st, "una funcin", 'f'); }
135 )* { return st; }
232
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
233
Gestin de tipos
234
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Captulo 8
Generacin de cdigo
235
Generacin de cdigo
236
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
237
Generacin de cdigo
ejemplo, se puede asumir que un valor entero ocupa 16 bits, uno real ocupa 32 bits, un
carcter ocupa 8 bits, etc.
Con los tercetos anteriores cubriremos todos los ejemplos propuestos en el
presente captulo. No obstante, estos tercetos no recogen todas las posibilidades de
ejecucin bsicas contempladas por un microprocesador actual. Por ejemplo, para
llamar a una subrutina se tienen tercetos para meter los parmetros en la pila de
llamadas, para invocar a la subrutina indicando el nmero de parmetros que se le pasa,
para tomar un parmetro de la pila, y para retornar:
param x: mete al parmetro real x en la pila.
call p, n: llama a la subrutina que comienza en la etiqueta p, y le dice que
tome n parmetros de la cima de la pila.
pop x: toma un parmetro de la pila y lo almacena en la direccin x.
return y: retorna el valor y.
Otros tercetos permiten gestionar el direccionamiento indirecto y el indexado:
Direccionamiento indexado: los tercetos son de la forma y := x[i] y x[i] := y,
donde x es la direccin base, e i es el desplazamiento.
Direccionamiento indirecto: los tercetos son de la forma y:=&x y x:=*y,
donde el smbolo & quiere decir la direccin de ... y el smbolo * quiere
decir el valor de la direccin contenida en la direccin ....
La eleccin de operadores permisibles es un aspecto importante en el diseo
de cdigo intermedio. El conjunto de operadores debe ser lo bastante rico como para
permitir implementar todas las operaciones del lenguaje fuente. Un conjunto de
operadores pequeo es ms fcil de implantar en una nueva mquina objeto, pero si es
demasiado limitado puede obligar a la etapa inicial a generar largas secuencias de
instrucciones para algunas operaciones del lenguaje fuente. En tal caso, el optimizador
y el generador de cdigo tendrn que trabajar ms si se desea producir un buen cdigo.
A continuacin se muestra un ejemplo de cdigo de tercetos que almacena en
la variable c la suma de a y b (ntese que b se destruye como consecuencia de este
clculo):
c=a
label etqBucle
if b = 0 goto etqFin
b = b-1
c = c+1
goto etqBucle
label etqFin
238
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
239
Generacin de cdigo
240
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
terceto de asignacin binaria, en el que slo hay dos operandos y un resultado. Sin
embargo, la reduccin por cada una de estas reglas representa un resultado intermedio
en el camino de calcular el resultado final, tal y como ilustra la figura 8.3.
Estos resultados intermedios deben almacenarse en algn lugar para que los
tercetos puedan trabajar con ellos. Para este propsito se utilizan variables temporales
generadas automticamente por el compilador en la cantidad que sea necesario. La
figura 8.2 muestra el cdigo de tercetos que se debe generar para una asignacin que
tiene como r-valor la expresin de la figura 8.3, as como las variables temporales que
es necesario generar en cada paso. Como resulta evidente, el cdigo de tercetos
resultante es equivalente al cdigo fuente.
Figura 8.2. Representacin de la variable temporal que hay que generar en cada
reduccin, y del terceto que debe producirse. El objetivo final de estos tercetos
es almacenar en la variable a el valor 3+5+7+8, lo que se consigue en base a las
variables temporales
241
Generacin de cdigo
otras variables, etc. Tambin resulta interesante observar que las variables temporales
pueden reutilizarse en clculos diferentes. No obstante, esto supone una optimizacin
que no implementaremos en aras de concentrarnos exclusivamente en la generacin de
cdigo correcto.
Para la realizacin de nuestra calculadora, hay que observar dos aspectos muy
importantes:
Gestionar adecuadamente los atributos. En la calculadora no hay parte de
declaraciones ni chequeo de tipos, ya que se supone que todas las variables
son de tipo numrico y que son declaradas en el momento en que hacen su
primera aparicin. Es por ello que no ser necesaria una tabla de smbolos ya
que, adems, en los tercetos aparecern los identificadores de las variables, y
no las direcciones de memoria a que representan. Esto tambin nos permite
omitir un gestor de direcciones de memoria que las vaya asignando a medida
que van apareciendo nuevas variables.
Realizar una secuencia de printf. Nuestro propsito es hacer una traduccin
textual, en la que entra un texto que representa asignaciones de alto nivel, y
sale otro texto que representa a los tercetos. Por tanto nuestras acciones
semnticas tendrn por objetivo realizar una secuencia de printf o
System.out.println para visualizar estos textos de salida. Por este motivo, en
tiempo de compilacin, no se van a evaluar las expresiones ni a realizar
clculos de ningn tipo. Nuestro traductor se encarga nicamente de gestionar
cadenas de texto; esto conlleva que, por ejemplo, el atributo asociado al token
NUM no tenga porqu ser de tipo int, sino que basta con que sea un char[] o
un String, ya que no va a ser manipulado aritmticamente.
En la construccin de un compilador real, la salida no suele ser un texto, sino
un fichero binario que contiene el cdigo mquina agrupado por bloques que sern
gestionados por un enlazador (linker) para obtener el fichero ejecutable final.
Asimismo, las acciones semnticas deben realizar toda la gestin de tipos necesaria
antes de proceder a la generacin de cdigo, lo que lleva a la necesidad de una tabla de
smbolos completa.
242
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
asig, entonces tambin se podr generar cdigo de tercetos incluso para las
asignaciones mltiples. En cuanto a las expresiones que representan a un nico valor
numrico, se puede optar por generar una variable temporal para almacenarlos, o bien
guardar el lexema numrico como atributo de la expresin, lo que conduce a una
optimizacin en cuanto al nmero de tercetos generados y de variables temporales
utilizadas. La figura 8.4.a) muestra el caso estricto en que slo se permite como atributo
el nombre de una variable, y la figura 8.4.b) muestra el caso relajado. En ambas
situaciones el tipo de atributo es el mismo. En la solucin que se propone ms adelante
se opta por el caso b), ya que supone una optimizacin sin ningn esfuerzo por parte
del desarrollador.
Para acabar, la figura 8.5 muestra la propagacin de atributos en el caso de
realizar una asignacin mltiple. Como ya se ha comentado antes, la generacin de
cdigo se hace posible mediante la utilizacin de un atributo al no terminal asig que
243
Generacin de cdigo
244
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
245
Generacin de cdigo
48 }
49 void yyerror(char * s){
50 printf("%s\n",s);
51 }
246
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
247
Generacin de cdigo
248
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
49 }
50 }
51 String asigCompuesta():{
52 String s;
53 String a, e;
54 }{ LOOKAHEAD(4)
55 s=id() ":=" a=asigCompuesta() { usarASIG(s, a); return s; }
56 | s=id() ":=" e=expr() { usarASIG(s, e); return s; }
57 }
58 /*
59 expr ::= term ('+' term)*
60 */
61 String expr():{
62 String t1, t2;
63 }{
64 t1=term() ( "+" t2=term() { t1=usarOpAritmetico(t1, t2, "+"); } )* { return t1; }
65 }
66 /*
67 term ::= fact ('*' fact)*
68 */
69 String term():{
70 String f1, f2;
71 }{
72 f1=fact() ( "*" f2=fact() { f1=usarOpAritmetico(f1, f2, "*"); } )* { return f1; }
73 }
74 /*
75 fact ::= ID | NUMERO | '(' expr ')'
76 */
77 String fact():{
78 String s, e;
79 }{
80 s=id() { return s; }
81 | s=numero() { return s; }
82 | "(" e=expr() ")" { return e; }
83 }
84 String id():{}{
85 <ID> { return token.image; }
86 }
87 String numero():{}{
88 <NUMERO> { return token.image; }
89 }
90 SKIP : {
91 <ILEGAL: (~[])> { System.out.println("Carcter: "+image+" no esperado.");}
92 }
Aunque los no terminales sentFinalizada() y asigCompuesta() podran
haberse fusionado en uno slo, se ha preferido diferenciar claramente entre la gestin
del error a travs del terminal <PUNTOYCOM A>, y el reconocimiento de una
asignacin compuesta con recursin a derecha. Ntese asimismo que el no terminal
249
Generacin de cdigo
250
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
ni control de tipos, se ha decidido que nuestro pequeo lenguaje slo posea el tipo
entero y que no sea necesario declarar las variables antes de su utilizacin.
La gramtica que se va a utilizar, en formato de reglas de produccin, es:
prog : prog sent ';'
| prog error ';'
| /* psilon */
;
sent : ID ASIG expr
| IF cond THEN sent ';' opcional FIN IF
| '{' lista_sent '}'
| WHILE cond DO sent ';' FIN WHILE
| REPEAT sent ';' UNTIL cond
| sent_case
;
opcional : ELSE sent';'
| /*Epsilon*/
;
lista_sent : /*Epsilon*/
| lista_sent sent ';'
| lista_sent error ';'
;
sent_case : inicio_case OTHERWISE sent ';' FIN CASE
| inicio_case FIN CASE
;
inicio_case : CASE expr OF
| inicio_case CASO expr ':' sent ';'
;
expr : NUMERO
| ID
| expr'+'expr
| expr'-'expr
| expr'*'expr
| expr'/'expr
| '-'expr %prec MENOS_UNARIO
| '('expr')'
;
cond : expr'>'expr
| expr'<'expr
| expr'MAI'expr
| expr'MEI'expr
| expr'='expr
| expr'DIF'expr
| NOT cond
| cond AND cond
| cond OR cond
| '(' cond ')'
;
251
Generacin de cdigo
Es de notar que el cuerpo de una sentencia W HILE, REPEAT, IF, etc. est
formado por una sola sentencia. Si se desea incluir ms de una, el mecanismo consiste
en recurrir a la sentencia compuesta. Una sentencia compuesta est formada por una
lista de sentencias delimitada por llaves, tal y como indica la regla:
sent : '{' lista_sent '}'
Adems, nada impide introducir como cuerpo de cualquier sentencia de
control de flujo a otra sentencia de control de flujo. Es ms, el anidamiento de
sentencias puede producirse a cualquier nivel de profundidad. Este hecho hace que
haya que prestar especial atencin al control de errores. Como puede observarse en la
gramtica, se tienen dos reglas de error, una a nivel de programa y otra a nivel de
lista_sent, esto es, en el cuerpo de una sentencia compuesta.
Si se produce un error sintctico en el interior de una sentencia compuesta, y
slo se tuviera la regla de error a nivel de programa, se hara una recuperacin
incorrecta, ya que el mecanismo panic mode extraera elementos de la pila hasta
encontrar prog; luego extraera tokens hasta encontrar el punto y coma. Y por ltimo
continuara el proceso de anlisis sintctico pensando que ha recuperado el error
convenientemente y que las siguientes sentencias estn a nivel de programa, lo cual es
falso puesto que se estaba en el interior de una sentencia compuesta. Este problema se
ilustra en la figura 8.6. Para evitarlo es necesario introducir una nueva regla de error
a nivel de sentencia compuesta.
Figura 8.6. Necesidad de dos reglas de error cuando se utilizan sentencias compuestas
252
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
253
Generacin de cdigo
}
BloqueCondicion condSimple():{}{
expr() ( <MAYOR> expr()
| <MENOR> expr()
| <IGUAL> expr()
| <MAI> expr()
| <MEI> expr()
| <DIF> expr()
)
}
Esta gramtica tiene dos diferencias con respecto a la expresada con reglas de
produccin. En primer lugar el no terminal lista_sent ha desaparecido, ya que
sintcticamente es equivalente a prog (que aqu hemos llamado gramtica). Ello nos
lleva a necesitar un solo control de errores en la ltima regla del no terminal
sentFinalizada. Evidentemente, esto tambin podra haberse hecho en la gramtica
basada en reglas, pero se ha preferido ilustrar la gestin de varias reglas de error en una
misma gramtica.
La segunda diferencia radica en la gestin de las condiciones mediante una
gramtica descendente. Ante una secuencia de caracteres como: ((((27*8) ... es
necesario conocer el contenido de los puntos suspensivos para poder decidir si los tres
primeros parntesis se asocian a una expresin o a una condicin: si nos encontramos
el carcter > los parntesis se asocian a una condicin, pero si nos encontramos -
7) entonces, al menos, el tercer parntesis es de una expresin y an habra que seguir
mirando para ver a qu pertenecen los otros dos parntesis. Este problema tiene tres
soluciones claramente diferenciadas:
Establecer un lookahead infinito.
Diferenciar las agrupaciones de expresiones y las de condiciones; por
ejemplo, las expresiones se pueden agrupar entre parntesis, y las condiciones
entre corchetes. Esta ha sido la solucin adoptada en este ejemplo.
Considerar que una condicin es una expresin de tipo lgico o booleano.
Esta es la decisin ms ampliamente extendida, aunque no se ha elegido en
este caso ya que implica una gestin de tipos sobre la que no nos queremos
centrar.
254
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
{
FACTORIAL := FACTORIAL * ALFA;
ALFA := ALFA - 1;
};
FIN WHILE;
El cdigo de tercetos equivalente sera:
ALFA = n
tmp1 = 1;
FACTORIAL = tmp1
label etq1
tmp2 = 1;
if ALFA > tmp2 goto etq2
goto etq3
label etq2
tmp3 = FACTORIAL * ALFA;
FACTORIAL = tmp3
tmp4 = 1;
tmp5 = ALFA - tmp4;
ALFA = tmp5
goto etq1
label etq3
El siguiente ejemplo ilustra el cdigo generado para una sentencia CASE:
CASE NOTA OF
* Podemos emplear comentarios, dedicando una linea a cada uno de ellos.
CASO 5 : CALIFICACION := SOBRESALIENTE;
CASO 4 : CALIFICACION := NOTABLE;
CASO 3 : CALIFICACION := APROBADO;
CASO 2 : CALIFICACION := INSUFICIENTE;
OTHERWISE
CALIFICACION := MUY_DEFICIENTE;
FIN CASE;
que equivale a:
tmp1 = 5;
if NOTA != tmp1 goto etq2
CALIFICACION = SOBRESALIENTE
goto etq1
label etq2
tmp2 = 4;
if NOTA != tmp2 goto etq3
CALIFICACION = NOTABLE
goto etq1
label etq3
tmp3 = 3;
if NOTA != tmp3 goto etq4
CALIFICACION = APROBADO
goto etq1
label etq4
tmp4 = 2;
255
Generacin de cdigo
256
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
SU M A_P U N TO S = tm p2 tm p19 = 1;
tm p3 = 0; tm p20 = D O SE S + tm p19;
N U M ER O _TIR AD AS = tm p3 D O SE S = tm p20
tm p4 = 0; goto etq13
TIR AD A_A N TE R IO R = tm p4 label etq15
label etq4 tm p21 = 3;
tm p5 = 5; if D AD O != tm p21 goto etq16
tm p6 = RA N D O M IZE * tm p5; tm p22 = 1;
tm p7 = 1; tm p23 = TR ES ES + tm p22;
tm p8 = tm p6 + tm p7; TR ES ES = tm p23
D AD O = tm p8 goto etq13
tm p9 = 6; label etq16
if TIR AD A_A N TE R IO R != tm p9 goto etq5 tm p24 = 4;
goto etq6 if D AD O != tm p24 goto etq17
label etq5 tm p25 = 1;
tm p10 = 1; tm p26 = C U AT R O S + tm p25;
tm p11 = N U M ER O _TIR AD AS + tm p10; C U AT R O S = tm p26
N U M ER O _TIR AD AS = tm p11 goto etq13
gotoetq7 label etq17
label etq6 tm p27 = 5;
label etq7 if D AD O != tm p27 goto etq18
tm p12 = S U M A _P U N TO S + D A D O S ; tm p28 = 1;
SU M A_P U N TO S = tm p12 tm p29 = C IN C O S + tm p28;
if SU M A_P U N TO S > TO TA L goto etq8 C IN C O S = tm p29
goto etq9 goto etq13
label etq8 label etq18
tm p13 = SU M A_P U N TO S - TO TA L; tm p30 = 1;
tm p14 = TO TA L - tm p13; tm p31 = SE ISE S + tm p30;
SU M A_P U N TO S = tm p14 SE ISE S = tm p31
gotoetq10 label etq13
label etq9 goto etq19
if SU M A_P U N TO S != TO TA L goto etq11 label etq12
goto etq12 label etq19
label etq11 label etq10
tm p15 = 1; TIR AD A_A NTE RIO R = D AD O
if D AD O != tm p15 goto etq14 if SU M A_P U N TO S = TO TA L goto etq20
tm p16 = 1; goto etq21
tm p17 = U N O S + tm p16; label etq21
U N O S = tm p17 goto etq4
goto etq13 label etq20
label etq14 JU G A R = D E S E O _D E L_U S U A R IO
tm p18 = 2; goto etq1
if D AD O != tm p18 goto etq15 label etq3
257
Generacin de cdigo
258
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
259
Generacin de cdigo
nuevaEtq($$.etqVerdad);
nuevaEtq($$.etqFalso);
printf(\tif %s > %s goto %s\n, $1, $3, $$.etqVerdad);
printf(\tgoto %s\n, $$.etqFalso);
}
Como puede observarse en esta accin semntica, los atributos asociados a
una condicin son un par de etiquetas, una de verdad y otra de falso. Adems, el bloque
de cdigo g enerado posee dos gotos (uno condicional y otro incondicional), a sendas
etiquetas, pero no ha ubicado el destino de ninguna de ellas, esto es, no se han puesto
los label. La figura 8.7 muestra el cdigo asociado a cada bloque gramatical.
Esto se debe a que la regla que haga uso de la condicin (ya sea en un IF, en
un W HILE, etc.), debe colocar estos label convenientemente. Por ejemplo, un W HILE
colocara el destino de la etiqueta de verdad al comienzo del cuerpo del bucle, mientras
que la de falso ira tras el cuerpo del bucle; de esta manera cuando la condicin sea
260
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
falsa se salta el cuerpo, y cuando sea cierta se ejecuta un nuevo ciclo. Una cosa
parecida ocurrira con el resto de las sentencias; la figura 8.8 muestra el caso de un IF.
8.4.3.1.2 Condiciones compuestas
Un hecho que se deduce de la discusin anterior es que las sentencias en las
que se enmarca una condicin (IF, W HILE, etc.) deben realizar un tratamiento
uniforme de stas tanto si se trata de condiciones simples como de condiciones
compuestas en las que intervienen los operadores lgicos AND, OR y NOT. Por tanto,
una condicin compuesta debe generar por s misma un bloque de cdigo en el que
existan dos gotos para los cuales no se ha establecido an el destino. Las etiquetas de
destino se asignarn como atributos del no terminal cond, con lo que el tratamiento de
las condiciones compuestas coincide con el de las simples.
Adems, se desea utilizar la tcnica del cortocircuito, por el que una condicin
slo se evala si su valor de verdad o falsedad es decisivo en la evaluacin de la
condicin compuesta en la que interviene.
La explicacin siguiente puede comprenderse mejor si pensamos en el bloque
de cdigo generado por una condicin como una caja negra que ejecuta, en algn
momento, o un salto a una etiqueta de falso, o uno a una etiqueta de verdad, tal y como
ilustra la figura 8.9.a). Por tanto, ante una regla como
cond : cond 1 AND cond 2
se generaran los bloques de cdigo de la figura 8.9.b). Pero, al tener que reducir todo
el conjunto a una condicin compuesta, los bloques de la figura 8.9.b) deben reducirse
a uno slo, en el que exista una sola etiqueta de verdad y una sola de falso, tal y como
ilustra la figura 8.9.c). Asumimos que el flujo le llega a un bloque por arriba.
261
Generacin de cdigo
262
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
condiciones ser necesario incluir una accin semntica intermedia. Asimismo, tambin
puede apreciarse en esta figura el subterfugio utilizado para conseguir que el destino
de falsedad para ambas condiciones sea el mismo: basta con ubicar una sentencia de
salto incondicional a la segunda etiqueta justo despus del destino de la primera.
Adems, las etiquetas de verdad y de falso del bloque completo se corresponden con
las de verdad y falso de cond 2, o sea etq_2. Verdad y etq_2.Falso, por lo que ambos atributos
deben copiarse tal cual desde cond 2 hasta el antecedente cond en la correspondiente
regla de produccin. La figura 8.12 muestra un ejemplo de cdigo generado para el
caso de la condicin: a > 3 AND a < 8.
263
Generacin de cdigo
264
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Con esto finalizamos todos los operadores lgicos que posee el lenguaje
propuesto. Para otros operadores, tales como NAND, NOR, IMPLIES (implicacin
lgica), etc., el procedimiento es el mismo. Un caso particular es el del operador XOR,
cuya tabla de verdad es:
A B A XOR B
Verdad Verdad Falso
Verdad Falso Verdad
Falso Verdad Verdad
Falso Falso Falso
Este operador no admite cortocircuito por lo que se debe generar cdigo
apoyado por variables temporales para que se salte a una etiqueta de falso caso de que
las dos condiciones sean iguales, y a una etiqueta de verdad en caso contrario.
265
Generacin de cdigo
266
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
necesitar una etiqueta de fin de IF diferente. Por tanto, est claro que esta etiqueta no
debe almacenarse en una variable global ya que la sentencia que hay en el ELSE
podra ser otro IF, lo que hara necesario reducirlo por esta misma regla y machacara
el valor de la variable global antes de llegar a la accin semntica del final (en negro).
Por otro lado, etqFINIF tampoco puede declararse en la accin semntica en
azul (donde se le da valor pasndola como parmetro a la funcin nuevaEtq), ya que
en tal caso se tratara de una variable local a un bloque de lenguaje C y no sera visible
en la accin semntica del final.
Por tanto, necesitar declararla en algn lugar tal que sea lo bastante global
como para ser vista en dos acciones semnticas de la misma regla; pero tambin debe
ser lo bastante local como para que los IF anidados puedan asignar valores locales sin
machacar las etiquetas de los IF ms externos. Es de notar que un metacompilador
como JavaCC soluciona este problema mediante el rea de cdigo asociado a cada
regla BNF; aqu se podra declarar la variable etqFINIF y utilizarla en cualquier accin
semntica de esa misma regla. En caso de haber IF anidados, la propia recursin en las
llamadas a las funciones que representan los no terminales se encargara de crear
nuevos mbitos con copias de dicha variable.
La solucin adoptada por JavaCC nos puede dar alguna pista para solucionar
el problema en PCYacc: necesitamos una variable local a la regla completa, que se
pueda utilizar en todas sus acciones semnticas, pero que sea diferente si hay IF
anidados. La solucin consiste en utilizar el token IF como contenedor de dicha
variable, asignndosela como atributo: el atributo del token IF es accesible por las
acciones semnticas que hay tras l, y si hay, por ejemplo, tres IF anidados, cada uno
de ellos tendr su propio atributo. Asumiendo esto, la regla queda:
sent : IF cond THEN { printf(label %s\n, $2.etqVerdad); }
sent { nuevaEtq($1);
267
Generacin de cdigo
268
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
269
Generacin de cdigo
270
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
271
Generacin de cdigo
272
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
273
Generacin de cdigo
intermedio que ya ha sido generado por otras reglas de produccin; por tanto, todo lo
que no est rayado debemos generarlo en acciones semnticas asociadas a las reglas de
produccin del CASE.
Como puede verse en esta figura, se ha conseguido que todos los bloques en
azul tengan aproximadamente la misma forma:
el clculo de una expresin local que se almacena en tmp i
una comparacin de tmp.expr con tmp i. Si no son iguales saltamos al final del
bloque
el cdigo intermedio asociado a una sentencia senti
un salto al final de la sentencia CASE
Parece claro que la mayor complejidad en la generacin de este cdigo, radica
en la regla del CASO (segunda regla de inicio_case), ya que ella es la que debe generar
todo el cdigo marcado de azul en la figura 8.24. Estudiemos con detenimiento uno de
los bloques azules. Los tercetos que debemos generar en cada bloque de cdigo azul
se dividen, a su vez en dos partes: una intercalada entre expr i y sent i, y otra al final del
bloque. El siguiente paso a seguir es examinar este cdigo para deducir qu atributos
vamos a necesitar en cada no terminal. Centrndonos en las variables y etiquetas que
aparecen, podemos clasificarlas en dos apartados:
La variable tmp i y la etiqueta etq i son locales a cada bloque, esto es, no se
utilizan ms que en un solo bloque.
La variable tmp. expr y la etiqueta etq FIN_CASE se utilizan repetidamente en todos
los bloques. Adems se utiliza en la regla de sent_case con el objetivo de
visualizar el label etq FIN_CASE ltimo. tmp.expr representa a la expresin que
se va a ir comparando y etq FIN_CASE representa el final del CASE, etiqueta a
la que se saltar tras poner el cdigo asociado a la sentencia de cada CASO.
De un lado, la variable tmp i es generada por la expresin local al CASO y
almacenada en el atributo del no terminal expr, por lo que es accesible en cualquier
accin semntica de la regla de produccin que se ponga detrs de dicho no terminal.
En cuanto a la variable etq i puede observarse que se utiliza en un goto en un terceto
entre la generacin del cdigo de expr i y de sent i, mientras que el label aparece
despus de senti. Esto quiere decir que necesitaremos esta etiqueta en dos acciones
semnticas de la misma regla, por lo que optaremos por asociarla como atributo del
token CASO, de forma parecida a como se ha hecho para las sentencias IF, W HILE
y REPEAT.
De otro lado, y un poco ms complicado, la variable tmp.expr se genera por la
expr que aparece en la regla del CASE, pero debe utilizarse en todas y cada una de las
reglas de CASO con el objetivo de comparar su valor con las sucesivas tmp i. Por ello
hemos de encontrar algn mecanismo que permita propagar el nombre de esta variable
a travs de todas las reducciones de la regla del CASO necesarias para reconocer la
sentencia completa. Con la etiqueta etq FIN_CASE sucede algo parecido, ya que dicha
etiqueta es necesaria en cada bloque CASO con el objetivo de saltar al final del CASE
274
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Figura 8.25. Propagacin de atributos del no terminal inicio_case. Las clusulas se han
coloreado de acuerdo a la figura 8.24. En rojo se ha indicado la primera aparicin de cada
atributo
275
Generacin de cdigo
sent {
strcpy ($$.var, $1.var);
strcpy( $$.etq, $1.etq);
printf(goto %s, $2.etq);
printf(label %s, $2.etq);
}
;
sent_ case : inicio_case OTHERWISE sent FIN CASE { printf(label %s, $1.etq); }
| inicio_case FIN CASE { printf(label %s, $1.etq); }
;
276
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
32 return NUMERO;
33 }
34 [A-Za-z_][A-Za-z0-9_]* {
35 strcpy(yylval.variable_aux, yytext);
36 return ID;
37 }
38 & \t]+
[b {;}
39 \n { linea_actual++; }
40 . { return yytext[0]; }
En cuanto al cdigo Yacc, es el siguiente:
1 /* Declaraciones de apoyo */
2 %{
3 typedef struct _doble_cond {
4 char etq_verdad[21],
5 etq_falso[21];
6 } doble_cond;
7 typedef struct _datos_case {
8 char etq_final[21];
9 char variable_expr[21];
10 } datos_case;
11 %}
12 /* Declaracion de atributos */
13 %union {
14 char numero[21];
15 char variable_aux[21];
16 char etiqueta_aux[21];
17 char etiqueta_siguiente[21];
18 doble_cond bloque_cond;
19 datos_case bloque_case;
20 }
21 /* Declaracion de tokens y sus atributos */
22 %token <numero> NUMERO
23 %token <variable_aux> ID
24 %token <etiqueta_aux> IF WHILE REPEAT
25 %token <etiqueta_siguiente> CASO
26 %token ASIG THEN ELSE FIN DO UNTIL CASE OF OTHERWISE
27 %token MAI MEI DIF
28 /* Declaracin de no terminales y sus atributos */
29 %type <variable_aux> expr
30 %type <bloque_cond> cond
31 %type <bloque_case> inicio_case
32 /* Precedencia y asociatividad de operadores */
33 %left OR
34 %left AND
35 %left NOT
36 %left '+' '-'
37 %left '*' '/'
38 %left MENOS_UNARIO
39
277
Generacin de cdigo
40 %%
41 prog : prog sent ';'
42 | prog error ';' { yyerrok; }
43 |
44 ;
45 sent : ID ASIG expr {
46 printf("\t%s = %s\n", $1, $3);
47 }
48 | IF cond {
49 printf("label %s\n", $2.etq_verdad);
50 }
51 THEN sent ';' {
52 nuevaEtq($1);
53 printf("\tgoto %s\n", $1);
54 printf("label %s\n", $2.etq_falso);
55 }
56 opcional
57 FIN IF {
58 printf("label %s\n", $1);
59 }
60 | '{' lista_sent '}' { ; }
61 | WHILE {
62 nuevaEtq($1);
63 printf("label %s\n", $1);
64 }
65 cond {
66 printf("label %s\n", $3.etq_verdad);
67 }
68 DO sent ';'
69 FIN WHILE {
70 printf("\tgoto %s\n", $1);
71 printf("label %s\n", $3.etq_falso);
72 }
73 | REPEAT {
74 nuevaEtq($1);
75 printf("label %s\n", $1);
76 }
77 sent ';'
78 UNTIL cond {
79 printf("label %s\n", $6.etq_falso);
80 printf("\tgoto %s\n", $1);
81 printf("label %s\n", $6.etq_verdad);
82 }
83 | sent_case
84 ;
85 opcional : ELSE sent ';'
86 | /* Epsilon */
87 ;
88 lista_sent : /* Epsilon */
278
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
279
Generacin de cdigo
280
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
281
Generacin de cdigo
8 return "etqL"+(++actualEtq);
9 }
10 %}
11 %unicode
12 %cup
13 %line
14 %column
15 %state COMENT
16 %%
17 ^[b& \t]*"*" { yybegin(COMENT); }
18 <COMENT>.+ {;}
19 <COMENT>\n { lineaActual ++; yybegin(YYINITIAL); }
20 "+" { return new Symbol(sym.MAS); }
21 "*" { return new Symbol(sym.POR); }
22 "/" { return new Symbol(sym.ENTRE); }
23 "-" { return new Symbol(sym.MENOS); }
24 "(" { return new Symbol(sym.LPAREN); }
25 ")" { return new Symbol(sym.RPAREN); }
26 "{" { return new Symbol(sym.LLLAVE); }
27 "}" { return new Symbol(sym.RLLAVE); }
28 ";" { return new Symbol(sym.PUNTOYCOMA); }
29 ":" { return new Symbol(sym.DOSPUNTOS); }
30 ":=" { return new Symbol(sym.ASIG); }
31 ">" { return new Symbol(sym.MAYOR); }
32 "<" { return new Symbol(sym.MENOR); }
33 "=" { return new Symbol(sym.IGUAL); }
34 ">=" { return new Symbol(sym.MAI); }
35 "<=" { return new Symbol(sym.MEI); }
36 "!=" { return new Symbol(sym.DIF); }
37 CASE { return new Symbol(sym.CASE); }
38 OF { return new Symbol(sym.OF); }
39 CASO { return new Symbol(sym.CASO, nuevaEtq()); }
40 OTHERWISE { return new Symbol(sym.OTHERWISE); }
41 REPEAT { return new Symbol(sym.REPEAT, nuevaEtq()); }
42 UNTIL { return new Symbol(sym.UNTIL); }
43 IF { return new Symbol(sym.IF, nuevaEtq()); }
44 THEN { return new Symbol(sym.THEN); }
45 ELSE { return new Symbol(sym.ELSE); }
46 WHILE { return new Symbol(sym.WHILE, nuevaEtq()); }
47 DO { return new Symbol(sym.DO); }
48 AND { return new Symbol(sym.AND); }
49 OR { return new Symbol(sym.OR); }
50 NOT { return new Symbol(sym.NOT); }
51 FIN { return new Symbol(sym.FIN); }
52 [:jletter:][:jletterdigit:]* { return new Symbol(sym.ID, yytext()); }
53 [:digit:]+ { return new Symbol(sym.NUMERO, yytext()); }
54 & \t\r]+ {;}
[b
55 [\n] { lineaActual++; }
56 . { System.out.println("Error lxico en lnea "+lineaActual+":-"+yytext()+"-"); }
282
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
283
Generacin de cdigo
284
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
90 :}
91 DO sent PUNTOYCOMA
92 FIN WHILE {:
93 System.out.println("\tgoto "+ etqInicioWhile);
94 System.out.println("label "+ c.etqFalso);
95 :}
96 | REPEAT:etqInicioRepeat {:
97 System.out.println("label "+ etqInicioRepeat);
98 :}
99 sent PUNTOYCOMA
100 UNTIL cond:c {:
101 System.out.println("label "+ c.etqFalso);
102 System.out.println("\tgoto "+ etqInicioRepeat);
103 System.out.println("label "+ c.etqVerdad);
104 :}
105 | sentCASE
106 ;
107 opcional ::= ELSE sent PUNTOYCOMA
108 | /* Epsilon */
109 ;
110 listaSent ::= /* Epsilon */
111 | listaSent sent PUNTOYCOMA
112 | listaSent error PUNTOYCOMA
113 ;
114 sentCASE ::= inicioCASE:ic OTHERWISE sent PUNTOYCOMA
115 FIN CASE {:
116 System.out.println("label "+ ic.etqFinal);
117 :}
118 | inicioCASE:ic
119 FIN CASE {:
120 System.out.println("label "+ ic.etqFinal);
121 :}
122 ;
123 inicioCASE ::= CASE expr:e OF {:
124 RESULT = new DatosCASE();
125 RESULT.tmpExpr = e.nombreVariable;
126 RESULT.etqFinal = nuevaEtq();
127 :}
128 | inicioCASE:ic
129 CASO:etqFinCaso expr:e DOSPUNTOS {:
130 System.out.println("\tif "+ ic.tmpExpr +" != "+ e
131 +" goto "+ etqFinCaso);
132 :}
133 sent PUNTOYCOMA {:
134 System.out.println("\tgoto "+ ic.etqFinal);
135 System.out.println("label "+ etqFinCaso);
136 RESULT = ic;
137 :}
138 ;
285
Generacin de cdigo
286
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
de la lnea pasa por utilizar estado lxicos que complican un poco la notacin, y que
obligan a declarar todos y cada uno de los tokens de la gramtica con el objetivo de
pasar al estado por defecto (DEFAULT) una vez reconocido cada uno de ellos. Esto
provoca, adems, que haya que comenzar el anlisis lexicogrfico con el estado lxico
INICIO_LINEA activado, lo que se encarga de hacer la funcin main() a travs de la
funcin SwitchTo de la clase TokenManager.
As, la solucin completa es:
1 PARSER_BEGIN(Control)
2 import java.util.*;
3 public class Control{
4
5 private static class BloqueCondicion{
6 String etqVerdad, etqFalso;
7 }
8 public static void main(String args[]) throws ParseException {
9 ControlTokenManager tm =
10 new ControlTokenManager(new SimpleCharStream(System.in));
11 tm.SwitchTo(tm.INICIO_LINEA);
12 new Control(tm).gramatica();
13 }
14 private static int actualTmp=0, actualEtq=0;
15 private static String nuevaTmp(){
16 return "tmp"+(++actualTmp);
17 }
18 private static String nuevaEtq(){
19 return "etq"+(++actualEtq);
20 }
21 private static void usarASIG(String s, String e){
22 System.out.println(s+"="+e);
23 }
24 private static String usarOpAritmetico(String e1, String e2, String op){
25 String tmp = nuevaTmp();
26 System.out.println("\t"+tmp+"="+e1+op+e2);
27 return tmp;
28 }
29 private static void usarLabel(String label){
30 System.out.println("label "+ label);
31 }
32 private static void usarGoto(String label){
33 System.out.println("\tgoto "+ label);
34 }
35 private static BloqueCondicion usarOpRelacional(String e1,
36 String e2,
37 String op){
38 BloqueCondicion blq = new BloqueCondicion();
39 blq.etqVerdad = nuevaEtq();
40 blq.etqFalso = nuevaEtq();
287
Generacin de cdigo
288
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
289
Generacin de cdigo
290
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
291
Generacin de cdigo
237 }
238 /*
239 condFact ::= (NOT)* ( condSimple | '[' cond ']' )
240 */
241 BloqueCondicion condFact():{
242 BloqueCondicion c1;
243 boolean negado = false;
244 }{ (<NOT> {negado = !negado;} )*
245 ( c1=condSimple()
246 | <LCOR> c1=cond() <RCOR>
247 ) { if (negado) intercambiarCondicion(c1);
248 return c1;
249 }
250 }
251 /*
252 condSimple ::= expr (('>'|'<'|'='|'>='|'<='|'!=') expr)*
253 */
254 BloqueCondicion condSimple():{
255 String e1, e2;
256 }{
257 e1=expr() (
258 <MAYOR> e2=expr() { return usarOpRelacional(e1, e2, ">"); }
259 | <MENOR> e2=expr() { return usarOpRelacional(e1, e2, "<"); }
260 | <IGUAL> e2=expr() { return usarOpRelacional(e1, e2, "="); }
261 | <MAI> e2=expr() { return usarOpRelacional(e1, e2, ">="); }
262 | <MEI> e2=expr() { return usarOpRelacional(e1, e2, "<="); }
263 | <DIF> e2=expr() { return usarOpRelacional(e1, e2, "!="); }
264 )
265 }
266 String id():{}{
267 <ID> { return token.image; }
268 }
269 String numero():{}{
270 <NUMERO> { return usarOpAritmetico(token.image, "", ""); }
271 }
Las funciones de las lneas 21 a 50 permiten clarificar las acciones semnticas
repartidas entre las reglas BNF. Por ltimo, las etiquetas declaradas en la lnea 127
podran haberse fusionado en una sola, ya que nunca van a utilizarse dos de ellas
simultneamente; no se ha hecho as para mejorar la legibilidad del cdigo.
292
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Captulo 9
Gestin de la memoria en tiempo de ejecucin
293
Gestin de la memoria en tiempo de ejecucin
9.2.1 Overlays
Algunos compiladores fragmentan el cdigo del programa objeto usando
overlays o solapas, cuando la memoria principal disponible es inferior al tamao del
programa completo. Estos overlays son secciones de cdigo objeto que se almacenan
en ficheros independientes y que se cargan en la memoria central dinmicamente, es
decir, durante la ejecucin del programa. Se les llama solapas porque dos overlays
pueden ocupar el mismo trozo de memoria en momentos de tiempo diferentes,
solapndose. Para hacer ms eficiente el uso de la memoria, los overlays de un
programa se agrupan en zonas y mdulos, cada uno de los cuales contiene un conjunto
de funciones o procedimientos completos. La figura 9.2 muestra cmo se ubican varios
294
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
295
Gestin de la memoria en tiempo de ejecucin
296
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
297
Gestin de la memoria en tiempo de ejecucin
(En algn momento debe haber una zona de memoria que contenga el valor
intermedio 15, y el valor intermedio 4 para sumarlos a continuacin). En caso
de utilizacin de memoria esttica, esta zona de temporales puede ser comn
a todo el programa, ya que su tamao puede deducirse en tiempo de
compilacin.
Q inicializa sus variables y comienza su ejecucin.
Dado que las variables estn permanentemente en memoria es fcil
implementar la propiedad de que conserven o no su contenido para cada nueva llamada
(comportamiento parecido al del modificador static del lenguaje C).
Figura 9.4. Estructura de un registro de activacin asociado a una funcin no recursiva. Cada
funcin o procedimiento tiene asociado un registro de activacin de tamao diferente, en
funcin de cuantas variables locales posea, del tipo del valor devuelto, etc.
298
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
Por esto, los distintos registros de activacin asociados a cada bloque debern colocarse
en una pila en la que entrarn cuando comience la ejecucin del bloque y saldrn al
terminar el mismo. La estructura de los registros de activacin vara de unos lenguajes
a otros, e incluso de unos compiladores a otros, siendo ste uno de los problemas por
los que a veces resulta difcil enlazar los cdigos generados por dos compiladores
diferentes. En general, los registros de activacin de los procedimientos suelen tener
algunos de los campos que pueden verse en la figura 9.5.
299
Gestin de la memoria en tiempo de ejecucin
PROCEDURE C2
VAR
C2_g, C2_h : INTEGER;
BEGIN
...
CALL B1;
...
END C2;
BEGIN
...
CALL C2;
...
END B2;
BEGIN (* Programa principal *)
...
CALL B2;
...
END;
El puntero al registro de activacin del padre se utiliza porque un
procedimiento puede hacer uso de las variables locales de los procedimientos en los
que se halla declarado. En este ejemplo, C2 puede hacer uso de las variables:
C2_g y C2_h (que son suyas).
B2_e y B2_f (que son de su padre, aqul en el que C2 est declarado).
A1_a y A1_b (que son del padre de su padre).
pero no puede hacer uso ni de B1_c ni de B1_d, ya que C2 no est inmerso en B1.
Al igual que en la zona de datos de tamao fijo, los registros de activacin
contienen espacio para almacenar los parmetros reales (valores asociados a las
variables que aparecen en la cabecera) y las variables locales, (las que se definen dentro
del bloque o procedimiento) as como una zona para almacenar el valor devuelto por
la funcin y una zona de valores temporales para el clculo intermedio de expresiones.
Cualesquiera dos bloques o procedimientos diferentes, suelen tener registros
de activacin de tamaos diferentes. Este tamao, por lo general, es conocido en
tiempo de compilacin ya que se dispone de informacin suficiente sobre el espacio
ocupado por los objetos que lo componen. En ciertos casos esto no es as como por
ejemplo ocurre en C cuando se utilizan arrays de longitud indeterminada. En estos
casos el registro de activacin debe incluir una zona de desbordamiento al final cuyo
tamao no se fija en tiempo de compilacin sino slo cuando realmente llega a
ejecutarse el procedimiento. Esto complica un poco la gestin de la memoria, por lo
que algunos compiladores de bajo coste suprimen esta facilidad.
A pesar de que es imprescindible guardar sucesivas versiones de los datos
segn se van realizando llamadas recursivas, las instrucciones que se aplican sobre
ellos son siempre las mismas, por lo que estas instrucciones slo es necesario
guardarlas una vez. No obstante, es evidente que el mismo cdigo trabajar con
diferentes datos en cada invocacin recursiva, por lo que el compilador debe generar
300
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
cdigo preparado para tal eventualidad, no haciendo referencia absoluta a las variables
(excepto a las definidas globalmente cuya ubicacin en memoria no cambia en el
transcurso de la ejecucin del programa), sino referencia relativa en funcin del
comienzo de su registro de activacin.
El procedimiento de gestin de la pila cuando un procedimiento P llama a otro
procedimiento Q, se desarrolla en dos fases; la primera de ellas corresponde al cdigo
que se incluye en el procedimiento P antes de transferir el control a Q, y la segunda,
al cdigo que debe incluirse al principio de Q para que se ejecute cuando reciba el
control. Un ejemplo de esta invocacin puede venir dado por un cdigo como:
PROCEDURE P( /* param. formales de P */ ) {
VAR
// var. locales de P
PROCEDURE Q( /* param. formales de Q */ ) {
VAR
// var. locales de Q
BEGIN
...
Q( ... ); // Invocacin recursiva de Q a Q
...
END Q;
BEGIN
...
x = Q( /* param. reales a Q */ ); // Invocacin de P a Q
END P;
La primera de estas fases sigue los siguientes pasos:
1.1 El procedimiento que realiza la llamada (P) evala las expresiones de la
invocacin, utilizando para ello su zona de variables temporales, y copia el
resultado en la zona correspondiente a los parmetros reales del
procedimiento que recibe la llamada. Previo a ello deja espacio para que Q
deposite el valor devuelto.
1.2 El llamador P coloca en la pila un puntero al registro de activacin del
procedimiento en el cual se halla declarado Q, con objeto de que ste pueda
acceder a las variables no locales declaradas en su padre. Por ltimo, P
transfiere el control a Q, lo que hace colocar la direccin de retorno encima
de la pila.
1.3 El receptor de la llamada (Q) salva el estado de la mquina antes de
comenzar su ejecucin usando para ello la zona correspondiente de su
registro de activacin.
1.4 Q inicializa sus variables y comienza su ejecucin.
Al terminar Q su ejecucin se desaloja su registro de activacin procediendo
tambin en dos fases. La primera se implementa mediante instrucciones al final del
procedimiento que acaba de terminar su ejecucin (Q), y la segunda en el
procedimiento que hizo la llamada (P), tras recobrar el control:
2.1 El procedimiento saliente (Q) antes de finalizar su ejecucin coloca el valor
301
Gestin de la memoria en tiempo de ejecucin
302
Java a tope: Traductores y compiladores con Lex/Yacc, JFlex/Cup y JavaCC
303
Gestin de la memoria en tiempo de ejecucin
informtica. A este respecto, las tcnicas suelen dividirse en dos grandes grupos:
aqullas en las que se aloja exactamente la cantidad de memoria solicitada (favorecen
la fragmentacin), y aqullas en las que el bloque alojado es de un tamao parecido,
pero generalmente superior, al solicitado, lo que permite que el heap slo almacene
bloques de determinados tamaos prefijados en el momento de construir el compilador
(favorece la desfragmentacin).
Para finalizar, una de las caractersticas ms interesantes de lenguajes
imperativos muy actuales, como Java, es que incluyen un recolector automtico de
basura (garbage collection). El recolector de basura forma parte del gestor del heap,
y lleva el control sobre los punteros y los trozos de memoria del heap que son
inaccesibles a travs de las variables del programa, ya sea directa o indirectamente, esto
es, de los trozos de memoria alojados pero que no estn siendo apuntados por nadie y
que, por lo tanto, constituyen basura. Mediante este control pueden efectuar de manera
automtica las operaciones de desalojamiento, liberando al programador de realizar
explcitamente esta accin. Entre las tcnicas utilizadas para implementar los
recolectores de basura podemos citar los contadores de referencia, marcar y barrer,
tcnicas generacionales, etc.
304