Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

582

Click here to load reader

Transcript of Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Page 1: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Contenido

¿Por qué compiladores? Una breve historia 2 Programas relacionados con los compiladores 4 Proceso de traducción 7 Estructuras de datos principales en un compilador 13 Otras cuestiones referentes a la estructura del compilador 14 Arranque automático y portabilidad 18 Lenguaje y compilador de muestra TINY 21 C-Minus: Un lenguaje para un proyecto de compilador 26 Ejercicios 27 Notas y referencias 29

2.1 El proceso del análisis Iéxico 32 2.2 Expresiones regulares 34 2.3 Autómatas finitos 47 2.4 Desde las expresiones regulares hasta los DFA 64 2.5 Implementación de un analizador Iéxico TINY ("Diminuto") 75 2.6 Uso de Lex para generar automáticamente un analizador Iéxico 8 1

Ejercicios 9 1 Ejercicios de programación 93 Notas y referencias 94

3 GRAMATICAS LIBRES DE CONTEXTO Y ANÁLISIS SINTACTICO 95 3.1 El proceso del análisis sintáctico 96 3.2 Gramáticas libres de contexto 97 3.3 Árboles de análisis gramatical y árboles sintácticos abstractos 106 3.4 Ambigüedad 1 14 3.5 Notaciones extendidas: EBNF y diagramas de sintaxis 123 3.6 Propiedades formales de los lenguajes libres de contexto 128 3.7 Sintaxis del lenguaje TlNY 133

Ejercicios 138 Notas y referencias 142

Page 2: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

4 ANALISIS S~NTACT~CO DESCENDENTE 143 4.1 Análisis sintáctico descendente mediante método

descendente recursivo 144 4.2 Análisis sintáctico LL(I) 152 4.3 Conjuntos primero y siguiente 168 4.4 Un analizador sintáctico descendente recursivo para

el lenguaje TINY 180 4.5 Recuperación de errores en analizadores sintácticos

descendentes 183 Ejercicios 189 Ejercicios de programación 193 Notas y referencias 196

5 ANALISIS SINTACTICO ASCENDENTE 197 Perspectiva general del análisis sintáctico ascendente 198 Autómatas finitos de elementos LR(0) y análisis sintáctico LR(0) 20 1 Análisis sintáctico SLR( I ) 2 10 Análisis sintáctico LALR(l) y LR(I) general 217 Yacc: un generador de analizadores sintácticos LALR(I) 226 Generación de un analizador sintáctico TlNY utilizando Yacc 243 Recuperación de errores en analizadores sintácticos ascendentes 245 Ejercicios 250 Ejercicios de programación 254 Notas y referencias 256

Atributos y gramáticas con atributos 259 Algoritmos para cálculo de atributos 270 La tabla de símbolos 295 Tipos de datos y verificación de tipos 3 13 Un analizador semántico para el lenguaje TlNY 334 Ejercicios 339 Ejercicios de programación 342 Notas y referencias 343

7 AMBIENTES DE EJECUCION 3 4 5 7.1 Organización de memoria durante la ejecución del programa 346 7.2 Ambientes de ejecución completamente estáticos 349 7.3 Ambientes de ejecución basados en pila 352 7.4 Memoria dinámica 373 7.5 Mecanismos de paso 'de parámetros 381

Page 3: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CONTENIDO

7.6 Un ambiente de ejecución para el lenguaje TlNY 386 Ejercicios 388 Ejercicios de programación 395 Notas y referencias 396

8.1 Código intermedio y estructuras de datos para generación de código 398

8.2 Técnicas básicas de generación de código 407 8.3 Generación de código de referencias de estructuras de datos 41 6 8.4 Generación de código de sentencias de control y expresiones

lógicas 428 8.5 Generación de código de llamadas de procedimientos

y funciones 436 8.6 Generación de código en compiladores comerciales:

das casos de estudio 443 8.7 TM: Una máquina objetivo simple 453 8.8 Un generador de código para el lenguaje TlNY 459 8.9 Una visión general de las técnicas de optimización de código 468 8.1 O Optimizaciones simples para el generador de código de TlNY 48 1

Ejercicios 484 Ejercicios de programación 488 Notas y referencias 489

Apéndice A: PROYECTO DE COMPILADOR 491 A. I Convenciones Iéxicas de C- 49 1 A.2 Sintaxis y semántica de C- 492 A.3 Programas de muestra en C- 496 A.4 Un ambiente de ejecución de la Máquina Tiny

para el lenguaje C- 497 AS Proyectos de programación utilizando C- y TM 500

Apéndice 1: LISTADO DEL COMPILADOR TINY 502

ApéndiceC: LISTADO DEL SIMULADOR DE LA MAQUINA TlNY 545

Bibliografía 558

Page 4: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Prefacio

Este libro es una introducción al campo de la constmcción de compiladores. Combina un es- tudio detallado de la teoría subyacente a1 enfoque moderno para el diseño de compiladores, junto con muchos ejemplos prácticos y una descripción completa, con el código fuente, de un compilador para un lenguaje pequeño. Está específicamente diseñado para utilizarse en un curso introductorio sobre el diseño de compiladores o construcción de compiladores a un nivel universitario avanzado. Sin embargo, también será de utilidad para profesio- nales que se incorporen o inicien un proyecto de escritura de compilador, en la medida en que les dará todas las herramientas necesarias y la experiencia práctica para diseñar y pro- gramar un compilador real.

Existen ya muchos textos excelentes para este campo. ¿Por qué uno más'? La respuesta es que la mayoría de los textos actuales se limita a estudiar uno de los dos aspectos irnpor- [antes de la construcción de compiladores. Una parte de ellos se restringe a estutli:~r la teo- ría y los principios del diseño de compiladores, y sólo incluye ejemplos breves dc I:i aplica- ción de la teoría. La otra parte se concentra en la meta práctica de producir un compilador real, ya sea para un lenguaje de programación real, o para una versión reducida de alguno, e incursiona sólo superficialmente en la teoría en la cual se basa el código para explicar su origen y comportamiento. Considero que ambos enfoques tienen carencias. Para cornpreu- der en realidad los aspectos prácticos del diseño de compiladores, se necesita h. b er com- prendido la teoría, y para apreciar realmente la teoría, se requiere verla en acción en una configuración práctica real o cercana a la realidad.

Este texto se encarga de proporcionar el balance adecuado entre la teoría y la práctica, y de suministrar suficientes detalles de impiementación real para ofrecer una visión real de las técnicas sin abrumar al lector. En este texto proporciono un compilador completo para un lenguaje pequeño escrito en C y desarrollado utilizando las diferentes técnicas estudia- das en cada capítulo. Además, ofrezco descripciones detalladas de técnicas de codificación para ejemplos adicionales de lenguajes a medida que se estudian los temas relacionados. Finalmente, cada capítulo concluye con un extenso conjunto de ejercicios, que se dividen en dos secciones. La primera contiene los diversos ejercicios que se resuelven con pavel Y lá- . . , piz y que implican poca prograkaciítn. La segunda contiene aquellos que involucran una cantidad importante de programación.

Al escribir un texto así se debe tener en cuenta los diferentes sitios que ocupa un curso de compiladores en diferentes currículos de ciencias de la comput;ición. En cilgnnos pro- gramas se requiere haber tornado un curso de teoría de autómatas; en otros, uno sobre len- guajes de programaci6n; mientras que en otros es suficiente con haber tomado el curso de estructuras de datos. Este texto sólo requiere que el lector haya tomado el curso hahitual sohre estructuras de datos y que conozca un poco del lenguaje C; incluso csth pl:~nccrdo

Page 5: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Introducción

¿Por que compiladores? Una 1 .S Otras cuestiones referentes a breve historia la estructura del compilador Programas relacionados con 1.6 Arranque automático y los compiladores portabilidad Proceso de traducción 1.7 Lenguaje y compilador de

Estructuras de datos principales muestra TlNY en un compilador 1.8 C-Minus: un lenguaje para un

proyecto de compilador

Los compiladores son programas de computadora que traducen un lenguaje a otro. Un compilador toma como su entrada un programa escrito en su lenguaje fuente y produce un programa equivalente escrito en su lenguaje objetivo. Por lo regular, el lenguaje fuente es un lenguaje de alto nivel, tal como C o C++, mientras que el lenguaje obje- tivo es código objeto (también llamado en ocasiones código de máquina) para la máquina objetivo, es decir, código escrito en las instrucciones de máquina correspondien- tes a la computadora en la cual se ejecutará. Podemos visualizar este proceso de manera esquemática como sigue:

Programa - Programa fuente objetivo

Un compilador es un programa muy complejo con un número de líneas de código que puede variar de i 0,000 a 1,000,000. Escribir un programa de esta naturaleza, o incluso comprenderlo, no es una tarea fácil, y la mayoría de los científicos y profesionales de la computación nunca escribirán un compilador completo. N o obstante, los compiladores se utilizan en casi todas las formas de la computación, y cualquiera que esté involucrado profesionalmente con las computadoras debería conocer la organización y el funciona- miento básicos de un compilador. Además, una tarea frecuente en las aplicaciones de las computadoras es el desarrollo de programas de interfaces e intérpretes de comandos, que son más pequeños que los compiladores pero utilizan las mismas técnicas. Por lo tanto, el conocimiento de estas técnicas tiene una aplicación práctica importante.

El propósito de este texto no sólo es el de proporcionar este conocimiento básico. sino también el de ofrecer al lector todas las herramientas necesarias, así como la expe- riencia práctica, para diseñar y programar un compilador real. Para conseguir esto es

Page 6: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

necesario estudiar las técnicas teóricas, principalmente las provenientes de la teoria de los autómatas, que hacen de la construcción de compiladores una tarea manejable. Al abordar esta teoria evitaremos suponer que el lector tiene un conocimiento previo de la teoria de autómatas. En vez de eso aquí aplicaremos un punto de vista diferente al que se aplica en un texto estándar de teoria de autómatas. en el sentido de que éste está dirigido especificamente al proceso de compilación. Sin embargo, un lector que haya estudiado teoría de autómatas encontrará el material teórico bastante familiar y podrá avanzar más rápidamente a través de estas secciones. En particular, un lector que tenga buenos funda- mentos en la teoría de autómatas puede saltarse las secciones 2.2, 2.3. 2.4 y 3.2 o revi- sarlas superficialmente. En cualquier caso, el lector debería estar familiarizado con mate- máticas discretas y estructuras básicas de datos. También es esencial que conozca un poco de arquitectura de máquinas y lenguaje ensamblador. en particular para el capitulo sobre la generación de código.

El estudio de las técnicas de codificación práctica en sí mismo requiere de una cuida- dosa planeación, ya que incluso con buenos fundamentos teóricos los detalles de la codi- ficación pueden ser complejos y abrumadores. Este texto contiene una serie de ejemplos simples de construcciones de lenguaje de programación que se utilizan para elaborar el análisis de las técnicas. El lenguaje que empleamos para éste se denomina TINY. También proporcionamos (en el apéndice A) un ejemplo más extenso, que se compone de un subconjunto pequeño, pero suficientemente complejo, de C, que denominamos C-Minus, el cual es adecuado para un proyecto de clase. De manera adicional tenemos numerosos ejercicios; éstos incluyen ejercicios simples para realizar con papel y lápiz. extensiones del código en el texto y ejercicios de codificación más involucrados.

En general, existe una importante interacción entre la estructura de un compilador y el diseño del lenguaje de programación que se está compilando. En este texto sólo estu- diaremos de manera incidental cuestiones de diseño de lenguajes. Existen otros textos disponibles para profundizar mas en las cuestiones de diseño y conceptos de lenguajes de programación. (Véase la sección de notas y referencias al final de este capitulo.)

Comenzaremos con una breve revisión de la historia y razón de ser de los compila- dores, junto con una descripción de programas relacionados con ellos. Después examinare- mos la estructura, mediante un ejemplo concreto simple, de un compilador y los diversos procesos de traducción y estructuras de datos asociadas con él. Al final daremos una perspectiva general de otras cuestiones relacionadas con la estructura de compiladores, incluyendo el arranque automático de transferencia ("bootstrapping") y portabilidad, para concluir con una descripción de los principales ejemplos de lenguajes que se emplean en el resto del libro.

1.1 iPOR QUE COI1PILADORES? UNA BREVE HISTORIA Con el advenimiento de la computadora con programa almacenado, iniciado por John von Neumarin a finales de la década de 1940, se hizo necesario escribir secuencias de códigor, o programas, que darían como resultado que estas computadoras realizaran los cálculos de- seados. A l principio estos programas se escribían en lenguaje de máquina: códigos nuni6- ricos que rcpresentaban las operaciones reales de la iiváquina que iban a ckctiiarse. Por +inplo,

representa la instrucción parti mover el nílrnertt 2 a la i~hicacióri 0000 (en sistenia hex;~<lecirii;il I en los procesa<lores Iritel 8x86 que se u ~ i l i m t en las PC cle IBM. Por wpuesto, I;i escrilim [le tales c6cligos es muy tediosa y corisiiiirc niucho tiempo, por Itr que esta tbrri~:i ilc coilii i c:ici<ín pronto t'iw rccniplazaJa por cl Ienguzije ensamblador, m el cu;iI I;is i~~si~-~~cciotic:s y

Page 7: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

¿Por que compiladores? Una breve historia

las localidiides de memoria son li~rniits sinibólicas dadas. Por tjernplo, la instrucción en len. p a j e ensnmbl;idor

MOV X , 2

es equiv;ilente a la instriiccicin de máquina anterior (suponiendo que la localidad de memo. ria sirnbcilicit X cs 0000). Un ensamhlador traduce los códigos simbólicos y las localidadc. de rriemoria del lenguaje enwniblxior a los códigos numéricos correspondientes del lengua je de máquina.

El lenguaje ensarnblador mejoró enortncmente la velocidad y exactitud con la que po- dían escribirse los programas, y en la actualidad todavía se encuentra en LISO, cn especia cuando se necesita una gran velocidad o brevedad en el código. Sin embargo, el Ienguajt etisamblador tiene varios defectos: aún no es fácil de escribir y es difícil de leer y cori i~ prender. Adeiiiás, el lenguaje ensamblador depende ni extremo de la niáquina en particda1 para la cual se haya escrito, de manera que el c d i g o escrito para una conip~itatlora debc volver :i escribirse por completo para otra máquina. corno es evidente, el siguiente paso t'trn- damental en la tecnología de progriirnación fue escribir las operaciones de un programa dt una manera concisa que se pareciera mucho a la notación niatemática o lenguaje natural de manera que fueran indepentlientes de cualquier máquina en particular y todavía se p~idierar traducir niediante iin programa para convertirlas en código ejecut&le. Por ejemplo, el aiitc- rior código del Lenguaje ensarnblacior se ptiede escribir de manera concisa e independiente de una máquina en particular corno

A l principio se temía que esto no fuera posible, o que s i lo fuera, el código objeto sería tan poco eficiente que restiltaría inútil.

El desarrollo del lenguaje FORTRAN y su compil:rdor, llevado a cabo por un equipo en IBM dirigido por John Backus entre 1954 y 1957 demostró que estos teniores eran infunda- dos. No obstmte, el éxito de este proyecto se debió sólo a un gran esf~ierzo. ya que la ma- yoría de los procesos involucr~dos en la traducción de lenguajes de programación no fueroti bien comprendidos en el momento.

Más o rne'nos al mismo tiempo en que el primer compilador se estaba desarrollando, Noain Chornsky corrienzó a estudiar la estructura del lenguaje natural. Sus hallazgos fiiial- riien- hicieron que la construcción de compiladores se volviera mucho más fácil e incluso pudiera ser automatizado hasta cierto punto. Los estudios de Chonisky condujeron a la cla- sificación de los lengmjes de acuerdo con la complejidad de sus gramáticas (las reglas que especifica~i su estructura) y la potencia de los algoritmos necesarios para reconocerlas. La jerarquía de ~ h o m s k y , como ahora se le conoce, se compone de cuatro niveles de gra- máticas, denominadas gramáticas tipo O, tipo 1, tipo 2 y tipo 3, cada una de las cuales es tina especialización (le su predecesora. Las gramáticas de tipo 2, o gramáticas libres de contexto, demostraron ser las más útiles para lenguajes de programación, en la actualidad son la manera estándar para representar la estructura de los lenguajes de programación. El estiidio del problema del análisis sint&ctico (la determinación de algoritmos eficientes pa- ra e1 reconocimiento de lenguajes libres de contexto) se llevó a cabo en las décadas de los 60 y 70 y condujo a una solución muy completa de este probleina, que en la actualidad se ha vuelto una parte estándar de la teoría de compiladores. Los lenguajes libres de contexto y los algoritmos de análisis sintáctico e estudiart eii los capítulos 3, 4 y 5.

Los autbmatas finitos y las expresiones regulares, que corresponden a Ins gr;iniiticas de tipo 3 de Chomsky, se encuentran estrechmente relacionados con las gr;~ni;íticas libres de contexto. A l comenzar a generarse casi al iiiisino ticnipo que el trahnjo de Chornsky, su estudio contlrfjo a inét«dos siiiibtilicos p x n expresar la estructur:i de las p;tlabrns. o tokens, Jc un Icnguaje de prognini:tcióri. En el capítulo 2 se an;ilizaii los :iutcírri;itas ririitos y las expresiones regulares.

Page 8: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. I 1 INTRODUCCldN

h<ucho más complejo ha sido el desarrollo de métodos para la generación de código objeto eficaz, que comenzó con 10s primeros compiladores y continúa hasta nuestros días. Estas técnicas suelen denominarse, incorrectamente, técnicas de optimización, pero en rea- lidad deberían llarnarse técnicas de mejoramiento de código, puesto que casi nunca pro- ducen un código objeto verdaderamente óptirno y sólo mejoran su eficacia. En el capítulo 8 se describen los fundamentos de estas técnicas.

A medida que el problema del análisis sintáctico se comprendía bien, se dedicó mucho trabajo a desarrollar programas que automatizarían esta pMe del desarrollo de compilado- res. Estos programas originalmente se llamaron compiladores de compilador, pero se hace referencia a ellos de manera más acertada como generadores de analizadores sintácticos, ya que automatizan sólo una parte del proceso dc compilación. El más conocido de estos programas es Yacc (por las siglas del término en inglés "yet another compiler-compiler", "otro compilador de compilador más"), el cual fue escrito por Steve Johnson en 1975 para el sistema Unix y se estudiará en el capítulo 5. De manera similar, el estudio de los autómatas finitos condujo al desarrollo de otra herramienta denominada generador de rastreadores (O generador de analizadores léxicos), cuyo representante más conocido es Lex (desarro- llado para el sistema Unix por Mike Lesk más o menos al nlismo tiempo que Yacc). Lex se estudiará en el capítulo 2.

A fines de los 70 y principios de los 80 diversos proyectos se enfocaron en automatizar la generaciún de otras partes de un compiiador, incluyendo La generación del c6digo. Estos inten- tos han tenido menos éxito, posiblemente debido a la naturaleza compleja de las operaciones y a nuestra poca comprensión de las mismas. No las estudiaremos con detalle en este texto.

Los avances más recientes en diseño de compiladores han incluido lo siguiente. En pri- mer lugar, los compiladores han incluido la aplicación de algoritmos más sofisticados para inferir y10 simplificar la información contenida en un programa, y éstos han ido de la mano con el desarrollo de lenguajes de progranación más soí'isticados que permiten esta clase de análisis. Un ejemplo típico de éstos es el algoritmo de unificación de verificación de tipo de Hindley-Milner, que se utiliza en la compilación de lenguajes funcionales. En segundo lu- gar, los compiladores se han vuelto cada vez más una parte de un ambiente de desarrollo interactivo, o IDE (por sus siglas en inglés de "interactive development environment"), basado en ventanas, que incluye editores, ligadores, depuradores y administradores de proyec- tos. Hasta ahora ha habido escasa estandariración de estos IDE, pero el desarrollo de ambien- tes de ventanas estándar nos está conduciendo en esa dirección. El estudio de estos temas rebasa el alcance de este texto (pero véase la siguiente sección para una breve descripción de algunos de los componentes de un IDE). Para sugerencias de literatura relacionada véase la sección de notas y referencias al final del capítulo. Sin embargo, a pesar de la cantidad de ac- tividades de investigación en los últimos años, los fundamentos del diseño de compiladores no han cambiado demasiado en los últimos veinte años, y se han convertido cada vez más en una parte del núcleo estándar en los currículos para las ciencias de la computación.

1.2 PROGRAMAS RELACIONADOS CON LOS COMPILADORES En esta sección describiremos brevemente otros programas que están relacionados, o que se utilizan, con los compiladores y que con frecuencia vienen junto con ellos en un ambiente de desarrollo de lenguaje completo. (Ya mencionamos algunos.)

INTÉRPRETES Un intérprete es un traductor de lenguaje, igual que u n compiiador, pero diticre de éste en que ejecuta el programia fuente inmediatamente, en vez de generar un código objeto que se ejecuta después de que se completa la traducción. En principio, cualquier lenguaje de programacióii se puede interpretar o compilar, pero se puedc preferir un intérprete a u11 cotnpilador dependiendo tlcl Icngnaje que se esté usando y de la situación en la cucil

Page 9: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Programar relacionados ton los compiiadorer 5

se presenta la traducción. Por ejemplo, BASIC es un lenguaje que por lo regular es in- terpretado en vez de compilado. De manera similar, los lenguajes funcioriales, corno LISP, tienden a ser interpretados. Los intérpretes también se utilizan con frecuencia en situaciones relacionadas con la enseñanza o con el desarrollo de software, donde los pro- gramas soti probablemente traducidos y vueltos a traducir muchas veces. Por otra parte, es preferible usar un compilador si lo que importa es la velocidad de ejecución, ya que el código objeto compilado es siempre más rápido que el código fuente interpretado, en ocasiones hasta por un factor de 10 o más. No obstante, los interpretes comparten muchas de sus operaciones con los compiladores, y ahí pueden incluso ser traductores híbri- dos, de manera que quedan en alguna parte entre los intérpretes y los compiladores. Analizaremos a los intérpretes de manera ocasional, pero en este texto nos enfocaremos principalmente en la compilación.

ENSAMBLADORES Un ensamblador es un traductor para el lenguaje ensamblador de una computadora en particular. Como ya lo advertimos, el lenguaje ensamblador es una forma simbólica del lenguaje de máquina de la computadora y es particularmente fácil de traducir. En oca- siones un compilador generará lenguaje ensamblador como su lenguaje objetivo y de- penderá entonces de un ensamblador para terminar la traducción a código objeto.

LIGADORES 'Tanto los compilridores como los ensambladores a menudo dependen de un programa co- nocido como ligador, el cual recopila el código que se compila o ensambla por separado en diferentes archivos objeto, a un archivo que es directamente ejecutable. En este senti- do, puede hacerse una distinción entre código objeto (código de máquina que todavía no se ha ligado) y código de ináquina ejecutable. Un ligador también conecta un programa objeto con el código de funciones de librerías estándar, así como con recursos suminis- trados por el sistema operativo de la computadora, tales como asignadores de memoria y dispositivos de entrada y salida. Es interesante advertir que los ligadores ahora realizan la tarea que originalmente era una de las principales actividades de un compilador (de aquí el uso de la palabra cotnpilrdor: construir mediante la recopilación o cornuilczcicín de fuen- tes diferentes). En este texto no estudiaremos el proceso de ligado porque depende dema- siado de los detalles del sistema operativo y del procesador. Tampoco haremos siempre una distinción clara entre el código objeto no ligado y el código ejecutable, porque esta distinción no tiene importancia para nueitro estudio de las técnicas de compilación.

CARGADORES Con frecuencia un compilador, ensamblador o ligador producirá un código que todavía no está completamente organizado y Itsto para ejecutarse, pero cuyas principales referencias de memoria se hacen relativas a una localidad de arranque indeterminada que puede estar en cualquier sitio de la memorka. Se dice que tal código es relocalizable y un cargador re- $olverá tadas las direcciones relocalizables relativas a una dirección baae, o de inicio, dada. El uso de un cargador hace más flexible el código ejecutable, pero el proceso de carga con frecuencia ocurre en segundo plano (como parte del entorno operacional) o coniuutamente con el ligttdo. Rara vez un cargador es en realidad un programa por separado.

PREPROCESADORES Un preprocesador es un programa separado que es invocado por el compilador antes de que comience la traducción real. Un preprocesador de este tipo puede eliminar los co- mentarios, incluir otros archivos y ejecutar sustituciones de macro (una macro es una descripción abreviada de una secuencia repetida de texto). Los preproccsadorcs pueden ser requeridos por el lenguaje (como en C) o pueden ser :tgregados posteriores que pro- porcioricn facilidades xlicion:tles (como el prcproccs;~lor R;itfor para I'OKTRAN).

Page 10: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. I i INTRODUCCIÓN

EDITORES Los compiladores por lo regular aceptan programas fuente escritos utilizando c~ialquier editor que' pueda producir un archivo estándar, tal como un archivo ASCII. Mis recien- teniente, los compiladores han sido integrados junto con editores y otros programas en u n ambiente de desarrollo interactivo o IDE. En un caso así, un editor, mientras que aún produce archivos esthndar, puede ser orientado hacia el forniato o estructura del lengua- je de programación en cuestión. Tales editores se denominan basados en estructura y ya incluyen algunas de las operaciones de un compilador, de manera que, por ejemplo, pueda informarse al programador de los errores a medida que el programa se vaya es- cribiendo en lugar de hacerlo cuando está compilado. El coinpilador y sus programas acompañantes también pueden llamarse desde el editor, de modo que el prograinador ptieda ejecutar el programa sin tener que abandonar el editor.

DEPURADORES Un depurador es un programa que puede utilizarse para determinar los errores de ejecu- ción en un programa compilado. A menudo está integrado con un compilador en un IDE. La ejecución de un programa con un depurador se diferencia de la ejecución direc- ta en que el depurador se mantiene al tanto de la mayoría o la totalidad dc la inforina- ción sobre el código fuente, tal conlo los números de línea y los nombres de las varia- bles y procediniientos. También puede detener la ejecución en ubicaciones previamente especificadas denominadas puntos de ruptura, ademhs de proporcionar información de cuáles funciones se han invocado y cuhles son Los valores actuales de las variables. Para efectuar estas funciones el conipilador debe suministrar al deptirador la información sim- bólica ;ipropiada, lo cual en ocasiones puede ser difícil, en especial en un conrpilador que intente optimizar el código objeto. De este modo, la depuración se convierte en una cuestibn de conipilación, la que, sin embargo, rebasa el alcance de este libro.

PERFILADORES Un perfilador es un programa que recolecta estadísticas sobre el comportamiento de un programa objeto durante la ejecución. Las estadísticas típicas que pueden ser de interés para el programador son el número de veces que se llama cada procedimiento y el por- centaje de tiempo de ejecución que se ocupa en cada uno de ellos. Tales estadísticas pue- den ser muy útiles para ayudar al programador a mejorar la velocidad de ejecución del progrrnna. A veces el compilador utilizará incluso la salida del perfilador para mejorar de manera automática el código objeto sin la intervención del programador.

ADMINISTRADORES DE PROYECTO Los modernos proyectos de softwae por lo general son tan grandes que tienen que ser eniprendidos por grupos de programadores en lugar de por un solo programador. En ta- les casos es importante que los archivos que se esthn trabajando por personas distintas se encuentren coordinados, y este es el trabajo de un programa de administración de proyectos. Por ejemplo, un administrador de proyecto debería coordinar la mezcla de di- ferentes versiones del mismo archivo producido por programadores diferentes. También debería mantener una historia de las modificaciones par+ cada uno de los grupos de archivos, de modo que puedan mantenerse versiones coherentes de un programa en desarrollo (esto es algo que también puede ser útil en un proyecto que lleva a cabo un solo progr;iinador). Un administrador de proyecto puede escribirse en una forma inde- pendiente del lenguaje, pero cuando se integra junto con un coinpiliidor, p~icde niante- ner información acerca del compilador específico y las operaciones de ligado necesarias para construir un programa -jccutable completo. Dos programas populares de admi- nistración de proyectos en sistenias Unix son sccs y rcs (source code control system, L. . ststcina de control para cbrligo L'ucnte") y (revision control system, "sistenta de coiitrol p r : ~ rcvisi(ín"~,

Page 11: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Proceso de traducción 7

1.3 PROCESO DE TRADUCCIÓN Un compilador se compone internamente de vanas etapas, o fases, que realizan distintas operaciones lógicas. Es útil pensar en estas fases como en piezas separadas dentro del com- pilador, y pueden en realidad escribirse como operaciones codificadas separadamente aun- que en la práctica a menudo se integren juntas. Las fases de un compilador se ilustran en la figura 1.1, junto con los tres componentes auxiliares que interactúan con alguna de ellas o

Figura 1.1 fases de un compilador

Código objetivo

Page 12: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. I 1 INTRODUCClON

con todas: la tabla de literales, la tabla de símbolos y el manejador de errores. Aquí descri- biremos brevemente cada una de las fases, las cuales se estudiwiin con más detalle en los capítulos siguientes. (Las tablas de literales y de símbolos se analizarán m6s"ampliamente en la siguiente sección y el manejador de errores en la sección 1 S.)

ANALIZADOR LÉXICO O RASTREADOR (SCANNER) Esta fase del compilador efectúa la lectura real del programa fuente, el cual general- mente está en la forma de un flujo de caracteres. El rastreador realiza lo que se cono- ce como análisis léxico: recolecta secuencias de caracteres en unidades significativas denominadas tokens, las cuales son como las palabras de un lenguaje natural, como el inglés. De este modo, se puede imaginar que un rastreador realiza una función similar al deletreo.

Como ejemplo, considere la siguiente línea de código, que podría ser parte de un programa en C:

Este código contiene 12 caracteres diferentes de un espacio en blanco pero sólo 8 tokens:

a identificador

I corchete izquierdo

index identificador

1 corchete derecho - asignación

4 número

+ signo más

2 número

Cada token se compone de uno o más caracteres que se reúnen en una unidad antes de que ocurra un procesamiento adicional.

Un analizador léxico puede realizar otras funciones junto con la de reconocimiento de tokens. Por ejemplo, puede introducir identificadores en la tabla de símbolos, y puede introducir literales en la tabla de literales (las literales incluyen constantes nu- méricas tales como 3.1415926535 y cadenas de texto entrecomilladas como "iHola, mundo!").

ANALliADOR SINT~CTICO (PARSER) El analizador sintáctico recibe el código fuente en la forma de tokens proveniente del analizador léxico y realiza el análisis sintáctico, que determina la estructura del progra- ma. Esto es semejante a realizar el análisis gramatical sobre una frase en un lenguaje na- tural. El análisis sintáctico determina los elementos estructurales del programa y sus re- laciones. Los resultados del análisis sintáctico por lo regular se representan como un árbol de análisis gramatical o un árbol sintáctico.

Como ejemplo, consideremos otra vez la línea de código en C que ya habíamos dado. Repreienta un elemento estructural denominado expresión, la cual es una expresión de aqignación compuesta de una expresión con subíndice a la izquierda y una expre~ión aritmética entera a la derecha. Esta estructura se puede representar como un jrbol de anlílisis gramatical de la forma siguiente:

Page 13: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Proceso de traducción

expresión

expresión de subíndice e.xpresión uditiva

expresión [ expresión 1 upraión + e.rpresión

I I identificador identificador número número

a index 4 2

Advierta que los nodos Internos del árbol de análisis gramatical están etiquetados con los nombres de las estructuras que representan y que las hojas del árbol representan la secuencia de tokens de la entrada. (Los nombres de las estructuras están escritos en un tipo de letra diferente para distinguirlos de los tokens.)

Un árbol de análisis gramatical es un auxiliar útil para visualizar la sintaxis de un programa o de un elemento de programa, pero no es eficaz en su representación de esa estructura. Los analizadores sintácticos tienden a generar un árbol sintáctico en su lugar, el cual es una condensación de la información contenida en el árbol de análisis gramati- cal. (En ocasiones los árboles sintácticos se denominan árboles sintácticos abstractos porque representan una abstracción adicional de los árboles de análisis gramatical.) Un árbol sintáctico abstracto para nuestro ejemplo de una expresión de asignación en C es el siguiente:

erpresión de asignación

/-" expresión de subíndice

\ exprestón aditiva

/ \ identificador identi ficador número número

/ \ a index 4 2

A árbol sintáctico muchos de los nodos han desaparecido (incluyendo los nodos de tokens). Por ejemplo, si sabemos que una expresión es una operación de subíndice, entonces ya no será necesario mantener los paréntesis cuadrados [ y ] que representan esta operación en la entrada original.

ANALIiADOR SEM~NTICO La semántica de un programa es su "significado", en oposición a su sintaxis, o estruc- tura. La semántica de un programa determina su comportamiento durante el tiempo de ejecución, pero la mayoría de los lenguajes de programación tienen características que se pueden determinar antes de la ejecución e incluso no se pueden expresar de manera adecuada como sintaxis y analizarse mediante el analizador sintáctico. Se hace referencia

Page 14: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

a tales características como semántica estática, y el análisis de tal semántica es la tarea del analizador semántico. (La semántica "dinámica" de un programa, es decir, aquellas propiedades del programa que solamente se pueden determinar al ejecutarlo, no se pueden determinar mediante un compilador porque éste no ejecuta el programa.) Las ca- racterísticas típicas de la semántica estática en los lenguajes de programación comunes iicluyen las declaraciones y la verificación de tipos. Las partes extra de la información (como los tipos de datos) que se calculan mediante el analizador semántico se llaman atributos y con frecuencia se agregan al árbol como anotaciones o "decoraciones". (Los atributos también se pueden introducir en la tabla de símbolos.)

En nuestro ejemplo de ejecución de la expresión en C

la información de tipo específica que se tendría que obtener antes del análisis de es- ta línea sería que a sea un arreglo de valores enteros con subíndices proveniente de un subintervalo de los enteros y que index sea una vaiabie entera. Entonces el 'analizador semántico registrda el árbol sintác6co con los tipos de todas las subexpresiones y pos- teriormente verificaría que la asignación tuviera sentido para estos tipos, y declararía un error de correspondencia de tipo si no fuera así. En nuestro ejemplo todos los tipos tienen sentido, y el resultado del análisis semántico en el árbol sintáctico podría repre- sentarse por el siguiente árhol con anotaciones:

sentencia de risignacidri

expreskjn de subíndice entero

r-rl>rt.sidn <iditivu entero

i d e n t i f icador ident i f icador número a index 4

arreglo de enteros entero entero

número 2

entero

OPTlMlZADOR DE CÓDIGO FUENTE Los compiladores a menudo incluyen varias etapas para el mejoramiento, u optimi- zación, del código. El punto más anticipado en el que la mayoría de las etapas de op- timización se pueden realizar es precisamente después del análisis semántico, y puede haber posibilidades para el mejoramiento del código que dependerán sólo del código fuente. Indicamos esta posibilidad al proporcionar esta operación como una fase por separado en el de compilación. Los compiladores individuales muestran una amplia variación no sólo en los tipos de optimizaciones realizadas sino también en la colocación de las fases de optimización.

En nuestro ejemplo incluimos una oportunidad para la optimización a nivel de fuen- te; a saber, la expresión 4 + 2 se puede calcular previamente por el compilador para ciar como resultado 6. (Esta optimización particular es conocida como incorporación de constantes.) Claro que existen posibilidades mucho niás con>plejas (algunas de las cuales se mencionarán en el capítulo 8). En nuestro ejemplo esta optimizaci6n se puede realizar de manera directa sobre el árbol sintáctico (con anotaciones) al colapsar el subárbol secundario de la derecha del nodo raíz a su valor constante:

Page 15: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Proceso de traducción

entero 6 entem

identificador iden t i f i cador a index

arreglo de enteros entero

Muchas optimizaciones se pueden efectuar directamente sobre el árhol. pero en varios casos es irás I'ácil optiniizar una forma linealizada del irbol que esté más cercana al código ensamblador. Existen muchas variedades diferentes de tal código, pero una elecciGn estándar es el código en tres direcciones, denominado así porque contiéne las direcciones de (y hasta) tres localidbdes en memoria. Otra selección popular es el código P. el cual se ha utilizado en muchos compiladores de Pascal.

En nuestro ejemplo el código en tres direcciones para la expresión original en C podría parecerse a esto:

t = 4 + 2

a [index] = t

(Advierta el uso de una variable temporal adicional t para almacenar el resulrado inter- medio de la suma.) Ahora el optimizador mejoraría este código en dos etapas, en primer lugar calculando el resultado de la suma

y después reemplazando a t por su valor para obtener la sentencia en tres direcciones

En la figura 1.1 indicamos la posibilidad de que el optimizador del código fuente pueda emplear código en tres direcciones al referirnos a su salida como a un código intermedio. Históricamente, el código intermedio ha hecho referencia a una forma de representación de código intermedia entre el código fuente y el código objeto, tal como el código de tres direcciones o una representación lineal semejante. Sin embargo, tam- bién puede referirse de manera más general a crcalquier representación interna para el código fuente utilizado por el compilador. En este sentido, también se puede hacer re- ferencia al árbol sintáctico como un código intermedio. y efectivamente el optimizador del código fiieiite podría continuar el uso de esta representación en su salida. En oca- siones este sentido más general se indica al hacer referencia al código intermedio como representación intermedia, o RI.

GENERADOR DE CODIGO El generador de código toma el código intermedio o RI y genera el código par3 la niá- quina objetivo. En este texto escribiremos el c6digo objetivo en la fornia de lenguaje ensarnh1;idor para facilitar su coinprerisión, aunque la mayoría tle los conipiladores

Page 16: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

generan el código objeto de manera directa. Es en esta fase de la compilación en la que las propiedades de la máquina objetivo se convierten en el factor principal. No sólo es necesario emplear instrucciones que existan en la máquina objetivo, sino que las deci- siones respecto a la representación de los datos desempeñarán ahora también un papel principal, tal como cuántos bytes o palabras de memoria ocuparán las variables de tipos de datos enteros y de punto flotante.

En iruestro ejemplo debemos decidir ahora cuántos enteros se almacenarán para generar el código para la indización del arreglo. Por ejemplo, una posible secuencia de código muestra para la expresión dada podría ser (en un hipotético lenguaje en- samblado~)

MOV RO, index ; ; valor de index -> RO MUL RO, 2 ;; doble valor en RO

MOV R1, &a ;; dirección de a -> R1

ADD R1, RO ;; sumar RO a R1

MOV *R1, 6 ;; constante 6 -> dirección en R1

En este código utilizamos una convención propia de C para direccionar modos, de manera que &a es la dirección de a (es decir, la dirección base del arreglo) y que *R1 significa direccionamiento indirecto de registro (de modo que la última instrucción al- macena el valor 6 en la dirección contenida en Rl). En este código también partimos del supuesto de que la máquina realiza direccionamiento de byte y que los enteros ocupan dos bytes de memoria (de aquí el uso del 2 como el factor de multiplicación en la se- gunda instrucción).

OPTlMlZADOR DE C~DIGO OBJETIVO En esta fase el compilador intenta mejorar el código objetivo generado por el generador de código. Dichas mejoras incluyen la selección de modos de direccionamiento para mejorar el rendimiento, reemplazando las instrucciones lentas por otras rápidas, y elimi- nando las operaciones redundantes o innecesarias.

En el código objetivo de muestra dado es posible hacer varias mejoras. Una de ellas es utilizar una instrucción de desplazamiento para reemplazar la multiplicación en la segunda instrucción (la cual por lo regular es costosa en términos del tiempo de eje- cución). Otra es emplear un modo de direccionamiento más poderoso, tal como el di- reccionamiento indizado para realizar el almacenamiento en el arreglo. Con estas dos optimizaciones nuestro código objetivo se convierte en

MOV RO, index ; ; valor de index -> RO

SHL RO ;Í doble valor en RO

MOV &a[ROl, 6 ;; constante 6 -> dirección a + RO

Esto completa nuestra breve descripción de las fases de un compilador. Querernos enfatizar que esta descripción sólo es esquemática y no necesariamente representa la or- ganización real de un compilador trabajando. En su Lugar, los compiladores muestran una amplia variación en sus detalles de organización. No obstante, las fases que descri- bimos están presentes de alguna forma en casi todos los compiladores.

También analizamos sólo de manera superficial las estructuras de datos necesarias para mantener la información necesaria en cada fase, tales como el árbol sintáctico, el código intermedio (suponiendo que éstos no sean iguales), la tabla de literales y la tabla de símbolos. Dedicamos la siguiente sección a una breve perspectiva general de las es- tructuras de datos principales cn u n compilador.

Page 17: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Estructuras de datas principales en un compilador

1.4 ESTRUCTURAS DE DATOS PRINCIPALES EN UN COMPIIADOR La interacción entre los algoritmos utilizados por las fases de un compilador y las estructu- ras de datos que soportan estas fases es, naturalmente, muy fuerte. El escritor del compila- dar se esfuerza por implementar estos algoritmos de una manera tan eticaz como sea posi- ble, sin aumentar demasiado la complejidad. De manera ideal, un compilador debería poder compilar un programa en un tiempo proporcional a1 tamaño del programa, es decir, en O(n) tiempo, donde n es una medida del t a m o del programa (por lo general el número de ca- racteres). En esta sección señalaremos algunas de las principales estructuras de datos que son necesarias para las fases como parte de su operación y que sirven para comunicar la in- formación entre las fases.

TOKENS Cuando un rastreador o analizador léxico reúne los caracteres en un token, generalmente representa el token de manera simbólica, es decir, como un valor de un tipo de datos enu- merado que representa el conjunto de tokens del Lenguaje fuente. En ocasiones tambi6n es necesario mantener la cadena de caracteres misma u otra información derivada de ella, tal como el nombre asociado con un token identificador o el valor de un token de número. En la mayoría de los lenguajes el analizador léxico sólo necesita generar un token a la vez (esto se conoce como búsqueda de símbolo simple). En este caso se puede utilizar una variable global simple para mantener la información del token. En otros casos (cuyo ejemplo más notable es FORTRAN), puede ser necesario un arreglo de tokens.

ÁRBOL SINTÁCTICO Si el analizador sintictico genera un árbol sintáctico, por lo regular se construye como una estructura estándar basada en un apuntador que se asigna de manera dinámica a medida que se efectúa el análisis sintiictico. El árbol entero puede entonces conservarse como una variable simple que apunta al nodo raíz. Cada nodo en la estructura es un registro cuyos campos representan la información recolectada tanto por el analizador sintáctico como, posteriornmte, por el analizador semántico. Por ejemplo, el tipo de datos de una expre- sión puede conservarse como un campo en el nodo del árbol sintáctico para la expresión. En ocasiones, para ahorrar espacio, estos campos se asignan de manera dinámica, o se almacenan en otras estructuras de datos, tales como la tabla de símbolos, que permiten una asignación y desasignación selectivas. En realidad, cada nodo de árbol sintáctico por sí mismo puede requerir de atributos diferentes para ser almacenado, de acuerdo con la clase de estructura del lenguaje que represente (por ejemplo, un nodo de expresión tiene requerimientos diferentes de los de un nodo de sentencia o un nodo de declaración). En este caso, cada nodo en el árbol sintáctico puede estar representado por un registro varia- ble, con cada clase de nodo conteniendo solamente la información necesaria para ese caso.

TABU DE S~MBOLOS Esta estructura de datos mantiene la información asociada con los identificadores: fun- ciones, variables, constantes y tipos de datos. La tabla de símbolos interactúa con casi todaslas fases del compilador: el rastreador o analizador léxico, el analizador sintáctico o el analizador semántico puede introducir identificadores dentro de la tabla; el analiza- dor semántico agregará tipos de datos y otra información; y las fases de optimización y generación de código utilizarán la información proporcionada por la tabla de símbolos para efectuar selecciones apropiadas de código objeto. Puesto que la tabla de símbo- los tendrá solicitudes de acceso con tanta frecuencia, las operaciones de inserción, eliminación y acceso necesitan ser eficientes, preferiblemente operaciones de tiempo constante. Una estructura de datos estándar pasa este propósito es la tabla de dispersión o de cálculo de dirección, aunque también se pueden utilizar diversas estructuras de irbol. En ocasiones se utilizan varias tablas y se mantienen cn una lista o pila.

Page 18: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

TABLA DE LITERALES La búsqueda y la inserción rápida son esenciales también para la tabla de literales, la cual almacena constantes y cadenas utilkzadas en el programa. Sin embargo, una tabla de literales necesita impedir las eliminaciones porque sus datos se aplican globalmente al programa y una constante o cadena aparecerá sólo una vez en esta tabla. La tabla de literales es importante en la reducción del tamaño de un programa en la memoria al permitir la reutilización de constantes y cadenas. También es necesaria para que el ge- nerador de código construya direcciones simbólicas para las literales y para introducir definiciones de datos en el archivo de código objetivo.

CÓDIGO INTERMEDIO De acuerdo con la clase de código intermedio (por ejemplo, código de tres direcciones y código P) y de las clases de optimizaciones realizadas, este código puede conservarse como un arreglo de cadenas de texto, un archivo de texto temporal o bien una lista de estructuras ligadas. En los compiladores que realizan optimizaciones complejas debe ponerse particular atención a la selección de representaciones que permitan una fácil reorganización.

ARCHIVOS TEMPORALES Al principio las computadoras no poseían suficiente memoria para guardar un programa completo durante la compilación. Este problema se resolvió mediante el uso de archivos temporales para mantener los productos de los pasos intermedios durante la traducción o bien al compilar "al vuelo", es decir, manteniendo sólo la información suficiente de las partes anteriores del programa fuente que permita proceder a la traducción. Las li- mitantes de memoria son ahora un problema mucho menor, y es posible requerir que una unidad de compilación entera se mantenga en la memoria, en especial si se dispone de la compilación por separado en el lenguaje. Con todo, los compiladores ocasionalmen- te encuentran útil generar archivos intermedios durante alguna de las etapas del proce- samiento. Algo típico de éstos es la necesidad de direcciones de corrección hacia atrás durante la generación del código. Por ejemplo, cuando se traduce una sentencia condi- cional tal como

if x = O then . . . else . . debe generarse un salto desde la prueba para la parte bicondicional "else" antes de conocer la ubicación del código para el "else":

CMP X,O

JNE NEXT ;; ubicación del NEXT aún no conocida

<código para la parte "thenM>

NEXT :

<código para la parte "else">

Por Lo regular debe dejarse un espacio en blanco para el valor de NEXT, el cual ae llena una vez que 5e logra conocer el valor. Esto se consigue fácilmente con el u\o de un archivo temporal.

OTRAS CUESTIONES REFERENTES A LA ESTRUCTURA DEL COMPILADOR La estructura de un compilador se puede ver desde muchos ángulos distintos. En la sección 1.3 describimos sus fases, las cuales representan la estructura lógica de u n compilador. Otros puntos de vista son posibles: la estructura física del compilador, la sccuenciación de las operaciones, y así sucesivamente. La persona que escribe el cornpilatlor clehel-ío estar

Page 19: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Otras cuestiones referentes a la estructura del tompilador 15

familiarizada con tantos puntos de vista de la estructura del cotnpilador corito sea posible, ya que la estructura del compilador será determinante para su confkthilidad, eficacia, utili- dad y mantenimiento. En esta sección consideraremos otros aspectos de la estructura del conipilador y señalaremos cómo se aplica cada punto de vista.

En esta perspectiva las operaciones del conipilador que analizan el programa fuente pa- ra calcular sus propiedades se clasifican como la parte de análisis del conipilador, inicn- . . tras que las operaciones involucradiis en la producción del código traducido se conocen como la parte de síntesis, del compilador. Como cs tiatural, el aniilisis léxico, el análisis sintáctico y el análisis semintico pertenecen a la parte de análisis, mientras que Iii ge- neración del código es la síntesis. Las etapas de optimizacicín pueden involucrar tanto análisis como síntesis. El análisis tiende a ser más inatemitico y a comprenderse mejor, mientras que la síntesis requiere de técnicas niis especializadas. Por consiguiente. es útil separar las etapas del análisis de las etapas de ¡a síntesis, de modo que cada una se pue- da modificar de manera independiente respecto a la otra.

ETAPA INICIAL Y ETAPA FINAL Esta perspectiva considera al coinpilador separado en aquellas funciones que dependen sólo del lenguaje fuente (lqetapa inicial) y aquellas operaciones que dependen única- mente del lenguaje objetivo (la etapa final). Esto es similar a la división en análisis y síntesis: el analizador léxico, el analizador sintáctico y el analizador semántico son par- te de la etapa inicial, mientras que el generador de código es parte de la etapa final. Sin embargo, algo del inálisis de optimización puede ser dependiente del objetivo y, por lo tanto, parte de la etapa final, mientras que la síntesis del código intermedio es a inenu- do independiente del objetivo y, por consiguiente, parte de la etapa inicial. De manera ideal, el compilador estaría estrictamente dividido en estas dos seccioties, con la repre- sentación intermedia como el medio ile comunicación entre ellas:

F.---

6 o ' Comgo I Etapa Cooigo

luente inicial intermedio fina' & oqetivo

Esta estructura es especialmente importante para la portabilidad del compilador, en la cual el compilador está diseñado con un enfoque hacia la modificación, ya sea del código fuente (lo que involucra volver a escribir la etapa inicial) o del código objetivo (lo que implica reescribir la etapa final). En la práctica esto ha probado ser difícil de conseguir, y los denominados compiladores portátiles todavía tienden a poseer caracte- rísticas que dependen tanto del lenguaje fuente como del lenguaje objetivo. Esto puede, en parte, ser culpa de los cambios rápidos y fundamentales tanto en los lenguajes de pro-" gramación como en las arquitecturas de las máquinas, pero también es difícil retener de manera eficaz a toda la información que uno pudiera necesitar al cambiar a un nuevo lenguaje objetivo o al crear las estructuras de datos adecuadamente generales para permitir un cambio a un nuevo lenguaje fuente. No obstante, una tentativa consistente para separar las etapas inicial y final redundar6 en beneficios pamtina portabilidad mís fácil.

PASADAS Un coinpilador a inenudo encuentra conveniente procesar todo el progmina fuente varias veces antes de gcncrx el código. Estas repeticiones son conocidas como pasadas. Después del paso iniciitl, tlonde sc corlstruye un irbol sintáctico o un ccídigo intermedio a partir tle la fuente, una pasada coiiiste en proccs;tr la reprc~cntocicíii iilternictli:i,

Page 20: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. I i INTROOUCCI~N

agregando información a ella, alterando su estructura o produciendo una representación diferente. Las pasadas nueden corresoonder o no a las fases, a menudo una pasada con- sistirá de varias etapas.^~n realidad, dependiendo del lenguaje, un compilad& puede ser de una pasada, en el que todas las fases se presentan durante un paso único. Esto resul- ta en una compilación efkaz pero también en (por lo regular) un código objetivo menos eficiente. Tanto Pascal como C son lenguajes que permiten La compilación de una pasa- da. (Modula-2 es un lenguaje cuya estructura requiere que un compilador tenga por lo menos dos pasadas.) La mayoría de los compiladores con optimizaciones utilizan más de una pasada; por lo regular se emplea una pasada para análisis léxico y sintáctico, otra pasada para análisis semántica y opti&ación a nivel del fuente, y una tercera pasada para generación de código y optimización a nivel del objetivo. Los compiladores fuer- temente optimizadores pueden emplear incluso más pasadas: cinco, seis o incltiso ocho no son algo fuera de lo común.

D E F I N I C I ~ N DE LENGUAJE Y COMPILADORES Advertimos en la sección l. i que las estructuras Iéxicas y sintácticas de un lenguaje de programaciún por lo regular son especificadas en términos formales y utilizan expresiones regulares y gramáticas libres de contexto. Sin embargo, la semántica de un lenguaje de programación todavía es comúnmente especificada utilizando descripciones en inglés (uotro lenguaje natural). Estas descripciones (junto con la estructura sintáctica y Iéxica formales) generalmente son recopiladas en un manual de referencia del lenguaje, o definición de lengua,ie. Con un nuevo lenguaje, una definición de lenguaje y un com- pilador con frecuencia son desarrollados de manera simultánea, puesto que las técnicas disponibles para el escritor de compiladores pueden tener un impacto fundamental so- bre la definición del lenguaje. Similarmente, la manera en la que se define un lenguaje tendrh un impacto fundamental sobre las técnicas que son necesarias para construir el compilador.

Una situación más común para el escritor de compiladores es que el lenguaje que se está implementando es bien conocido y tiene una definición de lenguaje existente. En ocasiones esta definición de lenguaje ha alcanzado el nivel de un lenguaje estándar que ha sido aprobado por algtina de las organizaciones de estandarización oficiales, tal como la ANSI (American National Standards Institute) o ISO (Intemational Orga- nization for Standardization). Por ejemplo, FORTRAN, Pascal y C tienen estándares ANSI. Ada tiene un estándar aprobado por el gobierno de Estados Unidos. En este caso, el escritor de compiladores debe interpretar la definición de lenguaje e imple- mentar un compilador acorde con la definición de lenguaje. Esto a menudo no es una tarea fácil, pero en ocasiones se hace más fácil por la existencia de un conjunto de programas de prueba estándar (una suite o serie de pruebas) contra el cual un com- pilador puede ser contrastado (una serie de pruebas así existe para Ada). El lenguaje de ejemplo TINY empleado en el texto tiene su estructura Iéxica, sintáctica y semán- tica especificadas en las secciones 2.5, 3.7 y 6.5, respectivamente. El apéndice A contiene un manual de referencia del lenguaje mínimo para el lenguaje de proyecto de compilador C-Minus.

Ocasionalmente, un lenguaje tendrá su semántica &da mediante una definición formal en términos matemáticos. Varios métodos actualmente en uso son empleados para esto, y ningún método ha conseguido el nivel de un estándar, aunque la denomina- da semántica denotacional se ha convertido en uno de los métodos más comunes, es- pecialmente en la comunidad de programación funcional. Cuando existe una definición formal para un lenguaje, entonces (en teoría) es posible dar una prueba inatenxítica de que un compilador se aviene a la definición. Sin embargo, hacer esto es tan dificil que casi nunca se hace. En cualquier caso, las técnicas para hacerlo así rebasan el alcance de este texto, y 13s técnicas de semántica formal no se estudiarán aquí.

Page 21: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Otras cuestiones referentes a la estructura del compiladar 17

Un aspecto de la construcción de compiladores que es particularmente afectado por la definición de lenguaje es la estructura y comportürniento del ambiente de ejecución. Los ambientes de ejecución se estudian con detalle en el capítulo 7. Sin embargo, vale la pena notar aquí que la estructura de datos permitida en un lenguaje de prograntación. y particularmente las clases de llamadas de funciones y valores devueltos permiiidos, tienen un efecto decisivo sobre La complejidad del sistema de ejecución. En particular, los tres tipos básicos de ambientes de ejecución, en orden creciente de complejidad, mn como se describe i continuación:

En primer lugar, FORTRAN77, sin apuntadores o asignación dinámica y sin lla- madas a funciones recursivas, permite un ambiente de ejecución completamente es- trítico, donde toda la asignación de memoria se hace antes de la ejecución. Esto hace la labor de asignación particularmente fácil para la persona que escribe el compilador, en la medida que no necesita generarse código para mantener el ambiente. En segundo lugar, Pascal, C y otros lenguajes provenientes del lenguaje Algol permiten una forina limitada de asignación dinámica y llamadas de funciones recursivas y requieren un am- biente de ejecución "semidinámico" o basado en pilas con una estructura dinjmica adi- cional conocida conio hetzp, desde el cual el prograniador puede organizar la asignación dinámica. Finalmente, los lenguajes funcionales y la mayoría de los lenguajes orien- tados a objetos. tales como LISP y Smalltalk, requieren un ambiente "c«mpletamente dinámico" en el que toda asignación se realiza de manera automática mediante código generado por el compilador. Esto es complicado, porque se requiere que la memoria también sea liberada automáticamente, y esto a su vez requiere complejos algoritmos de "recolección de basura". Examinaremos esos métodos junto con nuestro estudio de los ambientes de ejecución, aunque una completa relación de esta área rebasa el alcance de este libro.

OPCIONES DE COMPllADOR E INTERFACES Un aspecto importante de la construcción de compiladores es la inclusión de mecanis- mos para hacer interfaces con el sistema operativo y para proporcionar opciones al usua- rio para diversos propósitos. Ejemplos de mecanismos de interfaces son el suministro de la entrada y facilidades de salida, además del acceso al sistema de archivos de la máqui- na objetivo. Ejemplos de opciones de usuario incluyen la especificación de listar carac- terísticas (longitud, mensajes de error, tablas de referencia cruzada) así como opciones de optimización de código (rendimiento de ciertas optimizaciones pero no otras). Tanto la interface como las opciones son calificadas colectivamente como las pragmáticas del compilador. En ocasiones una definición de lenguaje especificará que se debe propor- cionar cierta pragmática. Por ejemplo, Pascal y C especifican ciertos procedimientos de entradalsalida (en Pascal, son parte del propio lenguaje, mientras que en C son parte de la especificación de una librería estándar). En Ada, varias directivas de compila- dor, denominadas pragmas, son parte de la definición del lenguaje. Por ejemplo, las sentencias de Ada

prasma LIST(0N) ;

. . . prasma LIST(0FF);

generan un listado de compilador para la parte del programa contenida dentro de los pragmas. En este texto veremos directivas de cornpilador solamente en el contexto de la generación de un listado con información para propósitos de la depuración del com- pilador. También, no trataremos cuestiones sobre interfaces de entraddsalida y sistenta operativo, puesto que involucran consider:iblcs detalles que varían clemasiado de un sis- tema operativo a otro.

Page 22: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. l I I N T R O D U C C I ~ N

MANEJO DE ERRORES Una de las funciones más importantes de un compitador es su respuesta a los errores en un programa fuente. Los errores pueden ser detectados durante casi cualquier fase de la compilación. Estos errores estáticos (o de tiempo de compilac'ión) deben ser notificados por un compilador, y es importante que el compilador sea capaz de generar mensajes de error significativos y reanudar la compilación después de cada error. C d a fase de un compilador necesitará una clase ligeramente diferente de manejo de erro- res. y, por lo tanto, un manejador de errores debe contener operaciones diferentes, cada una apropiada para una fase y situación específica. Por consiguiente, las técnicas de manejo de errores para cada fase se estudiarán de manera separada en el capítulo apropiado.

Una definición de lenguaje por lo general requerirá no solamente que los errores estrlticos sean detectados por un compilador, sino también ciertos errores de ejecución. Esto requiere que un compilador genere código extra, el cual realizará pruebas de ejecución apropiadas para garantizar que todos esos errores provocarán un evento apro- piado durante la ejecución. El más simple de tales eventos será detener la ejecución del programa. Sin embargo, a menudo esto no es adecuado, y una definición de lenguaje puede requerir la presencia de mecanismos para el manejo de excepciones. Éstos pueden complicar sustancialmente la administración de un sistema de ejecución, especialmente si un programa puede continuar ejecutándose desde el punto donde ocumó el error. No consideraremos la implementación de un mecanismo así, pero mostraremos c h o un compilador puede generar código de prueba para asegurar qué errores de ejecución es- pecificados ocasionarán que se detenga la ejecución.

1.6 ARRANQUE AUTOMATICO Y PORTABILIDAD Hemos analizado el lenguaje fuente y el lenguaje objetivo como factores determinantes en la estructura de un compilador y la utilidad de separar cuestiones de lenguaje fuente y obje- tivo en etapas inicial y final. Pero no hemos mencionado el tercer lenguaje involucrado en el proceso de construcción de compiladores: el lenguaje en el que el compilador mismo es- tá escrito. Para que el compilador se ejecute inmediatamente, este lenguaje de impleinenta- ción (o lenguaje anfitrión) tendría que ser lenguaje de máquina. Así fue en realidad como se escribieron los primeros compiladores, puesto que esencialmente no existían compi- Iadores todavía. Un enfoque más razonable en la actualidad es escribir el compilador en otro lenguaje para el cual ya exista un compilador. Si el compilador existente ya se ejecuta en la máquina objetivo, entonces solamente necesitamos compilar el nuevo coinpilador utilizan- do el compilador existente para obtener un programa ejecutable:

Cornpilador para lenguaje A Cornpilador en ejecución escrito en lenguaje B para lenguaje A

Si el compilador existente para el lenguaje B se ejecuta en una máquina diferente de la in5- quina objetivo, entonces la situación es un poco inás complicada. La compilación produ- ce entonces un compilador cruzado, es decir, un compilador que genera código objetivo para tina iniquina diferente de aquella en la que puede ejccutarse. Ésta y otras situaciones niis coutplejas se describen mejor al esquematizar un conipiliidor como un diagrama T (Ilanriido así ticbido a su fortna). Un compilador escrito en el lengwje H (por 1~111,qiccqr hosi

Page 23: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Arranque automático y portabilidad 19

o anfitrión) que traduce lenguaje S (de source o fuente) en lenguaje T (por I<iri,qlcigr Tcit.,y~t

u objetivo) se dibuja como el siguiente diagrama T:

Advierta que esto es equivalente a decir que el compilador se ejecuta en la "ináquina" H (si H no es código de ináquina, entonces la consideraremos como ccídigo ejecutable para una máquina hipotética). Típicamente, esperamos que H sea lo mismo que T (es decir, el com- pibador produce código para la misma máquina que aquella en la que se ejecuta), pero no es necesario que éste sea el caso.

Los diagrainas T se pueden combinar en dos maneras. Primero, si tenemos dos com- piladores que se ejecutan en la misma mhquina H, uno de los curtles traduce lenguaje A al lenguaje B mientras que el otro traduce el lenguaje B al lenguaje C, entonces podemos coin- binarlos dejando que la salida del primero sea la entrada al segundo. El resultado es un compilador de A a C en la máquina H. Expresamos lo anterior como sigue:

En segundo lugar podemos utilizar un compilador de la "máquina" ti a la "tnáquina" K para traducir el lenguaje de irnplementación de otro compilador de H a K. Expresamos esto últiino como sigue:

Ahora el primer escenario que describimos anteriormente, es decir, utilizando un coin- pilador existente para el lenguaje B en la máquina H para traducir un compilador de lengua- je A a H escrito en B, puede verse como el siguiente diagrama, el cual es precisamente un caso especial del diagrama anterior:

Page 24: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. I I INTRODUCCI~N

Figura 1 2 a Primera etapa en un proceso de arranque automático

El segundo escenario que describimos (donde el compilador de lenguaje B se ejecuta en una máquina diferente, lo cual resulta en un compilador cruzado para A) puede describirse de manera similar como sigue:

Es común escribir un compilador en el mismo lenguaje que está por compilarse:

Mientras que parece ser un enredo de circularidad (puesto que, si todavía no existe compila- dor para el lenguaje fuente, el compilador mismo no puede ser compilado) existen ventajas importantes que se obtienen de este enfoque.

Considere, por ejemplo, cómo podemos enfocar el problema de la circularidad. Podemos escribir un compilador "rápido e impreciso" en lenguaje ensamblador, traduciendo d a - mente aquellas características de lenguaje que en realidad sean utilizadas en el compilador (teniendo, naturalmente, limitado nuestro uso de esas características cuando escribamos el compilador "bueno"). Este compilador "rápido e impreciso" también puede producir códi- go muy ineficiente (¡solamente necesita ser correcto!). Una vez que tenemos el compilador "rápido e impreciso" en ejecución, lo utilizamos para compilar el compilador "bueno". Entonces volvemos a compilar el compilador "bueno" para producir la versión final. Este proceso se denomina arranque automático por transferencia. Este proceso se ilustra en las figuras 1 . 2 ~ y 1.26.

Después del arranque automático tenemos un compilador tanto en código fuente como en código ejecutable. La ventaja de esto es que cualquier mejoramiento al código fuente del

Compilador es&o en su / ~ompilado;en ejecucibn propio lenguaje A pero ineficiente

, Compilador "rápido e impreciso" escrito en lenguaje de máquina

Page 25: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Lenguaje y compilador de muestra TlNY

Fiqura 1.2b segunda etapa en un proceso de arranque automático

Compilador escrito en su Versión final propio lenguaje A del compilador

~om~ilador-en ejecución pero ineficiente (de la primera etapa)

compilador puede ser transferido inmediatamente a un coinpilador que esté trabajando. apli- cando el mismo proceso de dos pasos anterior.

Pero existe otra ventaja. Transportar ahora e1 compilador a una nueva computadora anfitrión solamente requiere que la etapa final del código fuente vuelva a cscribirse para generar código para la nueva máquina. Éste se compila entonces utilizando el compilador antiguo para producir un compilador cniz.ado, y el compilador es nuevamente recompilado mediante el compilador cruzado para producir una versión de trabajo para la nueva máquina. Esto se ilustra en las figuras 1 . 3 ~ y 1.3h.

Fioura 1.3a Transportación de un compilador escrito en su propio lenguaje fuente

(paso 1)

Cornpilador original

figura 13b A K

Transportación de un - --- - campilador escrito en su *- propio lenguaje fuente

(paso 4 Código fuente del cornptlador Compilador redirigido

redirtgido a K

Compilador cruzado

1.7 LENGUAJE Y COMPllADOR DE MUESTRA TlNY Un libro acerca de la construcción de compiladores estaría incompleto sin cjcmplos para ca- da paso cn el proceso de conipilacih. En muchos casos iIustrrirein«s técniciis con ejeinplos

Page 26: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

que son sustraídos de lenguajes existentes, tales como C, C++, Pascal y Ada. No obstante, estos ejemplos no son suficientes para mostrar cónm se conjuntan todas las partes de un compilador. Por eso, tantbién es necesario exhibir un compilador completo y proporcionar un comentario acerca de su funcionamiento.

Este requerimiento (que se ha demostrado en un compilador real) es difícil. Un compi- iador "real", es decir, uno que esperaríamos utilizar en la programación cotidiana, tiene demasiado detalle y sería algo abrumador para estudiar dentro del marco conceptual de un texto. Por otra parte, un compiiador para un lenguaje muy pequeño, cuyo listado pudiera compriniirse en aproximadamente 10 páginas de texto, no podría esperar demostrar de ma- nera adecuada todas las características que un compilador "real" necesita.

Intentaremos satisfacer estos requerimientos al proporcionar código fuente completo en C (ANSI) para un lenguaje pequeño cuyo compilador se pueda comprender fácilmente una vez que las técnicas se hayan entendido. Llamaremos a este lenguaje TINY y lo utilizaremos como un ejemplo ejecutable para las técnicas estudiadas en cada capítulo. El código para su compilador se analizará a medida que se cubran las ttknicas. En esta sección proporcio- naremos una perspectiva general del lenguaje y su compilador. El código completo del com- pilador se recopila cn i.1 apéndice B.

Un problema adicional es la elección del lenguaje de máquina a utilizar como el lengua- je objetivo del compilador TINY. Nuevamente, la complejidad al utilizar código de máqui- na real para un procesador existente hace que una selección de esta naturaleza sea difícil. Pero la elección de un procesador específico también tiene el efecto de limitar la ejecución del código objetivo resultante para estas máquinas. En cambio, simplificamos el código objetivo para que sea el lenguaje ensamblador para un procesador hipotético simple, el cual conoceremos como la máquina TM (por las siglas de "TINY Machine"). Daremos un rápido vistazo a esta máquina aquí, pero aplazaremos una descripción más extensa hasta el capítulo 8 (generación de código). Un listado del simulador de TM en C aparece en el apéndice C.

1.71 Lenguaje TINY

Un programa en TINY tiene una estructura muy simple: es simplemente una secuencia de sentencias separadas mediante signos de punto y coma en una sintaxis semejante a la de Ada o Pascal. No hay procedimientos ni declaraciones. Todas las variables son variables enteras, y las variables son declaradas simplemente al asignar valores a las mismas (de modo pare- cido a FORTRAN o BASIC). Existen solamente dos sentencias de control: una sentencia "it" y una sentencia "repeat". Ambas sentencias de control pueden ellas mismas contener secuencias de sentencias: Una sentencia "if' tiene una parte opcional "else" y debe termi- narse mediante la palabra clave end. También existen sentencias de lectura y escritura que realizar: i~raddsalida. Los comentarios, que no deben estar anidados, se permiten dentro de llaves tipográficas.

Las expresiones en TINY también se encuentran limitadas a expresiones aritméticas enteras y hooleanas. Una expresión hooleana se compone de una comparación de dos expresiones aritméticas que utilizan cualesquiera de los dos operadores de comparación < y =. Una expresión aritmética puede involucrar constantes enteras, variables, parénte- sis y cualquiera de los cuatro operadores enteros +. -, * y /. (división entera), con las propiedades matemáticas habituales. Las expresiones hooleanas pueden aparecer sola- mente conio pruebas en sentencias de control: no hay variables booleanas, asignación o E/S (entradalsalida).

Page 27: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Lenguaje y tompilador de muestra TlNY 23

La figura 1.4 proporciona un programa de muestra en este lenguaje para la conocida Fun- c i h factorial. Utilizaremos este programa como un ejemplo de ejecución a través del texto.

flfllra 1 4 Programa de muestra Un programa en lenguaje en lenguaje TINY - T ~ N Y que proporcma a la calcula el factorial

&la el fattorial de su 1

entrada read x; ( introducir un entero 1. if x > O then ( no calcule si x <= O j

fact := 1;

repeat

fact := fact * x;

until x O;

write fact ( salida del factorial de x 1 end

Aunque TINY carece de muchas características necesarias para los lenguajes de programa- ción reales (procedimientos, arreglos y valores de punto flotante son algunas de las omisio- nes más serias), todavía es lo suficientemente extenso para ejemplificar la mayoría de las características esenciales de un compilador.

11.2 Compilador TlNY El cotnpitador TINY se compone de los siguientes archivos en C, donde enumeramos los archivos de cabecera ("header") (por inclusión) a la izquierda y los archivos de código ("code") a la derecha:

globals . h uti1.h

8can.h

parse. h

symtab . h ana1yze.h

code. h

cgen. h

main. c

uti1.c

scan. c

parse. c

symtab . c analyze . c code. c

cgen. c

El código fuente para estos archivos se encuentra listado en el apéndice B, con números de línea y en el orden dado, excepto que main. c está listado antes de globals .h. El archi- vo de cabecera global8 . h está incluido en todos los archivos de código. Contiene las de- finiciones de los tipos de dhos y variables globales utilizadas a lo largo del compilador. El archivo main. c contiene el programa principal que controla el compilador, y asigna e ini- cializa las variables globales. Los archivos restantes se componen de pares de archivos de cabecerdcódigo, con los prototipos de función disponibles externamente dados en el archi- vo de cabecera e implementatlos (posiblemente con funciones locales estáticas adicionales) cn el archivo de código asociado. Los archivos scan, parse, analyze y cgen correspon- den exactamente a las fases del analizador léxico, analizador sintáctico, analizador semántica y generador de código (le la figura l . l . Los archivos util contienen funciones de utile- rías necesiirias pira generar la rcpresentaci6n interna del cíldigo fuente (el irhol sinticticoi

Page 28: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. I 1 INTRODUCCldN

y exhrbir la información de error y listado. Los archivos symtab contienen una implemen- tación de tabla de cálculo de dirección de una tabla de símbolos adecuada para usarse con TINY. Los archivos code contienen utilerías para la generación de código que .;on depen- dientes de la máquina objetivo (la máquina TM. descrita en la sección 1.7.3). Los compo- nentes restantes de la figura 1.1 se encuentran ausentes: no hay tabla de literales o maneja- dor de error por separado y no hay fases de optimización. Tampoco hay código intermedio separado del árbol sintáctico. Adicionalmente, la tabla de sín~bolos interactúa solamente con el analizador semántico y el generador de código (de modo que aplazaremos un análisis de esto hasta el capítulo 6).

Para reducir la interacciún entre estos archivos, también hemos hecho el compilador de cuatro pasadas: la primera pasada se compone del analizador léxico y el analizador sin- táctico, lo que construye el árbol sintáctico; la segunda y tercera pasadas se encargan del análisis semántico, con la segunda pasada construyendo la tabla de símbolos mientras que la tercera pasada realiza la verificación de tipos; la última pasada es el generador de código. El código en main. c que controla estas pasadas es particularmente simple. Ignorando las banderas y la compilación condicional, el código central es como sigue (véanse las líneas 69, 77,79 y 94 del apéndice B):

eyntaxTree = garse ( ) ;

buildSymtab(syntaxTree);

typeCheck(syntaxTree);

code~en(syntaxTree,codefile);

Por flexibilidad, también integramos banderas de compilación condicional que hacen posible construir compiladores parciales. Las banderas, con sus efectos, se describen a continuación:

MARCA O BANDERA NO-PARSE

NO-CODE

SU EFECTO SI SE ACTIVA Construye un compilador solamente de análisis léxico.

Construye un compilador que únicamente realiza análisis sintáctico y léxico.

Construye un compilador que realiza el análisis semántico pero no genera código.

ARCHIVOS NECESARIOS PARA COMPILACIÓN (ACUMULATIVOS) globals .h, main.c, util.h, util.c, scan.h, scan.c

garse .h, garae. c

symtab. h, symtab. c, analyze. h, analyze. c

Aunque este diseño para el compilador TINY es algo poco realista, tiene la ventaja pedagógica de que los archivos por separado corresponden aproximadamente a las fases, y se pueden analizar (así como compilar y ejecutar) de manera individual en los capítulos que siguen.

El compilador TINY se puede compilar por cualquier compilador C ANSI. Suponiendo que el nombre del archivo ejecutable es tiny, se puede utilizar para compilar un programa fuente de TINY en el archivo de texto samgle. tny al emitir el comando

tiny samgle. tny

(El compilador también agregará la extensión . tny si es on~itida.) Esto imprimirá un lista- do de programa en la pantalla (el cual be puede redirigir a un ;uchiwt) y (si la generación del

Page 29: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

lenguaje y comp~lador de muestra TlNY 25

código se activa) también generará el archivo de código objetivo samgle . tm (para uti- lizarse con la máquina TM, que se describe más adelante).

Existen diversas opciones para la información en el listado de compilacióii. Se cncuen- tran disponibles las siguientes banderas:

MARCA O BANDERA Echosource

TraceScan

Traceparse

TraceAnalyze

TraceCode

SU EFECTO SI SE ACTIVA Hace eco (duplica) el programa fuente TlNY hacia el listado junto con los números de línea.

Presenta la información en cada token a medida que el analizador léxico lo reconoce.

Presenta el árbol sintáctico en un formato linealizado.

Presenta la información resumida acerca de la tabla de sírnbolos y la verificación de tipos.

Imprime conientarios del rastreo de la generación de código hacia el archivo de código.

1.11 Máquina TM Empleamos el lenguaje ensamblador para esta máquina como el lenguaje objetivo para el compilador TINY. La máquina TM tiene instrucciones suficientes para ser un objetivo adecuado para un lenguaje pequetio como TINY. De hecho, TM tiene algunas de las pro- piedades de las computadoras con conjunto de instrucciones reducido (o RISC, de Reduced Instruction S@ Computers), en que toda la aritmética y las pruebas deben tener lugar en registros y los modos de direccionamiento son muy limitados. Para dar alguna idea de la simplicidad de esta máquina traducimos el código para la expresión C

en el lenguaje ensamblador de TM (compare éste con el lenguaje ensamblador hipotético para la misma sentencia en la sección 1.3, página 12):

LDC 1,0(0) carga O en registro 1 * la instrucción siguiente * supone que index está en la localidad 10 en memoria LD 0,10(1) carga valor para 10 + R1 en RO LDC 1,2(0) carga 2 en registro 1

MUL 0,1,0 pon R ~ * R O en RO

LDC 1,0(0) carga O en registro 1

* la instrucción siguiente * supone que a está en la localidad 20 en memoria LDA 1,20(1) carga 20+R1 en RO

ADD 0,1,0 pon R ~ + R O en RO

LDC 1,6(0) carga 6 en registro 1

ST 1,0(0) almacena Rl en O+RO

Advertirnos que existen tres modos de direccionamiento para la operación de carga, to- dos dados por instrucciones diferentes: LDC es "constante de carga", LD es "carga desde niemona" y LDA es "dirección de carga", todos por sus siglas en inglés. Notamos también que las direcciones siempre deben ser dadas corno valores de "registro+desplaiamiertto", como en 1 0 ( 1) (instrucción 2 del código precedente), que establece para la direcciijn

Page 30: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

calculada el agregar el desplazamiento 10 al contenido del registro l . (Puesto que el O fue cargado en el registro 1 en la instrucción previa, esto en realidad se refiere a la localidad absoluta 10.)' También advertimos que las instrucciones aritméticas MUL y ADD pueden tener solamente operandos de registro y son instrucciones de "tres direcciones", en que el registro objetivo del resultado se puede especificar independientemente de los operandos (contraste esto con el código en la sección 1.3, página 12, donde las operaciones fueron de "dos direcciones").

Nuestro simulador para la máquina TM lee el código ensamblador directamente de un archivo y lo ejecuta. De este modo, evitamos la complejidad agregada de traducir el lengua- je ensainblador a código de máquina. Sin embargo, nuestro simulador no es un verdadero ensamblador, en el sentido de que no hay etiquetas o direcciones simbólicas. Así, el compi- lador TINY todavía debe calcular direcciones absolutas para los saltos. También, para evi- tar la coniplejidad extra de vincularse con rutinas externas de entradalsalida, la máquina TM contiene facilidades integradas de EIS para enteros; éstas son leídas desde, y escritas hacia, los dispositivos estándar durante la simulación.

El simulador TM se puede compilar desde el código fuente tm. c utilizando cualquier compilador C ANSI. Suponiendo que el archivo ejecutable se llama tm, se puede emplear al emitir el comando

donde sample. tm es, por ejemplo, el archivo de código producido por el compilador TINY a partir del archivo fuente sample . tny. Este comando provoca que el archivo de código sea ensamblado y cargado; entonces el ~imulador TM se puede ejecutar de manera interactiva. Por ejemplo, si sample. tny es el programa de muestra de la figura 1.4, en- tonces el factorial de 7 ie puede calcular con la interacción siguiente:

tm samgle. tm

TM simulation (enter h for helg) ... Enter command: go

Enter value for IN instruction: 7

OUT instruction grinta: 5040

HALT: 0.0.0

Halted

Enter command: guit

Simulation done.

1.8 C-MINUS: UN LENGUAJE PARA UN PROYECTO DE COMPILADOR Un lenguaje más extenso que TINY, adecuado para un proyecto de compilador, Fe des- cribe en el apéndice A. Éste es un subconjunto considerablemente restringido de C, al

1. El cooiando LDC también requiere un formato de registrotdesplazantiento, pero cl registro es ignorado y el <lesplazamiento mismo es cargado como una constante. Esto se debe al f«nnnt« tinifor- nie simple del ensamhlador de TM.

Page 31: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejercicios 27

Figura 1.5 Un programa de C-Minur que da a la salida el factarial de su entrada

cual llamaremos C-Minus. Contiene enteros, arreglos de enteros y funciones (incluyendo procedimientos, o funciones sin tipo). Tiene declaraciones (estáticas) locales y globales y funciones recursivas (simples). 'Tiene una sentencia "if' y una sentencia "while". Carece de casi todo lo demás. Un programa se compone de una secuencia de declaraciones de va- riables y funciones. Una función main se debe declarar al último. La ejecución comienza con una llamada a rnain.'

Como un ejemplo de un programa en C-Minus, en la figura 1 .S escribimos el programa factorial de la figura 1.4 usando una función recursiva. La entradalsalida en este progra- ma es proporcionada por una función read y una función write que se pueden definir en términos de las funciones estándar de C scanf y grintf.

C-Minus es un lenguaje más complejo que TINY, particularmente en sus requerimien- tos de generación de código, pero la máquina TM todavía es un objetivo razonable para su compilador. En el apéndice A proporcionamos una guía sobre cómo modificar y extender el compilador TINY a C-Minus.

int fact( int x )

/ * función factorial recursiva * / { if (X > 1)

return x * fact(x-1); else

return 1;

1

void main( void )

{ int x;

x = read() ; if (x > O ) write( fact(x) );

1

EJERCICIOS 1. I Seleccione un compilahr conocido que venga empacado con un ambiente de desarrollo, y haga una lista de todos los programas acompaiíantes que se encuentran disponibles con el compilador junto con una breve descripción de sus funciones.

1.2 Dada la asignación en C

dibuje un irbol de análisis gramatical y un árbol sintáctico para la expresión utilizando como guía el ejemplo semejante de la sección 1.3.

1.3 Los emres de compilación pueden dividirse aproximadamente en dos categorías: errores sin- tácticos y errores semánticos. Los errores sintácticos incluyen tokens olvidados o colocados de

2. Para que sea consistente con otras funciones en C-Miiius, main es declarüda como una función void ("sin tipo") con una lista de parámetros void. Mientras qne esto difiere del C ANSI, muchos compiladores de C aceptaran csta anotación.

Page 32: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. I I INTRODUCCI~N

manera incorrecta, tal como el paténtesis derecho olvidado en la expresión aritmética (2+3 . Los errores semánticos incluyen tipos incorrectos en expresiones y variables no declaradas (en la mayoría de los lenguajes), tal como la asignación x ;. 2, donde x es una variable de tipo arreglo. a. Proporcione dos ejemplos más de errores de cada clase en un lenguaje de su elección. b. Seleccione un compilador con el que esté familiarizado y determine si se enumeran todos

los errores sintácticos antes de los ermres semibticos o si los errores de sintaxis y de semántica están entremezclados. ¿Cómo influye esto en el número de pasadas'?

1.4 Esta pregunta supone que usted trabaja cowun compillidor que tiene una opción para producir salida en lenguaje ensamblador. a. Determine si su compilador realiza optiiiiizaciones de incorporación de constantes. b. Una optimización relacionada pero ntás avanzada es la de propagación de constantes: una

variable que actualmente tiene un valor constante es reemplazada por ese valor en las ex- presiones. Por ejemplo, el código (en sintaxis de C)

se reemplazaría, utilizando propagación de constantes (e incorporación de constantes), por el código

Determine si su compilador efectuó proptación de constantes. c. Proporcione tantas razones como pueda de por qué la propagación de constantes es más

difícil que la incorporación de constantes. d. Una situación relacionada con la propagación de constantes y la incorporación de constantes

es el uso de las constantes nombradas en un programa. Utilizando una constante nombrada x en vez de una variable podemos traducir el ejemplo anterior como el siguiente código en C:

const int x = 4;

e..

y = x + 2 ;

Determine si su compilador realiza propagaciÓn/incorporación bajo estas circunstancias. ¿En qué difiere esto del inciso b?

1.5 Si su compilador aceptara la entrada directamente desde el teclado, determine si dicho compi- lador lee el programa entero antes de generar mensajes de error o bien genera mensajes de error a medida que los encuentra. ¿Qué implicación tiene hacer esto en el número de pasadas?

1.6 Describa las tareas que realizan los programas siguientes y explique en qué se parecen o se re- lacionan con los compiladores: a. Un preprocesador de lenguaje b. Un módulo de impresión c. Un formateador de texto

1.7 Suponga que tiene un traductor de Pascal a C escrito en C y un compilador trabajando para C. Utilice diagramas T para describir los pasos que tornaría para crear un compilador trabajando pira Pascal.

1.8 Utilizamos una flecha * para indicar la reducción de un patrón de dos diagramas T a un solo diagrama T. Podemos considerar esta flecha como una "relación de reduccih" y formar su ce- rradura transitiva **. en la cual permitimos que tenga lugar una secuencia de redncciones.

Page 33: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Notar y referencias 29

Dado el siguiente diagrama, en el que las letras establecen lenguajes arbitrarios, determine cuáles lenguajes deben ser iguales para que La reducción sea válida, y muestre los pasos de la reducción simple que la hacen válida:

Proporcione un ejemplo prktico de la reducción que describe este diagrama. 1.9 Una alternativa al método de transportar un compilador descrito en la sección 1.6 y en la figura

1.3 es utilizar un intérprete para el código intermedio producido por el compilador y suprimir una etapa final del todo. Un método así es utilizado por el sistema P de Pascal, el cual inclu- ye un compilador de Pascal que produce códiga P, una clase de código ensamblador para una miquina de pila "genérica" y un intérprete de código P que,simula la ejecución del código P. Tanto el compilador de Pascal como el intérprete de código P están escritos en código P. a. Describa los pasos necesarios para obtener un compilador trabajando para Pascal en una

máquina arbitra& dado un sistema P de Pascal. b. Describa los pasos necesarios para obtener un compilador trabajando en el cúdigo nativo

de su sistema, en el inciso a (es decir, un compilador que produce código ejecutable para la máquina anfitrión, en vez de utilizar el intérprete de código P).

1.10 El proceso de transportar un compilador puede ser considerado como dos operaciones distintas: reasignaeión del objetivo (la modificación del compilador para producir código objetivo para una nueva máquina) y reasignación del anfitrión (la modificación del compilador para ejecu- tarse en una nueva miquina). Analice las diferencias de estas dos operaciones en términos de diagramas T.

IOTAS P La mayoría de los temas mencionados en este capítulo se tratan con más detdle en capítu-

REFERENCIAS lossubsiguientes, y las notas y referencias de esos capítulos proporcionarán las referencias adecuadas. Por ejemplo, Lex se estudia en el capítulo 2; Yace en el capítulo 5; la verifica- ción de tipos, tablas de símbolos y análisis de ahlbutos en el capítulo 6; la generación de có- digo, el código en tres direcciones y el código P en el capítulo 8; finalmente, el manejo de errores en los capítulos 4 y 5.

Una referencia estándar completa acerca de los compiladores es Aho [1986], particu- larmente para la teoría y los algoritmos. Un texto que ofrece muchas sugerencias útiles de implementación es Fischer y LeBlanc [199 1 f . Pueden hallarse descripciones completas de los compiladores de C en Fraser y Hanson [1995] y Holub 119901. Un compilador popu- lar de C/C++ cuyo código fuente se encuentra ampliamente disponible a través de Intemet es el compilador Gnu. Éste se describe con detalle en Stallman [1994].

Para una visión general de los conceptos de los lenguajes de programación, con in- formación acerca de sus interacciones con los traductores, véase Louden [1993] o Sethi [1996].

Una útil referencia para la teoría de autómatas desde un punto de vista matemático (en oposición al punto de vibta práctico adoptado aquí) es la de Hopcroft y Ullman [1979]. Puede encontrarse también ahí información adicional acerca de la jerarquía de Chomsky (a\í como en el capítulo 3).

Page 34: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Una descripción de los primeros compiladores FORTRAN puede encontrarse en Backus [19.57] y Backus 119811. Una descripción de un antiguo compilador Algo160 puede hallarse en Randell y Russell [1964]. Los compiladores de Pascal se describen en Barron [1981], donde también puede encontrarse una descripción del sistema P de Pascal (Nori [1981]).

El preprocesador Ratfor mencionado en la sección 1.2 se describe en Kernighan [197.5]. Los diagramas T de la sección 1.6 fueron introducidos por Bratman [1961].

Este texto se enfoca en las tkcnicas de traducción estándar útiles para la traducción de la mayoría de los lenguajes. Es posible que se necesiten otras técnicas adicionales para la traducción eficaz de los lenguajes ajenos a la tradición principal de los lenguajes imperati- vos basados en Algol. En particular, la traducción de los lenguajes funcionales tales como ML y Haskell ha sido la fuente de muchas nuevas técnicas, algunas de las cuales pueden lle- gar a ser importantes técnicas generales en el futuro. Las descripciones de estas técnicas pueden encontrarse en Appel[1992], Peyton Jones 119921 y Peyton Jones [19871. Este últi- mo contiene una descripción de la verificación de tipos de Hindley-Milner (mencionada en la seccidn t . 1).

Page 35: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Capítulo 2

Rastreo o análisis Iéxico

2.1 El proceso del análisis léxico 2.5 Implementación de un

2.2 Expresiones regulares analizador léxico TlNY 2.3 Autómatas finitos 2.6 Uso de Lex para generar

2.4 Desde las expresiones automáticamente un

regulares hasta los DFA analizador Iéxico

La fase de rastreo, o análisis Iéxico, de un compilador tiene la tarea de leer el programa fuente como un archivo de caracteres y dividirlo en tokens. Los tokens son como las pala- bras de un lenguaje natural: cada token es una secuencia de caracteres que representa una unidad de infofmación en el programa fuente. Ejemplos típicos de token son las palabras reservadas, como if y while, las cuales son cadenas fijas de letras; los identifica- dores, que son cadenas definidas por el usuario, compuestas por lo regular de letras y números, y que comienzan con una letra; los símbolos especiales, como los símbolos aritméticos + y *; además de algunos símbolos compuestos de múltiples caracteres, tales como > = y <>. En cada caso un token representa cierto patrón de caracteres que el analizador Iéxico reconoce, o ajusta desde el inicio de los caracteres de entrada restantes.

Como la tarea que realiza el analizador Iéxico es un caso especial de coincidencia de pa- trones. necesitamos estudiar métodos de especificación y reconocimiento de patrones en la medida en que se aplican al proceso de análisis Iéxico. Estos métodos son principalmente los de las expresiones regulares y los autómatas finitos. Sin embargo, un analizador Iéxico también es la parte del compilador que maneja la entrada del código fuente, y puesto que esta entrada a menudo involucra un importante gasto de tiempo, el analizador Iéxico debe funcionar de manera tan eficiente como sea posible. Por lo tanto, también necesitamos po- ner mucha atención a los detalles prácticos de la estructura del analizador Iéxico.

Dividiremos el estudio de las cuestiones del analizador Iéxico como sigue. En primer lugar, daremos una perspectiva general de la función de un analizador Iéxico y de las es- tructuras y conceptos involucrados. Enseguida, estudiaremos las expresiones regulares, una notactón estándar para representar los patrones en cadenas que forman la estructu- ra Iéxica de un lenguaje de programación. Continuaremos con el estudio de las máquinas de estados finitos, o autómatas finitos, las cuales representan algoritmos que permiten reconocer los patrones de cadenas dados por las exprestones regulares. También estu- diaremos el proceso de construcción de los autómatas finitos fuera de las expresiones

Page 36: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 1 RASTREO O ANALISIS L ~ X I C O

regulares. Después volveremos a los métodos prácticos para escribir programas que im- plementen los procesos de reconocimiento representados por los autómatas finitos y estudiaremos una implementación completa de un analizador Iéxico para el lenguaje TINY. Por último, estudiaremos la manera en que se puede automatizar el proceso de producción de un programa analizador léxico por medio de un generador de analizador Iéxico, y repetiremos la implementación de un analizador léxico para TINY utilizando Lex, que es un generador de analizador léxico estándar disponible para su uso en Unix y otros sistemas.

2.1 EL PROCESO DEL ANALISIS LÉXICO El trabajo del analizador Iéxico es leer los caracteres del código fuente y formarlos en uni- dades lógicas para que lo aborden las partes siguientes del compilador (generalmente el ana- lizador sintáctico). Las unidades lógicas que genera el analizador Iéxico se denominan to- kens, y formar caracteres en tokens es muy parecido a formar palabras a partir de caracteres cn una oración en un lenguaje natural como el inglés o cualquier otro y decidir lo que cada palabra significa. En e5to se asemeja a la tarea del deletreo.

Los tokens son entidades lógicas que por lo regular se definen corno un tipo enumera- do. Por ejemplo, pueden definirse en C como'

typedef enum

(IF.THEN,ELSE,PLUS,MINUS,NüIú,ID, ... 1 TokenType ;

Los tokens caen en diversas categorías, una de ellas la constituyen las palabras reserva- das, como I F y THEN, que representan las cadenas de caracteres "if' y "then". Una segunda categoría es La de Los símbolos especiales, como los símbolos aritméticos MÁs y MENOS, los que se representan con los caracteres "+" y "-". Finalmente, existen tokens que pueden repre- sentar cadenas de múltiples caracteres. Ejemplos de esto son NUM e ID, los cuales represen- tan números e identificadores.

Los tokens como entidades lógicas se deben distinguir claramente de las cadenas de ca- racteres que representan. Por ejemplo, el token de la palabra reservada I P se debe distinguir de la cadena de caracteres "if' que representa. Para hacer clara la distinción, la cadena de caracteres representada por un token se denomina en ocasiones su valor de cadena o su lexema. Algunos tokens tienen sólo un lexema: las palabras reservadas tienen esta propie- dad. No obstante, un token puede representar un número infinito de lexemas. Los identifi- cadores, por ejemplo, están todos representados por el token siniple ID, pero tienen muchos valores de cadena diferentes que representan sus nombres individuales. Estos nombres no se pueden pasar por alto, porque un compilador debe estar al tanto de ellos en una tabla de sím- bolos. Por consiguiente, un rastreador o analizador Iéxico también debe construir los valo- res de cadena de por lo menos algunos de los tokens. Cualquier valor asociado a un token

1. En un lenguaje sin tipos enumerados tendrfamos que definir los tokens directamente como va- lores numéricos sirnb6licos. Así, al estilo antiguo de C, m ocasiones .se vafa !o sigiirnte:

#define IF 256

#de£ ine THEN 257

#define ELSE 258

. . . i h tos ntímcros comienzan en 256 p:ira evitar que sc cunfundan con los valores ASCll numéricos.)

Page 37: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

El proceso del analirir lkxm 33

se denomina atributo del token, y el valor de cadena es un ejemplo de un atributo. Los to-- kens también pueden tener otros atributos. Por ejemplo, un token NUM puede tener un atri- buto de valor de cadena corno "32767", que consta de cinco caracteres numéricos, pero tam- bién tendrá un atributo de valor numérico que consiste en el valor real de 32767 calculado a partir de su valor de cadena. En el caso de un token de símbolo especial conlo MÁs (PLUS), no sólo se tiene el valor de cadena "+", sino también la operacicín aritmética real -i- que estii asociada con él rnismo. En realidad, el sírnholo del token rnisnto se puede ver simplemente como otro atributo, mientras que el token se puede visiralizar corno la colec- ción de todos sus atributos.

Un analizador léxico necesita calcular a1 menos tantos atributos de un token como sean necesarios para permitir el procesamiento siguiente. Por ejemplo, se necesita calcular el va- lor de cadena de un token NUM, pero no es necesario calcular de inmediato su valor numé- rico, puesto que se puede calcular de su valor de cadena. Por otro lado, si se calcula su valor numérico, entonces se puede descartar su valor de cadena. En ocasiones el mismo analizador Iéxico puede realizar las operaciones necesarias para registrar un atributo en el lugar apropiado, o puede simplemente pasar el atributo a una fase posterior del compi- lador. Por ejemplo, un analizador Iéxico podría utilizar el valor de cadena de un identifi- cador para introducirlo a la tabla de símbolos. o podría pasarlo para introducirlo en una eta- pa posterior.

Puesto que el analizador Iéxico posibleinente tendrá que calcular varios atributos para cada token, a menudo es útil recolectar todos los atributos en un solo tipo de datos estntc- turados, al que podríamos denominar como registro de token. Un registro así se podría de- clarar en C como

typedef struct

{ TokenType tokenval;

char * stringval; int numval;

} TokenRecord;

» posiblemente como una unión

typedef struct

{ TokenType tokenva.1;

union

{ char * stringval; int nmval;

) attribute;

1 TokenRecord;

(lo que supone que el atributo de valor de cadena sólo es necesario para identificadores y el atributo de valor numérico sólo para números). Un arreglo mis común es que el analizador Iéxico solamente devuelva el valor del token y coloque los otros atributos en variables don- de se pueda tener acceso a ellos por otras partes del compilador.

Aunque la tarea del analizador Iéxico es convertir todo el programa fuente en una se- cuencia de tokens, pocas veces el maliziidor hará todo esto de una vez. En realidad, el ana- lizador Iéxico funcionará b?j« el control del analizador sintáctico, devolviendo el siguiente token simple desde la entrada bajo demanda mediante una función que tendrá una tlecliira- c i h similar a la declaración cn el lenguaje C

TokenType getTokenívoid);

Page 38: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 1 RASTREO O ANALISIS LÉXICO

La función getToken declarada de esta manera devolverá, cuando se le llame, el siguien- te token desde la entrada, y además calculará atributos adicionales, como el valor de cade- na del token. La cadena de caracteres de entrada por lo regular no tiene un parámetro para esta función, pero se conserva en un buffer (Localidad de memoria intermedia) o se propor- ciona mediante las facilidades de entrada del sistema.

Como ejemplo del funcionamiento de getToken considere la siguiente línea de códi- go fuente en C, que utilizamos como ejemplo en el capítulo 1:

Suponga que esta línea de código se almacena en un buffer de entrada como qigue, con el siguiente carácter de entrada indicado por la flecha:

Una llamada a getToken necesitará ahora saltarse los siguientes cuatro espacios en blau- co, reconocer la cadena "a" compuesta del carácter único a como el token siguiente, y de- volver el valor de token I D como el token siguiente, dejando el buffer de entrada como se aprecia a continuación:

De esta manera, una llamada posterior a getToken comen~ará de nuevo el proceso de re- conocimiento con el carácter de corchete izquierdo.

Volveremos ahora al estudio de métodos para definir y reconocer patrones en cadenas de caracteres.

2.2 EXPRESIONES REGULARES Las expresiones regulares representan patrones de cadenas de caracteres. Una expresión re- gular r se encuentra completamente definida mediante el conjunto de cadenas con las que concuerda. Este conjunto se denomina lenguaje generado por la expresión regular y se escribe como L(r). Aquí la palabra lenguaje se utiliza sólo.para definir "conjunto de cade- nas" y no tiene (por lo menos en esta etapa) una relación específica con un lenguaje de pro- gramación. Este lenguaje depende, en primer lugar, del conjunto de caracteres que se encuen- tra disponible. En general, estaremos hablando del conjunto de caracteres ASCII o de algún subconjunto del mismo. En ocasiones el conjunto será más general que el conjunto de ca- racteres ASCII, en cuyo caso los elementos del conjunto se describirán como símbolos. Este conjunto de símbolos legales se conoce como alfabeto y por lo general se representa me- diante el símbolo griego 2 (sigma).

Una expresión regular r también contendrá caracteres del alfabeto, pero esos caracteres tendrán un significado diferente: en una expresión regular todoo los símbolos indican putro- ttes. En este capítulo distinguiremos el uso de un carácter como patrón escribiendo todo los patrones en negritas. De este rnotlo, a es el carácter u usado corno patrón.

Page 39: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Expresiones regulares 3 5

Por último, una expresión regular r puede contener caracteres que tengan significados especiales. Este tipo de caracteres se llaman metacaracteres o metasímbolos, y por lo ge- neral no pueden ser caracteres legales en el alfabeto. porque no podríamos distinguir su uso como metacaracteres de su uso como miembros del alfabeto. Sin embargo, a nienudo no es posible requerir tal exclusión, por lo que se debe utilizar una convención para diferenciar los dos usos posibles de un metararheter. En tnuchas situaciones esto se realiza mediante el uso de un carácter de escape que "desactiva" el significado especial de un metacarácter. Unos caracteres de escape comunes son la diagonal inversa y las comillas. Advierta que los ca- racteres de escape, si también son caracteres legales en el alfabeto, son por sí niismos me- tacaracteres.

2.21 Definición de expresiones regulares Ahora estamos en posición de describir el significado de las expresiones regulares '1 , 1 esta- blecer cu;íies lenguajes genera cada patrón. Haremos esto en varias etapas. Comenzaremos por describir el conjunto de expresiones regulares básicas, las cuales se componen de sím- bolos individuales. Continuaremos con la descripción de las operaciones que generan nue- vas expresiones regulares a partir de las ya existentes. Esto es similar a la manera en que se construyen las expresiones aritméticas: las expresiones aritméticas básicas son Los números, tales como 43 y 2.5. Entonces las operaciones aritméticas, como la suma y la multiplicación, se pueden utilizar para formar nuevas expresiones a partir de las existentes, como en el ca- so de 43 2.5 y 43 * 2.5 + 1.4.

El grupo de expresiones regulares que describiremos aquí es mínimo, ya que sólo con- tiene los metasímbolos y las operaciones esenciales. Después considerarenios extensiones a este conjunto mínimo.

Expresiones regulares bdricar Éstas son precisamente los caracteres simples del alfabeto, los cuales se corresponden a sí mismos. Dado cualquier carácter cr del alfabeto Z, indicamos que la expresión regular a corresponde al crirácter u escribiendo L(a) = {a]. Existen otros dos sím- bolos que necesitaremos en situaciones especiales. Necesitamos poder indicar una concor- dancia con la cadena vacía, es decir, la cadena que no contiene ningún carácter. Utiliza- remos el símbolo E (épsilon) para denotar la cadena vacía, y definiremos el metasímbolo E

(e en negritas) estableciendo que L(E) = ( E J. También necesitaremos ocasionalmente ser capaces de describir un símbolo que corresponda a la ausencia de cadenas, es decir, cuyo lenguaje sea el conjunto vacío, el cual escribiremos como ( }. Emplearemos para esto el símbolo + y escribiremos L ( 4 ) = ( ). Observe la diferencia entre ( ) y ( o } : el conjunto ( ) no contiene ninguna cadena, mientras que el conjunto ( E 1 contiene la cadena simple que no se compone de ningún carácter.

Operaciones de expresiones regulares Existen tres operaciones básicas en las expresiones regula- res: 1) selección entre alternativas, la cual se indica mediante el metacarácter I (barra ver- tical): 2 ) concatenación, que se indica mediante yuxtaposición (sin un metacarácter), y 3) repetición o "cerradura", la cual se indica mediante el metacarácter *. Anaiizaremos cada una por turnol proporcionando la construcción del conjunto correspondiente para los lengua- jes de cadenas concordantes.

Selección entre alternativas Si r y s son gxpresiones regulares, entonces r I s es una expresión regular que define cualquier cadena que concuerda con r o con .S. En términos de len- guajes. cl lenguaje de rl .Y es la unión de los lenguajes de r y ,Y, o L(rI .S) = L(r) u L(s). Corno un cjemplo simple, considere la expresión regular al b: ésta corresponde tanto al

Page 40: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

carácter a como al carácter b, es decir, L(a 1 b) = L(a) U L(b) = { a ) U ( b ) = {a, b ) . Co- rno segundo ejemplo, la expresión regular a l E corresponde tanto al carácter simple a como a la cadena vacía (que no está compuesta por ningún carácter). En otras palabras, L(a I E ) = {a, E ) .

La selección se puede extender a más de una alternativa, de manera que, por ejemplo, L(a l b I c 1 d) = {a, b, c d ) . En ocasiones también escribiremos largas secuencias de seleccio- nes con puntos, como en a I b I . . . I z, que corresponde a cualquiera de las letras minhcu- las de la a a la z.

Concatenación La concatenación de dos expresiones regulares r y s se escribe como rs, y co- rresponde a cualquier cadena que sea la concatenación de dos cadenas, con la primera de ellas correspondiendo a r y la \egunda correspondiendo a s. Por ejemplo, ta expresión regu- lar ab corresponde sólo a la cadena lb, mientras que la expresión regular í a I b) c corres- ponde a las cadenas ac y bc. (El uso de los paréntesis como metacaracteres en esta expre- sión regular se explicará en breve.)

Podemos describir el efecto de la concatenación en términos de lenguajes generados al definir la concatenación de dos conjuntos de cadenas. Dados dos conjuntos de cadenas S, y S2, el conjunto concatenado de cadenas SlS2 es el conjunto de cadenas de SI com- plementado con todas las cadenas de S2. Por ejemplo, si S, = {m, b ) y S2 = {a, bb), enton- ces S1S2 = {aaa, aabb, ha, bbb). Ahora la operación de concatenación para expresiones re- gulares se puede definir como sigue: L(rs) = L(r)L(s). De esta manera (utilizando nuestro ejemplo anterior), L ( í a I b ) c ) = L(aI b)L(c) = (a, b } { c ) = {ac, bc}.

La concatenación también se puede extender a más de dos expresiones regulares: L(rl r, . . . r,) = L(rl)L(r2) . . . L(r,) = el conjunto de cadenas formado al concatenar todas Las cadenas de cada una de las L(rl), . . . , L(r,).

Repetición La operación de repetición de una expresión reguIar, denominaúa también en ocasiones cerradura (de Kleene), se escribe r*, donde r e s una expresión regular. La ex- presión regular r* corresponde a cualquier concatenación finita de cadenas, cada una de las cuales corresponde a r. Por ejemplo, a* corresponde a las cadenas E, a, aa, aaa, . . . . (Concuerda con E porque E es la concatenación de ninguna cadena concordante con a.) Po- demos definir la operación de repetición en términos de lenguajes generados definiendo, a su vez, una operación similar * para conjuntos de cadenas. Dado un conjunto S de ca- denas, sea

S* = ( E ) u S u SS U SSS u

Ésta es una unión de conjuntos infinita, pero cada uno de sus elementos es una concatena- ción finita de cadenas de S. En ocasiones el conjunto S* se escribe como sigue:

donde S" = S. . .S es la concatenación de S por n veces. (So = ( E ] .) Ahora podemos definir la operación de repetición para expresiones regulares como

sigue:

Page 41: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Expresiones regulares 3 7

Considere como ejemplo la expresión regulaf ( a I bb) *. (De nueva cuenta, la razón (te tener paréntesis como metacaracteres se explicará más adelante.) Esta expresión regular co- rresponde a cualquiera de las cadenas siguientes: E, a, bb, uu, trbh, hbu, hbhh, ma, iicihh y así sucesivaniente. En términos de lenguajes, L((a 1 bb) *) = L(a I bb)" = ((1, hh) * = (e , 11,

hh, aa, c~bb, hhn, hbbb, uciu, cmhb, cihbci, abbbh, bhriu, . . . ) .

Precedencia de operaciones y el uso de los paréntesis La descripción precedente no toma en cuen- ta la cuestión de la precedencia de las operaciones de elección, concatenación y repetición. Por ejemplo, dada la expresión regular a 1 b*, ¿deberíamos interpretar esto como ( a I b ) * o como a I ( b * ) ? (Existe una diferencia importante, puesto que L( ( a I b ) *) = (e;, 11, h, uu, (lb, ha, hh. . . . ), mientras que L(a I (b* ) ) = (e , u, b, hb, bhb, . . . j .) La convención estándar es que la repetición debería tener mayor precedencia, por lo tanto, la segunda interpretación es la correcta. En realidad, entre las tres operaciones, se Le da al * la precedencia tnhs alta. a la concatenación se le da la precedencia que sigue y a la I se le otorga la precedencia más ba- ja. De este modo, por ejemplo, a i be* se interpreta como a I ( b ( c * ) ) , mientras que ab I c*d se interpreta como (ab ) I ( ( c * ) d ) .

Cuando deseemos indicar una precedencia diferente, debemos usar paréntesis para hacerlo. Ésta es la razón por la que tuvimos que escribir ( a I b ) c para indicar que la ope- ración de elección debería tener mayor precedencia que la concatenación, ya que de otro modo a I bc se interpretaría como si correspondiera tanto a a como a bc. De manera similar, (al bb) * se interpretaría sin los paréntesis como al bb*, lo que corresponde a a, b, hh, bbh, . . . . Los paréntesis aquí se usan igual que en aritmética, donde ( 3 + 4) * 5 = 35, pero 3 + 4 * 5 = 23, ya que se supone que * tiene precedencia más alta que +.

Nombres para expresiones regulares A menudo es útil como una forma de simplificar la notación proporcionar un nombre para una expresión regular larga, de modo que no tengamos que escribir la expresión misma cada vez que deseemos utilizarla. Por ejemplo, si deseáramos desarrollar una expresión regular para una secuencia de uno o más dígitos numéricos, en- tonces escribisíamos

o podríamos escribir

dígi to dígito*

donde

dígito = 011121. ..19

es una definición regular del nombre digi to. El uso de una definición regular es muy conveniente, pero introduce la complicación

agregada de que el nombre mismo se convierta en un metasímbolo y se deba encontrar un significado para distinguirlo de la concatenación de sus caracteres. En nuestro caso hicimos esa distinción al utilizar letra cursiva para el nombre. Advierta que no se debe emplear el nombre del término en su propia definición (es decir, de manera recursiva): debemos poder eliminar nombres reemplazándolos sucesivamente con las expresiones regulares para las que se establecieron.

Antes de considerar una serie de ejemplos para elaborar nuestra definición de expresio- nes regulares, reuniremos todas las piezas de la definición de una expresión regular.

Page 42: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Definición

Una expresión regular es una de las siguientes:

l. Una expresión regular básica constituida por un solo carácter a , donde cr proviene de un alfabeto 2 de caracteres legales; el metacaricter E; o el metacarácter +. En el pri- mer caso, L(a) = { a ) ; en el segundo, L(E) = ( E ) ; en el,tercero, L(+) = ( 1.

2. Una expresión de la forma r 1 S, donde r y s son expresiones regulares. En este ca- so, L ( r l S) = Lfr) u L(s).

3. Una expresión de la forma rs, donde r y s son expresiones regulares. En este caso, L(rs) = L(r)L(s).

4. Una expresión de la forma r*, donde r es una expresión regular. En este caso, L(r*) = L(r)*.

5. Una expresión de la forma ( r ) , donde r es una expresión regular. En este caso, L(( r ) ) = L(r). De este modo, los paréntesis no cambian el lenguaje, sólo se utilizan para ajustar la precedencia de las operaciones.

Advertimos que, en esta definición, la precedencia de las operaciones en (2), (3) y (4) está en el orden inverso de su enumeración; es decir, I tiene precedencia más baja que la concatenación, y ésta tiene una precedencia más baja que el asterisco *. También adverti- mos que esta definición proporciona un significado de metacaricter a los seis símbolos 4, E, l>*, (, 1 .

En lo que resta de esta sección consideraremos una serie de ejemplos diseñados para ex- plicar con más detalles la definición que acabamos de dar. Éstos son algo artificiales, ya que por lo general no aparecen como descripciones de token en un lenguaje de programación. En la sección 2.2.3 consideraremos algunas expresiones regulares comunes que aparecen con frecuencia como tokens en lenguajes de programación.

En los ejemplos siguientes por lo regular se incluye una descripción en idionia coloquial de las cadenas que se harán corresponder, y la tarea es traducir la descripción a una expre- si6n regular. Esta situación, en la que un manual de lenguaje contiene descripciones de los tokens, es la que con más frecuencia enfrentan quienes escriben compiladores. En ocasio- nes puede ser necesario invertir la dirección, es decir, desplazarse de una expresión regular hacia una descripción en lenguaje coloquial, de modo que también incluiremos algunos ejer- cicios de esta clase.

Ejemplo 2.1 Consideremos el alfabeto simple constituido por sólo tres caracteres alfabéticos: 2 = (a , h. c j. También el conjunto de todas las cadenas en este alfabeto que contengan exactamente una h. Este conjunto e5 generado por la expresión regular

í a l c ) * b ( a l c ) *

Advierta que, aunque b aparece en el centro de la expresión regular, la letra h no necesita estar en el centro de la cadena que se desea definir. En realidad, la repeticibn de n o c antes y después de la b puede presentarse en diferentes números de veces. Por consiguiente, todas las cadenas siguientes estrín generadas mediante la expresión regular anterior: b, chc, cibn- m , btrcincic, ccbucci, crcccch. S

Page 43: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Exprer~oner regulares 39

Ejemplo 2.3

Con el mismo alhbeto que antes, considere el conjunto de todas las cadenas que contienen como rnáxitno una h. Una expresión regular para este conjunto se puede obtener utili~ando la soli~ción al ejemplo :iriierior conio una alternativa (definiendo aquellas cadenas con exac- tarnente una h) y la expresión regular (a I c ) * corno la otra alternativa (riefinietitlo los ca- sos sin h en todo). De este niodo, tenernos la soli1ci6n siguiente:

Una solución alternativa sería permitir que b o la cadena vacía apareciera entre las dos re- peticiones de i r o <.:

Este ejemplo plantea un punto importante acerca de las expresiones regulares: el mismo lenguaje se puede generar mediante muchas expresiones regulares diferentes. Por lo gene- ral, intentamos encontrar una expresión regular tan simple conio sea posible para describir un conjunto de cadenas, aunque nunca intentaremos demostrar que encontramos, de hecho, la "más simple": la más breve por ejemplo. Existen dos ruones para esto. La primera es que raramente se presenta en situaciones prácticas. donde por lo regular existe una solución es- tandar "más simple". La segunda es que cuando estudiemos métodos para reconocer expre- siones regulares, los algoritmos tendrán que poder simplificar el proceso de reconocimien- to si11 molestme en simplificar primero la expresión regular. 5

Consideremos el conjunto de cadenas S sobre el alfabeto 2: = {o, bJ compuesto de una h siniple rodeada por el mismo número de u:

S = (h , nhu, czuban, c~c~ahuarz, . . .) = (a"hanln # 0)

Este conjunto no se puede describir mediante una expresión regular. La razón es que la úni- ca operación de repe~ición que tenemos es la operación de cerradura *, la cual permite cual- quier número de repeticiones. De modo que si escribimos la expresión a*ba* (lo más cer- cano que podemos obtener en el caso de una expresión regular para S), no hay garantía de que el número de u antes y después de la h será el mismo. Expresanios esto al decir que "las expresiones regulares no pueden contar". Sin embargo, para proporcionar una demostración niatenxítica de este hecho, requeriríamos utilizar un famoso teorema acerca de las expresio- nes regulares conocido como lema de 1a.estracción (pumping Ienima), que se estudia en la teoría de autómatas, pero que aquí ya no volverenios a mencionar.

Es evidente que no todos los conjuntos de cadenas que podemos describir en términos simples se pueden generar mediante expresiones regulares. Por consiguiente, un conjunto de cadenas que es el lenguaje para una expresión regular se distingue de otros conjuntos al denominarlo conjunto regular. De cunndo en cuando aparecen conjuntos no regulares co- mo cadenas en lenguajes de programación que necesitan ser reconocidos por un analizador léxico, los cuales por 10 regular son abortados cuando surgen. Retotnarem«s este tema de iilaiiera breve en la sección en que se abordan las consideraciones para el analizador léxico prjctico. §

jemplo 2.4 Considerenios las cadenas en el alfabeto 5 = { r i , h, c J que no coritierieii dos h co~isecutivas. De modo que, entre cualcsquicra dos 17, debe haber por lo tnenos una (i o una c. Clonstniirenios

Page 44: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 1 RASTREO O A N ~ L I S I S LÉXICO

una expresión regular para este conjunto en varias etapas. En primer lugar, podemos obligar a que una a o c se presenten después de cualquier b al escnbir

( b ( a l c ) ) *

Podemos combinar esto con la expresión (a 1 c ) *, la cual define cadenas que no tienen b alguna, y escribimos

( ( a l c ) * l ( b ( a l c ) ) * ) *

o, advirtiendo que (r* I S* ) * corresponde con las mismas cadenas que (r I S)*

(icuidado! Ésta todavía no es la respuesta correcta.) El lenguaje generado por esta expresión regular tiene, en realidad, la propiedad que bus-

camos, a saber, que no haya dos b consecutivas (pero aún no es lo bastante correcta). En ocasiones podremos demostrar tales aseveraciones, así que esbozaremos una demostración de que todas las cadenas en L( (al c Iba I bc) *) no contienen dos b consecutivas. La de- mostración es por inducción sobre la longitud de la cadena (es decir, el número de caracte- res en la cadena). Es evidente que esto es verdadero para todas las cadenas de longitud O, 1 o 2: estas cadenas son precisamente las cadenas E , u, c, au, ac, ea, CC, bu, be. Ahora supon- gamos que es verdadero para todas las cadenas en el lenguaje con una longitud de i < n, y sea s una cadena en el lenguaje con longitud n > 2. Entonces, s contiene más de una de las cadenas no-E anteriormente enumeradas, de modo que s = sls2, donde S , y s2 también se en- cuentran en el lenguaje y no son E . Por lo tanto, mediante la bipótesis de inducción, tanto S ,

como s2 no tienen dos b consecutivas. Por consiguiente, la única manera de que s misma pu- diera tener dos b consecutivas sería que s, finalizara con una b y que comenzlira con una b. Pero esto es imposible, porque ninguna cadena en el lenguaje puede finalizar con una b.

Este último hecho que utilizamos en el esbozo de la demostración (o sea, que ninguna cadena generada mediante la expresión regular anterior puede finalizar con una b) también muestra por qué nuestra solución todavía no es bastante correcta: no genera las cadenas b, ub y cb, que no contienen dos b consecutivas. Arreglaremos esto agregando una b opcional rezagada de la manera siguiente:

Advierta que la imagen especular de esta expresión regular también genera el lenguaje dado

( b l e ) ( a l c l a b l c b ) *

También podríamos generar este mismo lenguaje escribiendo

( n o t b l b n o t b ) * ( b I e )

donde notb = a I c: Éste es un ejemplo del uso de un nombre para una subexpresión. De hecho, esta solución es preferible en los casos en que el alfabeto es grande, puesto que la definición de notb se puede ajustar para incluir todos los caracteres, excepto b, sin corn- plicar la expresión original. 9

Page 45: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Exprerioner regulares 41

En este ejemplo proporcionamos la expresión regular y solicitamos deterniinar una descrip- ción concisa en español del lenguaje que genera. Considere el alfabeto C = { a , h, c ) y la ex- presión regular

Esto genera el 1engua.je de todas las cadenas que contengan un número par de a. Para ver es- to considere la expresión dentro de la repetición exterior kzquierda:

Esto genera aquellas cadenas que finalizan en li y que contienen exactatnente dos a (cual- quier núniero de h y de c puede aparecer antes o entre las dos u). La repetición de estas co- denas nos da todas las cadenas que finalizan en u cuyo número de a es un múltiplo de 2 (es decir, par). A l añadir la repetición (b I c) * al final (como en el ejemplo anterior) obtene- mos el resultado deseado.

Advertimos que esta expresión regular también se podría escribir como

(nota* a nota* a)* nota*

2 2 2 Extensiones para las expresiones regulares Formulamos una definición para las expresiones regulares que emplean un conjunto míni- mo de operaciones comunes a todas las aplicaciones, y podríamos limitarnos a utilizar sólo las tres operaciones básicas (junto con paréntesis) en todos nuestros ejemplos. Sin embargo, ya vimos en los ejemplos anteriores que escribir expresiones regulares utilizando svlo estos operadores.en ocasiones es poco manejable, ya que se crean expresiones regulares que son más complicadas de lo que serían s i se dispusiera de un conjunto de operaciones más expre- sivo. Por ejemplo, sería útil tener una notación para una coincidencia de cualquier carácter (ahora tenemos que enumerar todos los caracteres en el alfabeto como una larga alternati- va). Además, ayudaría tener una expresión regular para una gama de caracteres y una ex- presión regular para todos los caracteres excepto uno.

En los párrafos siguientes describiremos algunas extensiones a las expresiones regu- lares estándar ya analizadas, con los correspondientes metasíinbolos nuevos, que cubren éstas y situaciones comunes semejantes. En la mayoría de estos casos no existe una ter- minología común, de modo que utilizaremos una notación similar a la empleada por el ge- nerador de analizadores léxicos Lex, el cual se describe niás adelante en este capítulo. En realidad, muchas de las situaciones que describiremos aparecerán de nuevo en nuestra descripción de Lex. No obstante, no todas las aplicaciones que utilizan expresiones regu- bares incluirán estas operaciones, e incluso aunque l o hicieran, se puede emplear una no- tación diferente.

Ahora pasaremos a nuestra lista de nuevas operaciones.

UNA O MAS REPETICIONES Dada una expresión regular p., la repetición de r se describe utilizando la operación de cerradura estándar, que se escribe r*. Esto permite que r se repita O o más veces. Una situación típica que surge es la necesidad de trrrci o más repeticiones en lugar de nin- guna, lo que garantiza que aparece por lo tnenos una cadena correspondiente a r, y no permite la cadena vacía c. Un ejemplo cs el de un número natural, donde queremos una secuencia de dígitos, pero deseamos que por lo menos aparezca uno. Por ejemplo. si tlescamos definir números binarios, podríamos escribir (O I 1) *, pero esto tnmhién

Page 46: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 1 PgiSTREO O ANALISIS LEXICO

coincidirá con la cadena vacía, ]a cual no es un número. Por supuesto, podríamos escribir

pero esta situación se presenta con tanta frecuencia que se desarrolló para ella una no- tación relativamente estándar en la que se utiliza + en lugar de *: rc, que indica una o más repeticiones de r. De este modo, nuestra expresión regular anterior para números biiiarios puede escribirse ahora como

CUALQUIER CARACTER Una situación común es la necesidad de generar cualquier carácter en el alfabeto. Sin una operación especial esto requiere que todo carácter en el alfabeto sea enumerado en una alternativa. Un metacarácter típico que se utiliza para expresar una concordancia de cualquier carácter es el punto ".", el cual no requiere que el alfabeto se escriba realmente en forma extendida. Con este metacarácter pode~nos escribir una expre- sión regular para todas las cadenas que contengan al menos una h como se muestra a continuación:

UN INTERVALO DE CARACTERES A menudo necesitamos escribir un intervalo de caracteres, como el de todas las letras minúsculas o el de todos los dígitos. Hasta ahora hemos hecho esto utilizando la nota- ción a I b I . . . I z para las letras minúsculas o O I 1 I . . . 1 9 para los dígitos. Una alter- nativa es tener una notación especial para esta situación, y una que es común es ta de emplear corchetes y un guión, como en [a- z 1 para las letras minúsculas y [ 0 - 9 1 pa- ra los dígitos. Esto también se puede emplear para alternativas individuales, de modo que a I b I c puede escribirse como [abcl . También se pueden incluir los intervalos múl- tiples, de manera que [a-zA-Z] representa todas las letras minúsculas y mayúsculas. Esta notación general se conoce como clases de caracteres. Advierta que esta notación puede depender del orden subyacente del conjunto de caracteres. Por ejemplo, al escri- bir [A-z] se supone que los caracteres R. C. y los demás vienen entre los caracteres A y Z (una suposición razonable) y que sólo 109 caracteres en mayúsculas ertán entre A y Z (algo que e i verdadero para el conjunto de caracteres ASCII). Sin embargo, al eicri- bir [A-z] no se definirán los mismos caracteres que para [A-Za-z] , incluso en el caso del conjunto de caracteres ASCII.

CUALQUIER CARÁCTER QUE NO ESTÉ EN UN CONJUNTO DADO Como hemos visto, a menudo es de utilidad poder excluir un carácter simple del con- junto de, caracteres por generar. Esto se puede conseguir al diseñar un metacarictcr para indicar la operación de negación ("not") o complementaria sobre un conjunto de aIternativas,Por ejemplo, un carácter estándar que representa la negación en lógica es la "tilde" -, y podríamos escribir una expresión regular para un carácter en el alfabeto que no sea a como -a y un carácter que no sea a, ni b, ni c, como

Uila alternativa para esta noracih se emplea en Lex, donde el carácter "cxat" A se utilim en co11,junto con las clases tlc caracteres que acabarnos tic dcscrihir pxa la li~rrnaci6ii (le

Page 47: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Expresiones regulares 43

complementos. Por ejemplo. cualquier carácter que no sea u se escribe corno ["a], mientras que cualquier carácter que no sea a, ni h ni c se escribe como

SUBEXPRESIONES OPCIONALES Por último, un suceso que se presenta comúnmente es el de cadenas que contienen pzir- tes opcionales que pueden o no aparecer en ctialquier cadena en particular. Por ejeniplo, un número puede o no tener un signo inicial, tal como + o -. Podemos emplear alterna- tivas para expresar esto como en las definiciones regulares

natural = [O-91+

naturalconSigno = natural I + natural l - natural

Esto se puede convertir rápi&amente en algo voluminoso, e iiitrodiiciremos e1 n i e ~ c a - rácter de signo de interrogación r? para indicar que las cademas que coincidan con r son opcionales (o que están presentes O o I copias de r). De este modo, el ejemplo del sig- no inicial se convierie en

natural = [O-91+

naturalconsigno = ( + 1-1 ? natural

22.3 Expresiones regulares para tokens de lenguajes de programación

Los tokens de lenguajes de programación tienden a caer dentro de varias categorías limita- das que son basta~ite estandarizadas a través de muchos lenguajes de programaciún diferen- tes. Una categoría es la de las palabras reservadas, en ocasiones también conocidas como palabras clave, que son cadenas fijas de caracteres alfabéticos que tienen un significado es- pecial en el lenguaje. Los ejemplos incluyen i f , wh i l e y do en lenguajes como Pascal, C y Ada. Otra categoría se compone de los símbolos especiales, que incluyen operadores arit- méticos, de asignación y de igualdad. Éstos pueden ser un carácter simple, tal como =, o múltiples caracteres o compuestos. tales como : = o ++. Una tercera categoría se compone de los identificadores, que por lo común se definen como secuencias de letras y dígitos que comienzan con una letra. Una categoría final se compone de literales o constantes, que in- cluyen constantes numéricas tales como 42 y 3.14159, literales de cadena como "hola, mun- do" y caracteres tales como "a" y "b. Aquí describiremos algunas de estas expresiones re- gulares típicas y analizaremos algunas otras cuestiones relacionadas con el reconocimiento de tokens. Más detalles acerca de las cuestiones de reconocimiento práctico aparecen pos- teriormente en el capítulo.

Números Los números pueden ser súlo secuencias de dígitos (números naturales), o núme- ros decimales, o números con un exponente (indicado mediante una "e" o "E) . Por ejem- plo, 2.71E-2 representa el número ,027 1. Po(lemos escribir definiciones regttlares para esos números como se ve a contin~raciún:

nat = [O-91+

natconSigno = í + I - ) ? nat número = natconSigno( " . " nat) ? ( E natconsigno) ?

Page 48: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 1 RASTREO O ANALISIS LÉXICO

Aquí escribimos el punto decimal entre comillas para enfatizar que debería ser generado di- rectamente y no interpretado como un metacarácter.

Identificadores y palabras reservadas Las palabras reservadas son las más simples de escribir como expresiones regulares: están representadas por sus secuencias fijas de caracteres. Si quisiéramos recolectar todas las palabras reservadas en una definición, escribiríamos algo como

reservada = if I while I do 1 . . . Los identificadores, por otra parte, son cadenas de caracteres que no son fijas. Un identifi- cador debe comenzar, por lo común, con una letra y contener sólo letras y dígitos. Podemos expresar esto en términos de definiciones regulares como sigue:

letra = [a-?.A-ZI

dígito = [O-91

identificad01 = letra(1etraldígito) *

Comentarios Los comentarios por Lo regular se ignoran durante el proceso del análisis Iéxi- co.' No obstante, un analizador léxico debe reconocer los comentarios y descartarlos. Por consiguiente, necesitaremos escribir expresiones regulares para comentarios, aun cuando un analizador Iéxico pueda no tener un token constante explícito (podríamos llamar a estos pseudotokens). Los comentarios pueden tener varias formas diferentes. Suelen ser de for- mato libre o estar rodeados de delimitadores tales como

{bate es un comentario de Pasc'al)

/ * éste es un comentario de C * /

o comenzar con un carácter o caracteres especificados y continuar hasta el final de la línea, como en

; éste es un comentario de Scheme -- éste es un comentario de Ada

No es difícil escribir una expresión regular para comentarios que tenga delimitadores de carácter simple, tal como el comentario de Pascai, o para aquellos que partan de algún(os) earáetedes) especificado(s) hasta el final de la línea. Por ejemplo, el caso di1 comentario de Pascal puede escribirse como

i(-))*l

donde escribimos - 1. para indicar "not }" y donde partimos del supuesto de que el carácter } no tiene significado como un metacarácter. (Una expresión diferente debe escribirse para Lex, la cual analizaremos más adelante en este capítulo.) De manera aimilar, un comentario en Ada se puede hacer coincidir mediante la expresión regular

-- 1 -nuevalínea) *

2. En ocasiones pueden contener directivas de compilador.

Page 49: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Exprer~oner regulares 45

en la cual suponeinos que nuevalínea corresponde al final de una línea (lo que se escri- he como \n en muchos sistemas), que el carácter "-" no tiene significado como un meta- carácter, y,que el final de la línea no está incluido en el comentario mismo. (Veremos c6mo escribir esto en [.ex en la sección 2.6.)

Es mucho más difícil cscribir una expresión reguiar para cl Caso de los deliniitadores que tienen más de un carácter de longitud, tal como los comentarios de C. Para ver esto considere el conjunto de cadenas bu. . . (ausencia de apariciones de uh). . .cih (utilizamos ha. . .ub en lugar de los delimitadores de C l*. . ."l. ya que el asterisco' y cn ocasiones la diagonal, es un inetacarácter que requiere de un manejo especial). No podemos escribir simplemente

porque el operador "not" por lo regular está restringido a caracteres simples en lugar de ca- denas de caracteres. Podemos intentar escribir una definición para - íab) utilizando -a, -b y - (a I b) , peco esto es no trivial. Una solución es

pero es difícil de leer (y de demostrar correctamente). De este modo, una expresión regu- lar para los comentaios de C es tan complicada que casi nunca se escribe en la práctica. De hecho, este caso se suele manejar mediante métodos ex profeso en los analizadores léxicos reales, lo que veremos rnás adelante en este capítulo.

Por último, otra complicación en el reconocimiento de los comentarios es que, en algu- nos lenguajes de programación, los comentarios pueden estar anidados. Por ejemplo, Mo- dula-2 permite comentarios de la forma

( * esto es ( * un comentario de * ) en Modula-2 * )

Los delimitadores de comentario deben estar exactamente pareados en dichos comentarios anidados, de manera que lo que sigue no es un comentario legal de Modula-2:

( * esto es ( * ilegal en Modula-2 * ) )

La anidación de los comentarios requiere que el analizador léxico cuente el número de los delimitadores. Pero advertimos en el ejemplo 2.3 (sección 2.2.1) que las expresiones regu- lares no pueden expresar operaciones de conteo. En la practica utilizamos un esquema de contador simple como una solución adecuada para este problema (véanse los ejercicios).

Ambigüedad, espacios en blanco y búsqueda hacia delante A menudo en la descripc~ón de los tokens de lenguajes de programación utilizando expresiones regulares,algunas cadenas se pueden definir mediante varias expresiones regulares diferentes. Por ejemplo, cadenas tales como if y whi le podrían ser identificadores o palabras clave. De manera semejante, la cadena <> se podría interpretar como la representación de dos tokens ("menor que" y "mayor que") o como un token simple ("no es igual a"). Una definición de lenguaje de progra- mación debe establecer cuál interpretación se observará, y las expresiones regulares por sí mismas no pueden hacer esto. En realidad, una definición de lenguaje debe proporcionar re- glas de no ambigüedad que iniplicarán cuál significado es el conveniente para cada uno de tales casos.

Dos reglas típicas que manejan los ejemplos que se acaban de dar son las siguientes. La primera establece que, cuando una cadena puede ser un identificador o una palabra clave, se prefiere por lo general la inte~pretación corno palabra clave. Esto se da a entender median- ti. el uso del térniino palabra reservada, lo que quiere decir que es simplemente una pala- hra clave que no puede ser también un identificador. La segunda establece que, cuando una

Page 50: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 1 RASTREO O A N ~ L I S I S LÉxlca

cadena puede ser un token simple o una secuencia de varios tokens, por lo común se prefie- re la interpretación del token sin@. Esta preferencia se conoce a menudo como el prin- cipio de la subcadena más larga: la cadena más Larga de caracteres que podrían constituir

3 un token simple en cualquier punto se supone que representa el siguiente token: Una cuestión que surge con el uso del principio de la subcadena más larga es la cues-

tión de los delimitadores de token, o caracteres que implican que una cadena más larga en el punto donde aparecen no puede representar un token. Los caracteres que son parte no ani- bigua de otros tokens son delimitadores. Por ejemplo, en la cadena xtemg=ytemg, el sig- no de igualdad delimita el identificador xtemg, porque = no puede aparecer como parte de un identificador. Los espacios en blanco, los retornos de línea y los caracteres de tabulación generalmente ianibién se asumen como delimitadores de token: while x . . . se inter- preta entonces como compuesto de dos tokens que representan la palabra reservada while y el identificador de nombre x, puesto que un espacio en blanco separa las dos cadenas de caracteres. En esta situación a menudo es útil definir un pseudotoken de espacio en blanco, similar al pseudotoken de comentaio, que sólo sirve al analizador léxico de manera interna para distinguir otros tokens. En realidad, los comentarios mismos por lo regular sirven co- rno delimitadores, de manera que, por ejemplo, el fragmento de código en lenguaje C

representa las dos palabras reservadas do e if más que el identificador doif. Una definición típica del pseudotoken de espacio en blanco en un lengucije de progra-

mación es

donde los identificadores a la derecha representan los caracteres o cadenas apropiados. Ad- vierta que, además de actuar como un delimitador de token, el espacio en blanco por lo re- gular no se tonla en cuenta. Los lenguajes que especifican este comportamiento se denomi- nan de formato libre. Las altemrttivas al formato libre incluyen el formato fijo de unos cuantos lenguajes como FORTRAN y diversos usos de la sangría en el texto, tal como la re- gla de fuera de lugar (véase la sección de notas y referencias). Un analizador Iéxico para un lenguaje de formato libre debe descartar el espacio en blanco después de verificar cual- quier efecto de delimitación del token.

Los delimitadores terminan las cadenas que forman el token pero no son pase del token mismo. De este modo, un analizador Iéxico se debe ocupar del problema de La búsqueda ha- cia delante: cuando encuentra un delimitador debe arreglar que éste no se elimine del resto de la entrada, ya sea devolviéndolo i la cadena de entrada ("respaldándolo") o mirando hacia de- lante antes de eliminar el carácter de la entrada. En la mayoría de los casos sólo se necesita ha- cer esto para un carácter simple ("búsqueda hacia delante de carácter simple"). Por ejemplo, en la cadena xtemg=ytemg, el final del identificador xtemg se encuentra cuando se halla el =, y el signo = debe permanecer en la entrada, ya que representa el siguiente token a reconocer. Advierta también que es posible que no se necesite la búsqueda hacia delante para reconocer un token. Por ejemplo, el signo de igualdad puede ser el único tokeu que comienza con el carácter =, en cuyo caso se puede reconocer de inmediato sin consultar el carácter siguiente.

En ocasiones un lenguaje puede requerir más que la búsqueda hacia delante de carácter sim- ple, y el analizador Iéxico debe estar preparado para respaldar posiblemente de nianera arbitra- ria muchos caracteres. En ese caso, el alinacenamiento en memoria intermedia de los carocte- res de entrada y la marca de lugares para un retroseguimiento se convierten en el diseño de un analizador Iéxico. (Algunas de estas preguntas se aborcian tnás adelante en este capítulo.)

3. En ocüsiones esto se denomina principio del "miximo bocado"

Page 51: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Áutómatar finitos 47

FORTRAN cs im buen ejerirplo de un lengutje que viola inuchos de los principios que :icabr~mos de anitliiar. Este es rlri lenguaje de kmnato Wo en el cual el espacio en blanco se elimina por nicdio de u11 prcprocesodor antes que comience la traducción. Por consiguien- te. la línea en E'ORTRAN

1 F ( X 2 . EQ. O ) THE N

de i-nniiern que cl espacio en hlanco ya ii0 I'unciona como un deliniit;tdm. Tuti1pocu hay p;tlabr a\ ,. itstc\aclas ..., . ,. cn FORTRAN, de modo que todas las palabras clave títmbi5n piteden ser idciitificadures, y la posición de la cadena de caracteres e11 cada línea de entrada es irnportarite parü determinar el tokcn que ser5 reconticiOo. Por ejemplo, la siguiente línea de cfidigo cs perfeck~~iiente correcta en FORTRAN:

Los primeros IF y THEN son palabras clave. mietitr¿is que los segundos I F y THEN son identificadores que representan variables. El efecto de esto es que un rinalizüdor léxico de FORTRAN debe poder retroceder a posicio~ies xhirrarias dentro de utia Iínea de código. Considerenios, en coticreto. el siguiente bien conocido ejenipl«:

Esto inicia un ciclo que C ~ I I I ~ I ~ C I I ~ ~ el código subsiguiente hasta la Iínea cuyo nún~ero es 99, con el mismo efecto que el Pascal for i : = 1 t o 10 . Por otra pa1-te. al canihiar I;i co- tila por un pu~ito

cambia el significado del código por completo: esto asigna el valor l . I o la variable con nombre D099I. De este modo, u n analimdor léxico no puede concluir que cl DO inicial es un;t p:tl:ihr;t clave hasta qtie alcance el signo de coma (o el punto), en cuyo caso puede ser obligado a retroceder hacia el principio de la Iínea y comenzar de nuevo.

2.3 AUTOMATAS FINITOS Los autóruatas finitos. o tnáquinas de estados finitos, son una rriniiera rnateinrítica para des- crihir clases particuliires de algoritmos (o "niáquinas"). En partic~ilar, los autóriiatas finitos se pueden utilizar para describir cl proceso de reconocimiento de patrones en cadenas de en- tr:ida, y de este modo se pueden utilizar para construir analizadores léxicos. Por supuesto, tatiihién existe una fuerte relación entre los autóniatas finitos y las expresiolies regulares, y veremos en I;i sección siguiente citrno constr~~ir un autfimata finito 3 partir de una expresión regt~lar. Sin embargo. ontcs (le conieiimr nuestro estudio de los autóniatas fiiiiios de niim- I-21 apropiada, corisitlerarenios u n -jcmplo explicativo.

El patrón para itlentificaclores conio se define co~níininentc en 10s lenguajes de progra- niación es15 dado por la sipicntc det'inicifin regular (si~porttlrernt~s que l e t r a y dígito ya sc definieron):

i d e n t i f i c a d o r = l e t r a ( l e t r a 1 d í g i t o ) *

Page 52: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 2.1 Un autbmata finito para identificadores

Esto representa una cadena que comienza con una letra y continúa con cualquier secuencia de letras y10 dígitos. El proceso de reconocer una cadena así se puede describir mediante el diagrama de la Figura 2.1.

le tra - letra

dígi to

En este diagrama los círculos numerados 1 y 2 representan estados, que son localidades en el proceso de reconocimiento que registran cuánto del patrón ya se ha visto. Las líneas con flechas representan transiciones que registran un cambio de un estado a otro en una coin- cidencia del carácter o caracteres mediante los cuales son etiquetados. En el diagrama de muestra, el estado 1 es el estado de inicio, o el estado en el que comienza el proceso de re- conocimiento. Por convención, el estado de inicio se indica dibujando una línea con flechas sin etiqueta que proviene de "de ninguna parte". El estado 2 representa el punto en el cial se ha igualado una sola letra (lo que se indica mediante la transición del estado l al estado 2 etiquetada con le tra) . Una vez en el estado 2, cualquier número de letras y10 dígitos se puede ver, y una coincidencia de éstos nos regresa al estado 2. Los estados que representan el fin del proceso de reconocimiento, en los cuales podemos declarar un éxito, se denomi- nan estados de aceptación, y se indican dibujando un borde con Iínea doble alrededor del estado en el diagrama. Puede haber más de uno de éstos. En el diagrama de muestra el es- tado 2 es un estado de aceptación, lo cual indica que, después que cede una letra, cualquier secuencia de letras y dígitos subsiguiente (incluyendo la ausencia de todas) representa un identificador legal.

El proceso de reconocimiento de una cadena de caracteres real como un identificador ahora se puede indicar al enumerar la secuencia de estados y transiciones en el diagrama que se utiliza en el proceso de reconocimiento. Por ejemplo, el proceso de reconocer xtemg como un identificador se puede indicar como sigue:

t. - + l X 2 2 2 2 2 . 2 A 2

En este diagrama etiquetamos cada transición mediante la ietra que se iguala en cada paso.

2.3.1 Definición de los autómatas finitos deterministicos

Los diagramas como el que analizamos son descripciones útiles de los autómatas finitos porque nos permiten visualizar fácilmente las acciones del algoritmo. Sin embargo, en oca- siones es necesario tener una descripción más formal de un autómata finito, y por ello proce- deremos ahora a proporcionar una definición matemática. La mayor paae del tiempo, no obstante, no necesitaremos una visidn tan abstracta como ésta, y describiremos la mayo- ría de los ejemplos en términos del diagrama solo. Otras descripciones de autómatas fi- nitos también son posibles, particularmente las tablas, y éstas serán útiles para convertir los algoritmos en código de trabajo. Las describiremos a medida que surja la necesidad de utilizarlas.

Page 53: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]
Page 54: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 2.2 Un automata fintto para idenoficadares con tranrlciones de errar

Por ejemplo, el nombre letra representa cualquier letra del alfabeto de acuerdo con la siguiente definición regular:

le t ra = [a-zA-Zl

Ésta es una extensión conveniente de la definición, ya que seria abruniador dibujar 52 trm- siciones por separado, una para cada letra minúscula y una para cada letra mayúsctila. Con- tinuaremos empleando esta extensión de la definición en el resto del capítulo.

Una tercera diferencia fundamental entre la definición y nuestro diagrama es que la definición representa las transiciones como una función T: S X C -+ S. Esto significa que T(s, C ) debe tener un valor para cada s y c. Pero en el diagrama tenemos Tiinicio, c) de- finida sólo si c es una letra, y T(entrada-id, c) está definida sólo si c es una letra o un dígi- to. ¿Dónde están las transiciones perdidas? La respuesta es que representan errores: es dc- cir, en el reconocimiento de un identificador no podemos aceptar cualquier otro carácter aparte de letras del estado de inicio y letras o números después de éste.%a convención es que estas transiciones de error no se dibujan en el diagrama sino que simplemente se su- pone que siempre existen. Si las dibujáramos, el diagrama para un identificador tendría el aspecto que se ilustra en la figura 2.2.

l e t r a

dígi to

error

c u a l q u i e r a

En esta figura etiquetamos el nuevo estado error (porque representa una ocurrencia errónea), y etiquetamos tas transiciones de error como otro. Por convención, otro re- presenta cualquier carácter que no aparezca en ninguna otra transición desde el estado donde se origina. Por consiguiente, la definición de otro proveniente desde el estado de inicio es

y la definición de otro proveniente dede el estado entrada-id es

o t ro = - ( l e t ra Id íg i to )

4. En realidad, estos caracteres no alianuméricos significan que no hay un identificador cn todo (si cstarnos en el estado de inicio), 0 que no hemos encontrado un delimirador que finalice el reco- nocimiento de un identificador (si estamos en iiri estado de aceptación). Más ndelante en esta scccidn veranos c6mo manejar estos casos.

Page 55: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]
Page 56: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Es sencillo escribir un DFA para nat como sigue (recuerde que a+ = a a * para cual- quier a):

Un naturalconsigno es un poco más difícil debido al signo opcional. Sin embargo, po- demos observar que un naturalconsigno comienza con un dígito o con un signo y un dígito, y entonces escribir el siguiente DFA:

También es fácil agregar La parte fraccional opcional como se aprecia a continuación:

d í g i t o d í g i t o

d í g i t o

Observe que conservamos anibos estados de aceptación para reflejar el hecho de que la parte fracciona1 es opcional.

Por último, necesitamos agregar la parte exponencial opcional. Para hacer esto observa- mos que la parte exponencial debe comenzar con la letra E y puede presentarse sólo después de que hayamos alcanzado cualquiera de los estados de aceptación anteriores. El diagrama final se ofrece en la figura 2.3.

Figura 2.J Un autómata finito para números de punto flotante

d í g i t o d í g i t o

Ejemplo 2.9 Los comentarios no anidados se pueden describir utilizando DFA. Por ejemplo, los comen- tarios encerrados entre llaves son aceptados por el siguiente DFA:

Page 57: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Autómatas finitos 53

Figura 2.1 Un autómata finito para comentarios tipo C

En este caso, o t ro significa todos los caracteres, excepto el de la llave derecha. Este DFA corresponde a la expresión regular (: ( - } ) * 1 , el cual escribimos previamente en la sección 2.2.4.

Observamos en esa sección que era difícil escribir una expresión regul:ir prni comenta- rios que estuvieran delimitados mediante una secuencia de dos caracteres, como ocurre con los comentarios en C, que son de la forma / * . . . (no * / S ) . . . *l . En realidad es inás f .' ~ L I I escribir un DFA que acepte tales comentarios que escribir una expresión regular para ellos. Un DFA para esos comentarios en C se proporciona en la figura 2.1.

otro

En esta tigura la transición o t ro desde el estado 3 hacia sí mismo se permite para todos los caracteres, excepto el asterisco *. mientras que la transición o t ro del estado 4 al estado 3 se permite para todos los caracteres, excepto el "asterisco" :* y la "diagonal" f . Numeramos los estados en este diagrama por simplicidad, pero podríamos haberles dado nombres más significativos, como los siguientes (con los números correspondientes entre parén- tesis): inicio (1 ); preparar-comentario (2); entrada-comentario (3); salida-comentario (4) y final (5) . 5

2.3.2 Búsqueda hacia delante, retroseguimiento y autómatas no deterministicos

Estudiamos los DFA como una manera de representar algoritmos que aceptan cadenas de caracteres de acuerdo con un patrón. Como tal vez el lector ya haya adivinado, existe una fuerte relación entre una expresión regular para un patrón y un DFA que acepta cadenas de acuerdo con el patrón. lnvestigaremos esta relación en la siguiente sección. Pero primero necesitamos estudiar más detalladamente ,los algoritmos precisos que representan los DFA, porque en algún momento tendremos que convertirlos en el código para un analiza- dor léxico.

Ya advertimos que el diagrama de un DFA no representa todo 10 que riecesita un DFA. sino que s6lo proporciona un esbozo de su funcionamiento. En realidad, vimos que la defi- nición matemática implica que un DFA debe tener una transición para cada estado y carác- ter, y que aquellas transiciones que dan errores como resultado por lo regular se dejan fuera del diagrama para el DFA. Pero incluso la definición matemática no describe todos los aspectos del comportamiento de un algoritmo de DFA. Por ejemplo, no especifica lo que ocurre cuando se presenta un error. Tanipoco especifica la acción que tomará un pro- grama al alcanzar un estado de aceptación, o incluso cuando iguale un carácter durante una transición.

Una acción típica que ocurre cuando se hace una transición es mover el carácter de la cadena de entrada a una cadena que acumula los caracteres hasta convertirse en un token simple (el valor de cadena del token o Iexema del token). Una acción típica cuando se al- canza un estado de aceptación es devolver el tokcn que se acaba de recotiocer, junto con cualquier atributo asociado. Una acción típic:t'cuand« se alcanza un estado de error es retro- ceder hacia la entrada (rctrnseguimiento) o generar un token de error.

Nuestro ejemplo original de u n token de identificador muestra gran parte del comporta- iiiizuto que clesel~mos describir aquí. y así regresaremos al diagrama de la figura 1.4. El DFA

Page 58: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 1 RASTREO O A N ~ L I S I S L ~ X I C O

de esa figura no muestra el comportalniento que queremos de un analizador léxico por varias razones. En primer lugar, el estado de error no es en realidad un error en absoluto, pero representa el hecho de que un identificador no va a ser reconocido (si veninlos desde el estado de inicio), o bien, que se ha detectado un tleliinitador y ahora deberímos aceptar y generar un token de identificador. Supongalnos por el momento (lo que de hecho es el comportamiento correcto) que existen otras transiciones representando las transiciones sin letra desde el estado de inicio. Entonces podemos indicar que se ha detectado un delimi- tador desde el estado entrada-id, y que debería generarse u n token identificador niediante el diagrama de la figura 2.5:

Un autómata finito para u n identifitador ton valor de ID de retorno

retorno y delimitador

En el diagrama encerramos la transición o t ro entre corchetes para i~idicar que el carác- ter delimitados se debería considerar como búsqueda hacia delante, es decir, que debería ser devuelto a la cadena de entrada y no consumido. Además, el estado de error se ha con- vertido en el estado de aceptación en este diagrama y no hay transiciones fuera del estado de aceptación. Esto es lo que querernos, puesto que el analizador léxico debería reconocer un token a la vez y corneniar de nuevo en su estado de inicio después de reconocer cada token.

Este nuevo diagrama también expresa cl principio de la subcadena m& larga descrito en la sección 2.2.4: el DFA continúa igualando letras y dígitos (en estado entrada-id) hasta que se encuentra un delimitador. En contraste. el diagrama antiguo permitiría aceptar al DFA en cualquier punto mientras se lee una cadena de identificador, algo que desde luego no que- remos que ocurra.

Ahora volvamos nuestra atención a la cuestión de cómo llegar al estado de inicio en primer lugar. En un lenguaje de programación típico existen muchos tokens, y cada token será reco- nocido por su propio DFA. Si cada uno de estos tokens comienza con un carieter diferente, entonces es ficil conjuntarlos uniendo simplemente todos sus estados de inicio en un solo estado de inicio. Por ejemplo, considere los tokens dados por las cadenas :=, <= e =. Cada uno de éstos es una cadena fija, y los DFA para ellos se pueden escribir conlo sigue:

- - ASSIGN ('asignación" ) de retorno

Corno cada uno de estos tokrns comienza con u n carácter diferente, potlerrios d o idcntifi- citr SUS rstittl~s (le iiticiti para ol>tcner el siguiente DFA:

Page 59: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Autómatar finitos

ASSIGN ( "asignaci6n") de retorno

LE ("menor O igual que") de retorno

EQ ("igualdad") de retorno

Sin embargo, supongamos que teníamos varios tokens que cotnenzaban con el mismo carácter. tales como <, i = y <>. Ahora no podemos limitarnos a escribir el diagrama si- guiente, puesto que no es un DFA (dado un estado y un carácter, siempre debe haber una trrinsicih única h ~ c i a un nuevo estado único):

LE ("menor o igual que") de retorno

NE ("no igual que") de retorno

LT ("menor que") de retorno

En vez de eso debemos arreglarlo de manera que sólo quede una transición por hacerse en cada estado, tal como en el diagrama siguiente:

LE ("menor o igual que") de retorno

- NE ("no igual quew) de retorno

0 ("menor queu) de retorna En principio, deberíamos poder coinbiriar totlos los tokens en un DFA gigante de esta manera. No obstante. la complejidad tic una tarea así se vuelve enorme, especialmente si se hace tlc una nianera no sistcmiiicii.

Page 60: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 1 RASTREO O ANALISIS LÉXICO

Una solución a este problema es ampliar la definición de un autómata finito para incluir el caso en el que pueda existir más de una transición pma un carácter pa~ticular, mientras que al mismo tiempo se desanolla un algoritmo para convertir sistemáticamente estos nue- vos autómatas finitos generalizados en DFA. Aquí describiremos estos autómatas generali- zados, mientras que pospondremos la descripción del algoritmo de traducción hasta la si- guiente sección.

La nueva clase de autómatas finitos se denomina NFA por las siglas del tkrmino autó- mata finito no determinístico en inglés. Antes de definirlo necesitamos otra generalización que será útil en la aplicación de los autómatas finitos a los analizadores Léxicos: el concep- to de la transición E.

Una transición E es una transición que puede ocumr sin consultar la cadena de entrada (y sin consumir ningún carácter). Se puede ver como una "igualación" de la cadena vacía, la cual antes describimos como E. En un diagrama una transición E se describe como si e fuera en realidad un carácter:

Esto no debe confundirse con una correspondencia del carácter E en la entrada: si el alfabe- to incluye este carácter, se debe distinguir del carácter e que se usa como metacarácter pa- ra representar una transición E.

Las transiciones E Son hasta cierto punto contraintuitivas, porque pueden ocurrir "espon- táneamente", es decir, sin búsqueda hacia delante y sin modificación de la cadena de entrada, pero son Útiles de dos maneras. Una es porque permiten expresar una selección de alterna- tivas de una manera que no involucre la combinación de estados. Por ejemplo, la selección de los tokens :=, <= e = se puede expresar al combinar los autómatas para cada token como se muestra a continuación:

Esto tiene la ventaja de que mantiene intacto al autómata original y sólo agrega un nuevo estado de inicio para conectarlos. La segunda ventaja de las transiciones E es que pueden describir explícitamente una coincidencia de la cadena vacía:

Page 61: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Autómatas finitos 57

Por supuesto, esto es equivalente al siguiente DFA, el cud expresa que Iii accptaci6n ocu- rriría sin hacer coincidir ningún carácter:

Pero es útil tener la notaci6n explícita anterior. Ahora definirenios qué es un autómata no determinístico, el cual tiene una definición

muy siniilar a la de un DFA, pero en este caso, según el ;inilisis anterior, necesitanios ain- pliar el alfabeto X para incluir a E. Haremos esto escribiendo 2: u { E ) (la r i n i h de 2 y E), donde antes utilizainos 2 (esto supone que E no es originalmente un miembro de 2). También necesitainos ampliar la definición de T (la función de transición) de modo que cada caric- ter pueda conducir a más de un estacto. Hacemos esto permitiendo que el valor de T sea un conjurrro de estados en lugar de un estado simple. Por ejemplo, dado el diagrama

teneinos T( 1, <) = (2, 3 ) . En otras palabras, desde el estado 1 podemos moverlos a cua- lesquiera de los estados 2 o 3 en el carácter de entrada <, y T se convierte en una función que mapea pares estado/símbolo hacia cortjunros de estarlos. Por consiguiente, el rango de Tes el conjunto de potencia del conjunto S de estados (el conjunto de todos los subcon- juntos de S); escribimos esto como p(S) (p manuscrita de S). Ahora estableceremos la de- finición.

Definición

Un NFA (por las siglas del término autómata finito no determinístico en inglés) M consta de un alfabeto 2, u n conjunto de estados S y una Funcicín de transición T: S X (2 u ( E ] ) -+ pis), así como de un estado de inicio so de S y un conjunto de estados de aceptación A de S. El lenguaje aceptado por M, escrito como L(IM), se define como el conjunto de cadenas de caracteres cIc> . .c, con cada c, de i. u ( e ] tal que existen estados s i en T(.s,,, c I ) . s2 en T(.sl, c,), . . . , .S,, en T(s , , I , c,,), curi S,, corno u n elemento de A.

De nuevo necesitamos advertir ;ilgunas cosas sobre esta definicih. Cualquiera de las c, en c,cl. . .c,, puede ser E , y Iü cadena que en realidad es aceptada es la cadena t,,co. . .c.,,,

cori la E eliniinada (porque la coircatcnacih <le .S con e es s rilis~n:~). Por lo tanto, la catlena

Page 62: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 1 RASTREO O A N ~ L I S I S LÉXICO

clc,. . .c, puede en realidad contener menos de n caracteres. Además, la secuencia de estados $ 1 , . . . , .S,, se elige de los conjuntos de estados T(s,,, c,), . . . , T ( S , ~ - , , c,,), y esta elección no siempre estará determinada de manera única. Ésta, de hecho. es la razítn por la que los autómatas se denominan no determinísjicos: la secuencia de transiciones que acepta una cadena particular no está determinada en cada paso por el estado y el siguiente carácter de entrada. En realidad, pueden introducirse números arbitrarios de E en cualquier punto de la cadena, los cuales corresponden a cualquier número de transiciones E en la NFA. De este modo, una NFA no representa un algoritmo. Sin embargo, se puede simular mediante un algoritmo que haga un retroseguimiento a través de cada elección no determinística, como veremos más adelante en esta sección.

No obstante, consideremos primero un par de ejemplos de NFA.

Ejemplo 2.1 O Considere el siguiente diagrama de un NFA.

La cadena abb puede ser aceptada por cualquiera de las siguientes secuencias de tran- siciones:

En realidad las transiciones del estado 1 al e~tado 2 en a, y del estado 2 al estado 1 en b, per- miten que la máquina acepte la cadena nb, y entonces, utilizando la transición E del estado 4 al estado 2, todas las cadenas igualan la expresión regular ab+. De manera similar, las transiciones del estado 1 al estado 3 en a, y del estado 3 al estado 4 en E, permiten la acep- tación de todas las cadenas que coinciden con ab*. Finalmente, siguiendo la transición E

desde el estado 1 hasta el estado 4 se permite la aceptación de todas las cadenas coinci- dentes con b*. De este modo, este NFA acepta el mismo lenguaje que la expresión regular ab+ 1 ab* 1 b*. Una expresión regular más \imple que genera el mismo lenguaje es (a 1 & ) b*. El siguiente DFA también acepta este lenguaje:

Page 63: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Autómatas h t o s

Considere el 5iguiente NFA:

Este acepta la cadena cictlh al efectuar las transiciones siguientes:

a - 1 E- 2 & 3 - 4 & 7 & 2 - - % - 5 & 6

De hecho, no es difícil ver que este NFA acepta el mismo lenguaje que el generado por la expresih regular (al c) b*. 9

2.3.3 lmplementación de autómatas finitos en código Existen diversas inaneras de traducir un DFA o un NFA en código, y las analizaremos en esta secciítn. Sin embargo, no todos estos métodos seriín útiles para un analizador Iixico de coinpilador, y en las últiinrts dos secciones de este capítulo se demostrarán los aspectos de la codificación apropiada para analizactores léxicos con niiís deidle.

Considere de nueva cuenta nuestro ejemplo original de un DFA que acepta identifica- dores co~npuestos de una letra seguida por una secuencia de letras y/« dígitos, en su fortna corregida que incluye búsqueda hacia delante y el principio de la subcadena rnás larga (véase la figura 2.5):

1.a primera y más sencilla rilaneln tlc simu1;ir este ¡>FA es cscrihic el cddigo de la niitncra higuiente:

Page 64: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

avcinza en la entrada; ( permanece en el estado 2 ) end while; { ir al estado 3 sin avanzar en la entrada ) uceptar;

else { error u otros casos 1

end if;

Tales códigos utilizan la posición en el código (anidado dentro de pruebas) para mantener el estado implícitamente, como indicamos mediante los comentarios. Esto es razonable si no hay demasiados estados (lo que requeriría muchos niveles de anidación), y si las iteraciones en el DFA son pequeños. Un código como éste se ha utilizado para escribir pequeños ana- lizadores léxicos. Pero existen dos desventajas con este método. La primera es que es de propósito específico, es decir, cada DFA se tiene que tratar de manera un poco diferente, y es difícil establecer un algoritmo que traduzca cada DFA a código de esta manera. La se- gunda es que la complejidad del código aumenta de manera dramática a medida que el nú- mero de estados se eleva o, más específicamente, a medida que se incrementa el número de estados diferentes a lo largo de trayectorias arbitrarias. Como un ejemplo simple de estos problemas, consideremos el DFA del ejemplo 2.9 como se dio en la figura 2.4 (página 53) que acepta comentarios en C, el cual se podría implementar por medio de código de la forma siguiente:

( estado 1 ) if el siguiente curácter es "/'then

uvunzun en la entraáu; ( estado 2 ) if el siguiente carácter es "*" then

adelantar la entrczda; ( estado 3 ) hecho : = false; while not hecho do

while el siguiente carúcter de entrado no es ""do uvonscr en la entrada;

end while; avanza en la entrada; [ estado 4 ) while el siguienre carácter de entratiu es "*" do

uvarzza en la entra&; end while; if el siguiente carácter de entrarla es "7" then

hecho : = true; end if; avanza en la entrada;

end while; aceptar; ( estado 5 ]

else ( otro procesamiento ] end if;

else { otro procesamiento ) end if;

Advierta el considerable incremento en complejidad, y la necesidad de lidiar con 13 i tm- ción que involucra los estados 3 y 4 mediante el uso de la variable booleana hecho.

IJn método de implementación sustancialmente mejor se obtiene al utilizar una variable para mantener el estado actual y escribir las transiciones como una sentencia case doblcrnen- ¡e anidada dentro de una iteración, donde la primera sentencia case prueba el estado actual y el segundo nivel anidado prueba el ceráctr de entrada, lo que da el estado. Por ejcmplo, el DFA anterior p a identiticadores se puede traducir en el esquema de c6rligo dc la figura 2.6.

Page 65: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Autómatas finitos

Figura 2 6 ~~~lementación del ldentfficador DFA utilizando

una variable de estado y pebs tase anidadas

estado :== 1; ( inicio } while estado = 1 o 2 do

case estado of 1: case carácter de entrfuía of

letra: avmza en la entrada; estado : = 2;

else estado := . . . ( error u owo 1; end case;

2: case carácter de entrada of letra, dbgito: avanza en la enir&&;

estado : = 2; ( realmente innecesario } efse estado : = 3; end case;

end case; end whiie; if estado = 3 then aceptar else error ;

Observe cómo este código refleja el DFA de inmera directa: las transiciones c«rrespon<len a la asignación de un nuevo estado a la variable estado y avanzan en la entrada (excepto en el caso de la transición "que no consume" del e s t í o 2 al estado 3).

Ahora el DFA para los con~cntarios en C (figura 2.4) se puede tr~tlucir en el esqrienia de cúdigo mris legible de la figura 2.7. Una alternativa para esta organizaciítn cs tener e l ca- se exterior basado en el carácter de entrada y los case internos basados en el estado actual (véanse los ejercicios).

En los ejemplos que acabamos de ver, el DFA se "conectó" directamente dentro del código. También es posible expresar el DFA como una estructura de datos y entonces escri- bir un código "genérico" que tomará sus acciones de la estructura de datos. Una estructura de datos simple qne es adecuada para este propósito es una tabla de transición, o arreglo bidimensional, indizado por estado y carácter de entrada que expresa los valores de la Función de transiciún T:

/ Caracteres en el alfabeto c

Estados 1 Ewdos representando transiciones

T(s, c)

Como ejemplo, el DFA para ~dentiftcadores se puede representar como la siguiente tabla de tran\ic~úri:

\\ z%tEda estado '\

I

2

3

letra

- 7 - ?

dígito

2

otro

3

Page 66: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

62 CAP. 2 1 RASTREO O ANALISIS LEXICO

F i q ~ r d 2.1 Implementación del DFA de la figura 2.4

En esta tabla las entradas en blanco representan transiciones que no se muestran en el diagrama DFA (es decir, representan transiciones a estados de error u otros procesamien- tos). También suponemos que el primer estado que se muestra es el estado de inicio. Sin en?- bargo, esta tabla no indica cuáles estados aceptados y cuiies transiciones no consumen sus entradas. Esta información puede conservarse en la misma estructura de datos que repre- senta la tabla o en una estructura de datos por separado. Si agregamos esta información a la tabla de transición anterior (utilizando una columna por separado para indicar estados de aceptaci6n y corchetes para señalar transiciones "no consumidoras de entrada"), obtenemos la tabla siguiente:

I

letra

2

digito otro Aceptación

110

Page 67: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Autómatas ffnitor 63

Como un segurttlo t+xiipl« de una tabla de transición. presentamos I:i tabla IiarLi el DFA corresporidicnie :i los comeiiiarios en C (miestro segundo ejcriiplo presentado con anterioridad):

Ahora podemos escribir del código en una Sorvtia que iinplementüri cualquier DFA, dadas las entradas y cstriictiiras de datos apropiadas. El siguiente esquema de c6digo supo- ne que las tr;insiciones se niantieneri en iin arreglo de traiisici6n 7' indimdo por esrüdos y carácter de entrada; que las transiciones que avanzan en la entriida (es decir, aquellas que no estin marcadas con corchetes en la tabla! están dadas por el arreglo hn(ilean« Aivin:clr. indizado también por estados y caracteres de entrada; y que los estados (le aceptación estin tlndos por el arreglo booleano Acepttrr, indizado por estados. El siguiente es el esquema de ccídigv:

carácter

I

2

3

4

5

esttrclo : = I ; 1.h : = .si,q~~if~r~/e ctrr~ícrer clc. rrrlrcrd(l: while not Acc.l,fcrr[estad~ and not rrror(c~,stirtio) do

iiurivestcitlo : = T[c~.sf~ldo,th]; if i\iv~rz:,cir[c~rt~r~l~~,cl~I then 01 : = .si,quierr/e ccr,iictrr de rrrtrcirkr: rstcrdo := I I I ( C I L ' ~ ~ ~ S / ~ I ~ ~ O ;

end while; if Ac~~[~ttrr[r.srtrt/o then iicc~pttrr:

Los métodos algorítmicos coino los que acabamos de describir se denominan controla- dos por tabla porq~ie emplean tablas para dirigir el progreso del algoritnio. Los niétoitos coritrolatlos por tabla tiene11 ciertas ventajas: el titrnaño del código se reduce, el mismo cci- digo fiiiicioiiará paln inuchos problemas diferentes y el ccidigo es mis ficil de niodificar (mantener). La desventaja es que las tablas pueden volverse muy grandes y provocar un irn- portante aumento en el espacio utilizado por el programa. En realiclod. gran parte del espa- cio en los arreglos que acabUtnos de describir se desperciicia. Por lo tanto, los ~nétodos con- trolados por tabla a menudo dependen de tnétodos de coinpresión de tablas. tales coirio representaciones de arreglos dispersos, aunque por lo regular existe una penalización en tiempo que se debe p q n r por dicha compresión, ya que la húsqucda eii tiihlas se vuelve mis lenta. Como los analizadores léxicos deben ser eficientes. rara vez se utilizan estos rnétoclcts para ellos, aunque se ptiedeii emplear en programas generadores de ;tnalizadores léxicos t:i- les como Lex, N0 los cstiitliaremos mlís aquí.

Por último, advertirno.; qi~e I O Y NF/\ se pueden iniplcnicntíir de tr1aner;is siniilares a los I X A . excepto que co~rio los Nt'A son no determinísticos, tienen muchas sccueiicias dit'ercn- [es de tmisiciones etl potetici:~ que ic deben probar. Por coiisiguientc, un progran1:i que h ~ i l l s u n NI'A tlehe nlni;icunnr tr:insicionc\ tpe t~(liiví:i tto se hm probado y ~.cdilar cl re- ti-iwguimiento :i ellas i.11 c:iw ilc l i l l n . Esto e\ I I W ~ si~nilar a los algctritnios que irrteiitiiri e11- LY1nti-:ir t~-~lycc~ori~ls en griI'iG15 dil-igid:i>, s d o y e 121 cL1~lcna de c~1t~-:id:l g11í:1 lil l>lí.;i~lic,lLl.

1

7

3

5 -

:#

3

4

1

otro

3

3

Aceptación

110

110 -- 110

$10 -

s í

Page 68: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Como los algoritmos que realizan una gran cantidad de retroseguimientos tienden a ser po- co eficientes, y un analizador léxico debe ser tan eficiente como sea posible, ya no descri- biremos tales algoritmos. En su lugar, se puede resolver el problema de simular un NFA por medio del método que estudiaremos en la siguiente sección, el cual convierte un NFA en un DFA. Así que pasaremos a esta sección, donde regresaremos brevemente al problema de simular un NFA.

2.4 DESDE LAS EXPRESIONES REGULARES HASTA LOS DFA En esta sección estudiaremos un algoritmo paca traducir una expresión regular en un DFA. También existe un algoritmo para traducir un DFA en una expresión regular, de manera que las dos nociones son equivalentes. Sin embargo, debido a lo compacto de las expresiones re- gulares, se suele preferir a los DFA como descripciones de token, y de este modo la gene- ración del analizador Iéxico comienza comúnmente con expresiones regulares y se sigue a través de la construcción de un DFA hasta un programa de analizador léxico final. Por esta razón, nuestro interés se enfocará sólo en un algoritmo que realice esta dirección de la equivalencia.

El algoritmo más simple para traducir una expresión regular en un DFA pasa por una construcción intermedia, en la cual se deriva un NFA de la expresión regular, y posterior- mente se emplea para construir un DFA equivalente. Existen algoritmos que pueden traducir tina expresión regular de manera directa en un DFA, pero son más complejos y la construc- ción intermedia también es de cierto interés. Así que nos concentraremos en la descripción de dos algoritmos, uno que traduce una expresión regular en un NFA y el segundo que tra- duce un NFA en un DFA. Combinado con uno de los algoritmos para traducir un DFA en un programa descrito en la sección anterior, el proceso de la construcción de un analizador léxico se puede automatizar en tres pasos, como se ilustra mediante la figura que se ve a con- tinuación:

2.41 Desde una expresión regular hasta un NFA La construcción que describiremos re conoce como la construcción de Thompson, en ho- nor a su inveittor. Utili~a transiciones E para "pegar" las máquinas de cada segmento de una expresión regular con el fin de formar una máquina que corresponde a la expresión comple- ta. De este modo, la construcción es inductiva, y sigue la estructura de la definición de una expresión regular: mostramos un NFA para cada expresión regular básica y posteriormente moitramos cómo se puede conseguir cada operación de expresión regular al conectar entre sí los NFA de las subexpresiones (suponiendo que éstas ya se han construido).

Expresiones regulares básicas Una expresión regular básica es de la forma a, E o +, donde a representa una correspondencia con un carácter simple del alfabeto, E representa una coinci- dencia con la cadena vacía y I$ representa la correspondencia con ninguna cadena. Un NFA que es equivalente a la expresión regular a (es decir, que acepta precisamente aquellas cadenas en su lenguaje) es

Page 69: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Derde lar exprerlones regulares hasta los DFA

De manera wnil,ii, un NFA que e5 equivalente a E e\

E l caso de la expresiún regular + (que nunca ocurre en la prcictica en 1111 coinpilador) se de- ,ja como ejercicio.

Concatenación Deseamos construir un N F A equivalente a la expresicín regular m, doride r y s soii expresiones regulares. Suponernos (de manera inductiva) que los NFA equivalentes :I r y s ya se construyeron. Expresamos esto al escribir

para el N F A correspondiente a r. y de nianera similar para .s. En este dibujo, el círculo a lii iLquierda dentro del rectángulo redoiidendo indica el estado tic inicio, cl círculo doble ;I la de- recha indica el estado de aceptaci6n y los tres pnnlos indican los estados y trnnsicioiies (Icntro del N F A que no se mnestrao. Esta figura supone que el NFA correspondiente a r. tiene súlo Lin estado de aceptacicín. Esta stiposici6n se justificará s i todo NFA que constrrtyanios tiene rcn estado de aceptación. Esto es verd;t(lero para los N F A de expresiones regulares bcisi- cas, y será cierto para cada una de las construcciones siguientes.

Ahora podemos construir un NFA correspondiente a rs de la manera siguiente:

Conectamos el estado de aceptacicín de la rnáquiiia de r a1 estado de inicio de 13 máquina de s mediante una tra~isiciún 6:. La nueva máquina tiene el estatío de inicio de la lniquint l tle r como hti estado de inicio y el estado de ;iceptaci<in de la m k p i n a de s c rmo su estado tle aceptaciún. Evidentemente, esta máquina acepta Lics) .= L(r)L(.s) y de cste rnotlo correspon- de ;i la expresiún regular cs.

Seleccih entre akernafivas I )exmios coristi-uir LIII N F h ct>rrespondici,te a I - I .S b-jo la\ mi\m;is w[xisicio~ies que :iillcs. I!;icc~nos esto como he bc ;I contiriii:ici6ir:

Page 70: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ck 2 1 RASTREO O ANALISIS LÉXICO

Agregamos un nuevo estado de inicio y un nuevo estado de aceptación y los conectamos como se muestra utilizando transiciones E. Evidentemente, esta máquina acepta el lenguaje L(r I S ) = L(r) u L(s).

Repetición Queremos construir una máquina que corre~ponda a r*, dada una máquina que corresponda a r. Haremos eito como se muestra enseguida:

Aquí nuevamente agregamos dos nuevos estados, un estado de inicio y un estado de acep- tación. La repetición en esta máquina la proporciona La nueva transición E del estado de aceptación de la máquina de r a su estado de inicio. Esto permite a la máquina de r ser atravesada una o más veces. Para asegurar que la cadena vacía también es aceptada (corres- pondiente a las cero repeticiones de r), también debemos trazar una transición E desde el nuevo estado de inicio hasta el nuevo estado de aceptación.

Esto completa la descripción de la construcción de Thompson. Advertimos que esta construcción no es única. En particular, otras constnicciones son posibles cuando se traducen operaciones de expresión regular en NFA. Por ejemplo. al expresar la concatenación rs, po- dríamos haber eliminado la transición E entre las máquinas de r y s e identificar en su lugar el estado de aceptación de la ináq~iifia de r con el estado de inicio de la máquina de S , como se ilustra a continuación:

Page 71: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Desde lar expresiones regulares harta los DFA 67

Ejemplo 2.12

(Sin embargo, esta simplificación depende del hecho que en las otras construcciones el estado de aceptación no tiene transiciones desde ella misma hacia otros estados: véanse los ejercicios.) Otras simplificaciones son posibles en los otros casos. La razón por la que expresamos las traducciones a medida que las tenemos es que las máquinas se construyen según reglas muy simples. En primer lugar, cada estado tiene con10 máximo dos transicio- nes, y si hay dos. ambas deben ser transiciones F . En segundo lugar, ningún estado se eli- mina una vez que se construye, y ninguna hansicicín se modifica. excepto para la adición de transiciones desde el estado de aceptación. Estas propiedades hacen muy Picil automatizar el proceso.

Concluimos el málisis de IU construcción de Thornpson con algunos ejemplos.

Traduciremos la expresión regular ab l a en un NFA de acuerdo con la construcción de Thompson. Primero formamos las máquinas para las expresiones regulares básicas a y b:

Entonces formamos la máquina para la concatenación ab:

Ahora formamos otra copia de la máquina para a y utili7arnos la constmcción para selec- ción con el fin de obtener el NFA completo para ab I a que se muestra en la figura 2.8.

Figura 2.8 NFA para la expresión regular ab l a utilizando la construcción de Thompron

Ejemplo 2.13 Formamos el NFA de la construcción de Thompson para la expresión regular l e t r a - ( l e t r a I dígi to) *. Conlo en el ejemplo anterior, formamos las máquinas para las expre- siones regulares l e t r a y dígi to:

Page 72: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 1 RASTREO O A N ~ L I S I S L~XICO

Después formamos la máquina para la selección 1 et ra I d ígi to:

Ahora formamos la NFA para la repetición ( l e t ra1 d íg i to ) * como se ve a conti- nuación:

Por último construimos la máquina para la concatenación de l e t r a con ( l e t r a I d ígi- to) * para obtener el NFA completo, como se ilustra en la figura 2.9.

Figura 2.9 N I A para la expres~ón regular letra- (letraldígito)*

utilizando la conrtrucc16n de Thompron

E

letra

Page 73: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Desde lar expresiones regulares hasta los DfA 69

.Como ejemplo final, observanios que el NFA del ejemplo 2.1 I (scccicin 2.3.2) corres- ponde exactaniente a la expresión regular (a 1 c ) *b hajo la construcciih de Thornpson.

2.4.2 Desde u n NFA hasta u n DFA Ahora deseamos dcscrihir un algoritnio que, dado un NFA arbitrario, coristniirá un DFA equivalente (es decir, uno que acepte precisamente las mismas cadenas). Pitra hacerlo necesi- taremos algún método con el que se eliminen tanto las transiciones i conlo las transiciones inúltiples de un estado en un carácter de entrada simple. La eliminación de las transiciones E

implica el construir cerraduras E, las cuales son el conjunto (le todos los estados qite pueden alc~inzar las transiciones E desde un estado o estados. La eliminación de transiciones múlti- ples en un carácter de entra& simple implica mantenerse al tanto del conjunto de estados que son alcatizables al igualar un carácter simple. Ambos procesos nos conducen a conside- rar conjuntos de estados en lugar de estados simples. De este modo, no es ninguna sorpresa que el DFA que construimos tenga como sus estados los conjitraros de estados del NFA origi- nal. En consecuencia, este algoritmo se denomina construcción de subconjuntos. Primero analizaremos la cerradura e con un poco más de detalle y posteriormente continuaremos con una descripción de la constmcción de subconjuntos.

La cerradura E de un conjunto de estados Definimos la cerradura i de un estado simple .r como el conjunto de estados alcanzables por una serie de cero o más tmnsicinnes e, y escri- birnos este conjunto como F. Dejürernos una afirmación más matenxítica de esta definición para un ejercicio y continuaremos directamente con un ejemplo. Siu embargo, advierta que la cerradura E de un estado siempre contiene al estado mismo.

Ejemplo 2.14 Considere el NFA siguiente que corresponde a la expresih regular a* bajo la construcción de Thornpson:

Ahora definiremos la cerradura e de un conjunto de estados corno la urti6n de las cerra- duras i de cada cstado individual. En símbolos, s i S cs un conjunto de estados. entonces te- nernos que

Page 74: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 i RASTREO O AN~LISIS ~ É X I C O

Por ejemplo, en el NFA del ejemplo 2.14, Tí;S) = 7 u 3 = ( 1 , 2, 4 ) u {2, 3, 4 ) = ( 1 , 2 ,3 , 4 ) .

La construccian del subconjunto Ahora estamos en posiciún de describir el - algoritmo para la construcción de un DFA a partir de un NFA M dado, al que denominarenlos M. Primero calcu- .-

l m s la cerradura S del estado de inicio de M. esto se convierte en el estado de inicio de M. Para este conjunto, y para cada conjunto subsiguiente, calculamos las transiciones en los caracteres ca como sigue. Dado un conjunto S de estados y un carácter u en el alfabeto, calcula- rnos el conjunto - S: = [ t / para alguna s en S existe una transición de s a r en u ). Después calcutamos S:, la cerradura S de S:. Esto define un - nuevo estado en 1.a construcción del subconjunto, junto con una nueva transición S A S:. Continuamos con este proceso has- ta que no se crean nuevos estados o transiciones. Marcamos conio de aceptación aquellos estados construidos de esta manera que contengan un estado de aceptación de M. Éste es el DFA No contiene transiciones E debido a que todo estado es construido como una cerra- dura E. Contiene como máximo una transición desde un estado en un carácter a porque cada nuevo estado se construye de todos los estados de M alcanzables por transiciones desde un estado en un carjcter simple a.

Ilustraremos la construcción del subconjunto con varios ejemplos.

Ejemplo 2.15 Considere el NFA del ejemplo 2.14. El estado de inicio del DFA correspondiente es i = ( 1 , 2, 4 ) . Existe una transición desde el estado 2 hasta el estado 3 en a, y no hay transiciones desde los estados 1 o 4 en a, de manera que hay una transición en a desde ( 1, 2, 4 ) hasta - (1, 2, 4 ), = ( 3 ) = (2, 3, 4 ) . Como no hay hansiciones adicionales en un carácter desde cualquiera de los esiados 1, 2 o 4, volveremos nuestra atención hacia el nuevo estado (2, 3, 4 ) . De nueva cuenta existe una transición desde 2 a 3 en a y ninguna transición a desde 3 o 4, de modo que hay una transición desde (2, 3, 4) hasta {2 ,3 ,4 ), = (3) = (2, 3, 4 ) . De manera que existe una transición a desde (2, 3, 4) hacia sí misma. Agotamos los esta- dos a considerar, y así construimos el DFA completo. Sólo resta advertir que el estado 4 del NFA es de aceptación, y puesto que tanto ( 1 , 2, 4 ) como 12, 3, 4 ) contienen al 4, ambos son estados de aceptación del DFA correspondiente. Dibujamos ei DFA que construimos como sigue, donde nombramos a los estados mediante sus subconjuntos:

(Una vez que se termina la construcción, podríamos descartar la terminología de subconjun- tos si así lo deseamos.) 9 3

Ejemplo 2.16 Considereinos cl NFA de la figura 2.8. al cual agregaremos números de estado:

Page 75: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Desde las expresiones regulares hasta los DFA

Ejemplo 2.17

La constmcci6n del subconjunto de DFA tiene como su estado de inicio (I) = ( 1, 2. 6 ) . Exis- te una transición en (1 desde el estado 2 hasta el estado 3, y también desde el estado 6 hasta el estado 7. De este modo, ( 1 , 2 , 6 1, = ( 3 , 7 ) = (3 , 4, 7 , U) , y tenemos ( 1 , 2, 6)&(3, 4, 7 ,U) . Como no hay otras transiciones de carácter desde l , 2 o 6, vayamos a (3 ,4 ,7 ,8 ). Exis- - te una transición en h desde 4 hasta 5 y ( 3 , 4 , 7 , U),, = ( 5 ) = ( 5 , 81, y tenemos la transición ( 3 , 4 , 7, 8)&(5, 8 ) . No hay otras transiciones. Así que la construcción del subconjunto produce el siguiente DFA equivalente al anterior NFA:

Considere el NFA de la figura 2.9 (la conqtrucción de Thompson para la expresión regular l e t r a í l e t r a l d í g i t o ) * ) :

La construcción del subconjunto continúa como se explica a continuación. El estado de inicio es (I) - ( I ). Existe una transición en letra para (2) = (2, 3,4,5,7, LO). Desde este esta-

.- do existe una transición en l e t ra para ( 6 ) = (4, 5, 6, 7, O, 10) y una transición en dígi to - para ( 8 ) = ( 4 , 5, 7 , 8, 9, 10). Finalmente, cada uno de estos estados también tiene transi- ciones en le t ra y dígito, ya sea hacia sí mismos o hacia el otro. El DFA completo se ilustra en la siguiente figura:

Page 76: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 1 RASTREO O ANALISIS L~XICO

l e t r a

2.43 Simulación de un NFA utilizando la construcción de subconjuntos

En la sección anterior analizamos brevemente la posibilidad de escribir un programa para simular un NFA, una cuestión que requiere tratar con la no determinación, o naturaleza no aigorítmica, de la máquina. Una manera de simular un NFA es utilizar la constnicción de subconjuntos, pero en lugar de construir todos los estados del DFA asociado, construimos solamente el estado en cada punto que indica el siguiente carácter de entrada. De este modo, construimos sólo aquellos conjuntos de estados que se presentan en realidad en una trayectoria a través del DFA que se toma en la cadena de entrada proporcionada. La ventaja de esto es que podemos no necesitar construir el DFA completo. La desventaja es que, si la trayectoria contiene bucles, un estado se puede construir muchas veces.

Pongamos por caso el ejemplo 2.16, donde si tenemos la cadena de entrada compuesta del carácter simple u, construiremos el estado de inicio { 1, 2, 6 ) y luego el segundo estado {3 ,4 ,7 , 8) hacia el cual nos movemos e igualamos la a. Entonces, como no hay b a conti- nuación, aceptamos sin generar incluso el estado ( S , 8).

Por otra parte, en el ejemplo 2.17, dada la cadena de entrada r2d2, tenemos la siguiente secuencia de estados y transiciones:

Si estos estados se construyen a medida que se presentan las transiciones, entonces todos los estados del DFA están construidos y el estado (4,5,7, 8,9, 10) incluso se construyó dos ve- ces. Así que este proceso es menos eficiente que construir el DFA completo en primer lu- gar. Es por esto que no se realiza la simulación de los NFA en analizadores léxicos. Qne- da una opción para igualar patrones en programas de búsqueda y editores, donde el uwario puede dar las expresiones regulares de manera dinámica.

2.44 Minimización del número de estados en un DFA

El proceso que describimos para derivar un DFA de manera algorítmica a partir de una expresión regular tiene la desafortunada propiedad de que el DFA resultante puede ser ni5s complejo de lo necesario. Como un caso ilustrativo, en el ejemplo 2.15 derivamos el DFA

Page 77: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Desde lar expresiones regulares hasta los DFA

prr" la expreíibn regular a*. rntelitra que el DFA

tarribi6n lo h a r i Corrto la eficiencia es inuy importante en un analizador léxico, iios gustaría poder construir, si es posible, un DFA que sea ~iiíniitio en algún sentido. De hecho, un resul- tado itiiportante de la teoría de los autóiiiat;is establece que, dado cualquier DFA, existe un DFA equiv;ilente que coiitiene un núnrero niítriino de estados, y que este DFA de estados ~iiíriinios o iiiíiiirno es único (excepto en el caso de renoiubrx estados). 'Trrinbién es posible obtener directaniente este DFA míniirio de crtalquier DFA <la<lo. y aquí describiremos hreve- meiite el algoritmo. sin deniostrar que en realidad construye el DFA mínimo eqtiiv;rlente (debería ser f ic i l para el lector convencerse de esto de manera infornial al leer el dgoritiiio).

El algoritmo funcioiia al crear conjuiitos de estados que se unificarin en estados siirlples. Coinienza coi1 la suposicibn inás optirriista posible: crea dos con,juntos, uno cuinpuesto <le !o<l«s los estridos de :rceptación y el otro coinpucsto de tctdos los estados de iio nccptacih. Dada esta particióii de los estaclos del DFA original, considere las transiciones en cada carácter r i del alhbeto. Si todos I«s estados de aceptacitíti tienen traiisiciones cn a para es- tados de aceptación, errtotices esto define una transicibri ri desdiel nuevo estado de acepta- ción (el con,~uiiti de todos los estados de aceptacitín antiguos) hacia sí m ima. De irianct-a sirnilar, s i todos los estados de aceptación tienen transiciones en ci hacia estados de no acep- tacitín. entonces esto M i n e una transición ri desde el riuevo estad6 de aceptación hacia el nuevo estado de no :rceptacií,n (el conjunto de todos 10s estados de no acepktcibn mtiguos). Por otra parte, s i hay dos estados de xeptaciciii ,S y t que tengan transiciones en e1 que caigan en diferentes conjuntos, entonces ninguna transición ri se puede definir para este :igrupa- iiiento de los estados. Decimos que 11 distingue los estados s y t. En este caso, el conjunto de estados que se está consi<lerando (es decir, el corUunto de todos los estados [le ;icepta- ciónf se debe dividir de acuerdo con el lugar en que caen sus transiciones a. Por supuesto, declaraciones similares se inantie~ieti para cada uno de los otros conjuntos de estados, y una vez que hemos considerado todos los caracteres del alfabeto, debemos movernos a ellos. Por supuesto, s i cualquier conjunto adicional se divide, debernos regresar y repetir el proceso desde el principio. Continuamos este proceso de refinar la partición de los estados del DFA original en con.j~into hasta que todos los conjuntos contengan sblo un elemento (en cuyo ca- so: mostrarnos que el DFA original es mínimo) o hasta que no se presente ninguna divisiOn ritlicional de cotijuntos.

P:ira que el proceso que acobamos de ciescribir funcione correctamente, también tlebe- mos considerar trmsiciones de error a u11 estado de error que es de iio aceptación. Esto es, si existeii estados de :iceptación S y t , t;iles que S tenga una trmsicibn ci hacia otro estxio de :iccptacih, mientras que t no iciign tiingiiria transición (1 (es decir. una trmiición de error), mtoriccs (1 ilistirigue .S y l . De l a iiiisiiia iiiruiera, s i u11 cst;itlo de iio aceptaciih S iiciie una trtinsici6it e1 hacia un esiaclo iIe ;tcept;rci;>ii. ~nicntr;rs que otro estado de no :iccpt;rciOti t 110

iicire Ir:uisición r i , eritonccs o tlisti~rguc .\ y t 1.1 . in b': ILII CII este c ü o . ('o~icluire~nos liucstro mi l is i \ dc l a tirirrimi~xi¿iti (le ebt:rdo con un par (te c.jc~iiplos.

Page 78: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejemplo 2.18 Consideremos el DFA que construimos en el ejemplo anterior, correspondiente a la expre- sión regular letra(1etra 1 dígito) *. Tenía cuatro estados compuestos del estado itii- cial y tres estados de aceptación. Los tres estados de aceptación tienen transiciones a otros estados de aceptacicín tanto en letra como e11 dígi to y ninguna otra transición (sin erro- res). Por consiguiente, los tres estados & aceptación no se pueden distinguir por ningún ca- rácter, y el algoritmo de minimización da como resultado la combinación de los tres estados de aceptacicín en tino. dejando el siguiente DFA iníninto (el cual ya virrios al principio de IU sección 2.3):

l e t r a

l e t r a

dígi t o

Ejemplo 2.19 Considere el siguiente DFA, que dimos en el ejemplo 2.1 (sección 2.3.2) como equivalente a la expresicín regular (al e ) b*:

En este caso todos los estados (excepto e1 estado de error) son de aceptación. Consideremos ahora el carácter h. Cada estado de aceptación tiene una transición b a otro estado de acep- tación, de modo que ninguno de los estados se distingue por b. Por otro lado, el estado 1 tie- ne una transición o hacia un estado de aceptación, mientras que los estados 2 y 3 no tienen transición u (o, mejor dicho, una transición de error en a hacia el estado de no aceptación de error). Así, rt distingue e1 estado 1 de los estados 2 y 3, y debemos repartir los estados en los conjuntos ( 1 j y (2, 3 ) . Ahora comenzainos de nuevo. El coujunto ( I ) no se puede di- vidir más, así que ya no lo consideraremos. Ahora los estados 2 y 3 no se pueden distinguir por i r o b. De esta manera, obtenemos el DFA de estado mínimo:

Page 79: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Implementation de un anakzador l4xita TlNY ("D~minuto") 75

IMPLEMENTAUÓN DE U N ANALIZADOR LEXICO TlNY ("DIMINUTO") Ahora deseamos desarrollar el código real para un analizador léxico con el fin de ilustrar los conceptos estudiados hasta ahora en este capítulo. Haretrtos esto para el lenguaje T lNY que se preseritó de manera inh~rrnal en el capítulo I (sección 1.7). Después analizarrrnos diver- sas cuestiones de implernentación prhctica planteachs por este analizador Iixico.

2.51 lmplementacion de un analizador léxico para el lenguaje de muestra TINY

En el capítulo 1 expusimos brevemente y de manera informal el,lcnguaje TINY. Nues- tra tarea aquí es especificar por completo la estructura. léxica de TINY, es decir, definir los tokens y sus atributos. Los tokens y Las clases de tokens de.TINY se resuirien en Ia tabla 2.1.

Los tokens de TINY caen dentro de tres categorías típicas: palabras reservadas, símbolos especiales y "otros" tokens. Existen ocho palabras reservadas, con significados familiares (aunque no necesitamos conocer sus semhnticas hasta mucho después). Existen 10 símbo- los especiales. que dan las cuatro operaciones aritméticas básicas con enteros, dos operacio- nes de comparación (igual y inenor que), paréntesis, signo de punto y coma y asignación. Todos los símbolos especiales tienen un cruáeter de longitud, excepto el de asignación, qtie tiene dos.

Tahla 2 1 Tokens del lenguaje TlNY Palabras reservadas Simbolos especiales Otros

if then else end regeat until read write

+ número ( 1 o más

e dígttos)

1

Los otros tokens son números, que son secuencias de uno o mis dígitos e identificadores, los cuales (por simplicidad) son secuencias de una o niás letras.

Además de los tokens, T lNY tiene las siguientes convenciones léxicas. Los coinentarios están encenados entre llaves ( . . . J y no pueden estar anidados; el código es de formato li- bre: el espacio en blanco se compone de blancos, tabulaciones y retornos de línea; y se si- gue el principio de la suhcadena mis larga ert el reconociiniento de tokens.

A l diseñar un analizador léxico para esk lenguaje, podríarncts comenzar con expre- siones regulares y desarrollar NFX y DFA de acuerdo con los iilgoritriios de la seccitin ;interior. En rcalidad 1:s expresiones regulares se han dado anteriorniente para núineros, itlentific;tdores y corncnt:iricts ('ClNY tiene versiones p;irticular~ilente sirriples de &tos). [..as cxpresiones rcgul;rres pxa los 011-os 1i)kctis 110 son i~iiport;intes p<rrqw tcdos soti catleiiii\

Page 80: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

fijas. En lugar de seguir esta ruta &sano«llaremos un DFA püra el analizador léxico directa- mente, ya que los tokens son muy simples. Haremos esto en varios pasos.

En primer lugar, advertimos que todos los símbolos especiales, excepto los de asigna- ción, son caracteres únicos distintm, y un DFA para estos símbolos tendría el aspecto que sigue:

MÁS de retorno

MENOS de retorno

En este diagrama, los diferentes esrados de aceptación distinguen el token que el analizador livico está por devolver. Si empleamos algún otro indicador para el token que ser6 devuel- to (digamos, una variable en el código), entonces todos los estados de aceptación se pueden colapsar en un estado que denominaremos HECHO. Si combinamos este DFA de cios esta- dos con DFA que acepten números identificadores, obtenemos el DFA siguiente:

Advierta el uso de los corchetes para indicar caracteres de búsqueda hacia delante que no deberjn ser consumidos.

Necesitarnos agregar comentarios, espacio en blanco y asignación a este DFA. El espa- cio en blanco es consun~ido mediante un bucle simple desde el estado de inicio hacia sí rnismo. Los comentarios requieren un estado extra, que se alcanza desde el estado de inicio en la llave izquierda y regresa a él en la llave derecha. La asignacitin también requiere de un estalo intermedio, que se alcanza desde el estado de inicio en el signo de punto y coma. Si sigue inmediatüinente un signo de igualdad, entonces se genera u n tokcn de asignación. De otro niodo. el siguiente carhcter no debería ser consumido, y ?e genera un token de crror.

Page 81: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Irnplementac~bn de un analizador Iéxico TlNY ("D~rn~nuta")

f i gu i d 110 ~ f b del analizador lexico

de TlNY espacio en blanco

De hecho, totlos los caracteres simples que no están en la lista de símbolos especiales. que no son espacios eii hlanco o comentarios, y que no son tlígitos o letras. se deberán aceptar conio errores, y iolerarernos éstos con los símbolos de carácter simple. E l DFA final para nuestro analirador Iéxtco 5c puede aprcctar en la figura 2.10.

No incluirnos palabras reservadas en nuestro aniílisis o e n el DFA de la figura 2.10. Es- io es porque es más fácil desde el punto de vista del DFA considerar que las palabras rescr- vadas son lo mismo que los identificadores, y entonces consultar los identificadores en una tabla de palabras reservadas tScsptiés de la aceptación. En realidad, el principio de la stihca- denii niás larga garantiza que la única acción del analizador léxico que necesita niodificar- se es el token que es devuelto. De este modo, las palabras reservadas se cimsideran s6Io úes- pués (le que se ha reconocido un identificador.

Volvamos ahora a un análisis del código para iinplemeritar este DFA, el cual está con- tenido en los archivos 8can.h y 8can.c (véase el apéndice B). El procedimiento pririci- pal es getToken (líneas 674-793), el cual consume caracteres de entrada y ilevuelve el si- guiente tokeri reconocido de acuerdo con el DFA de la figura 2.10. La irnpleri~entacióu utilizael análisis case doblemente anidado que describimos en la sección 2.3.3, c m una gran l i s t ~ case basada en el estado, dentro de la cual se encuentran listas case individuales basa- das en el carácter de entrada actual. Los tokens mismos están definidos como un tipo enu- merado en globals . h (líneas 174- 186), el cual incluye todos los tokens enumerados en la tabla 2.1, junto con los tokens de administración EOF (cuando el fin del archivo se alcan- za) y ERROR (cuando se encuentra un carácter erroneo). Los estados del analizador léxico también est6n definidos como un tipo enumerado, pero dentro del analizador itiisrno (líneas' 612-614).

Un analizador léxico tarnbiSn necesita en general calcular 10s atributos, s i existen. de cada token, y en ocasiones tornar también otras acciones (tales coi110 la inserción de ideu- tificadores eii una tabla de símbolos). En el caso del malizador Iéxico de TlNY, e1 único ntr~buto que \e calcula es el Icxeina, o valor de cadena del token reconociclo. y &te \e c(h>- ea en la variable tokenstring. Esta v:iri:iblc, junto con getToken, son 105 Gnicos servicios ofrecidos a otras partes del conipilador, y sus c(ci'inici«nes estlín reunidas en el :ir- chivo de cabecera scan. h (líneas 550-57 1). Observe que tokenstring se i.leclarri con iula Iorigitutl fija de 41, así ~ I K los iileutific;rtloi.es. por qjcmplo, no j>~~etleri tericr mis (le -1-0 m x t c r c s (más e l cnrickr nril(i [le tcrnrin;icirin). Ebta cs una liriiit;¡ci<j~i que se an i~ l imr i [ > ~ ) s i c i ~ i o r ~ ~ ~ e ~ ~ t e .

Page 82: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

El analizador léxico emplea tres variables globales: las variables de archivo source y listing, y la variable entera lineno, que están declaradas en globals .h y asignadas e inicializadas en main. c.

La administración adicional efectuada por el procediniiento getToken es conio se des- cribe a continuaci6n. La tabla reservedwords (Iíneas 649-656) y el procedimiento re- servedLookup (Iíneas 658-666) realizan una búsqueda de palabras reservadas después de que un identificador es reconocido por la iteración principal de getToken, y el valor de currentToken es modificado de acuerdo con esto. Una variable de marca ("flag") deno- minada save se utiliza para indicar si un carácter está por ser agregado a tokenstring; esto es necesario, porque los espacios en blanco, los comentarios y las búsquedas no consu- mitlas no deberán incluirse.

La entrada de caracteres al analizador léxico se proporciona mediante la función get - NextChar (Iíneas 627-642), la cual obtiene los caracteres (le lineBuf, un buffer interno de 156 caracteres para el analizador Iéxico. Si el buffer se agota, getNextChar lo "refrcs- ca" o renueva desde el archivo fuente con el procedimiento estándar de C fgets, asu- miendo cada vez yue una nueva línea de código fuente se está alcanzando (e increnientan- do lineno). Aunque esta suposición admite un código más simple, un programa 'TINY con líneas de más de 255 caracteres no se manejará de manera tan correcta como se necesita. Dejaremos la investigación del comportamiento de getNextChar en este caso (y las me- joras a su comportamiento) para los ejercicios.

Finalmente, el reconocimiento de números e identificadores en TINY requiere que las transiciones hacia el estado final desde ENTRADANUM y ENTRADAID no sean consu- midas (véase la figura 2.10). Implementaremos esto al proporcionar un procedimiento ungetNextChar (Iíneas 644-647) que respalde un carácter en el buffer de entrada. De nuevo esto no funciona muy bien para programas con Iíneas de código fuente muy largas, y las alternativas se exploran en los ejercicios.

Como una ilustración del comportamiento del analizador léxico TINY, considere el programa TINY sample. tny de la figura 2.1 1 (el mismo programa que se dio como c,jeinplo en el capítulo 1). La figura 2.12 muestra el listado de salida del analizador Iéxico, dado este programa como la entrada, cuando TraceScan y EchoSource están estable- cidos.

El resto de esta sección se dedicará a elaborar algunas de las cuestiones de itnplementa- ción alcanzadas mediante esta implemeiitación de analizador Iéxico.

Figura 2 11 Programa de muestra en el lenguaje TINY

Page 83: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Implementacton de un analizador ié.xtco TINY ("D~mtnuto")

Fiqurr 2 11 salida del analtzador lexm

proporctonand~ al programa llNY de la ftgura 2 1 I tomo

entrada

TINY COMPILATION: sample.tny 1: { Programa de muestra 2: en lenguaje TINY - 3: calcula factoriales 4: }

5: read x; { se introduce un entero 1 5: reserved word: read 5: ID, name= x 5: ;

6: if O < x then f no se calcula si x <= O 1 6: reserved word: if 6: NUM, val= O 6: < 6: ID, name= x 6: reserved word: then

7: fact :=1; 7: ID, name= fact 7: := 7: NUM, val= 1 7: ;

8: regeat 8: reserved word: repeat

9: fact := fact * x; 9: ID, name= fact 9: := 9: ID, names fact 9: * 9: ID, namer x 9: ;

10: x : = x - 1 10: ID, name= x 10: := 10: ID, name= x 10: - 10: NUM, val= 1

11: until x = 0; 11: reserved word: until 11: ID, name x 11: = Ilr NUM, val= O 11: ;

12: write fact { salida del factorial de x 1 12: reserved word: write 12: ID, name= fact

13: end 13: reserved word: end 14: EOF

Page 84: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 1 RASTREO O ANÁLISIS LÉXICO

25.2 Palabras reservadas contra identificadores Nuestro analizador léxico TINY reconoce palabras reservadas considerándolas primero co- mo identificadores y posteriormente buscándolas en una tabla de palabras reservtrdas. Esto es una práctica común en analizadores léxicos, pero significa que la eficiencia del analiza- dor dependerá de la eficiencia del proceso de búsqueda en la tabla de palabras reservadas. En nuestro analizador léxico utilizamos un método muy simple (la búsqueda lineal), en el cual la tabla se consulta de manera secuencia1 de principio a fin. Esto no es un problema para tablas muy pequeñas como las de TINY, que sólo tienen ocho palabras reservadas, pero se welve una situación inaceptable en analizadores léxicos para los lenguajes reales, los cuales por lo regular tienen entre 30 y 60 palabras reservadas. En este caso se requiere de una búsqueda más rápida, y esto puede exigir el uso de una mejor estructura de datos que el de una lista lineal. Una posibilidad es una búsqueda binaria, la cual podríanios haber apli- cado al escribir la lista de palabras reservadas en orden alfabético. Otra posibilidad es em- plear una tabla de cálculo de direcciones (o tabla de dispersión). En este caso nos gustaría utilizar una función de cálculo de dirección que tuviera un número muy pequeño de colisio- nes. Una función de cálculo de direcciones de esta clase se puede desarrollar en principio, porque las palabras reservadas no van a cambiar (al menos no rápidamente), y sus lugares en la tabla estarán fijos para toda ejecución del compilador. Se realizó cierto esfuerzo de investigación para determinar las funciones de cálculo de dirección mínimas perfectas para diversos lenguajes, es decir, funciones que distinguen cada patabra reservada de las otras, y que tienen el número mínimo de valores, de manera que se puede utilizar una tabla de cálculo de dirección no mayor que el número de palabras reservadas. Por ejemplo, si sólo existen ocho palabras reservadas, entonces una función de cálculo de dirección mí- nima perfecta produciría siempre un valor de O a 7, y cada palabra reservada producir& un valor diferente. (Véase la sección de notas y referencias para mayor información.)

Otra opción para tratar con palabras reservadas es emplear la misma tabla que almace- na identificadores, es decir, la tabla de símbolos. Antes de comenzar el procesamiento, se introducen todas las palabras reservadas en esta tabla y se marcan como reservadas (de ma- nera que no se permita una redethición). Esto tiene la ventaja de que se requiere sólo una tabla de búsqueda. Sin embargo, en el analizador léxico de TINY no construimos la tabla de símbolos sino hasta después de la fase de análisis léxico, de manera que esta solución no es apropiada para este diseño en particular.

2.5.3 Asignación de espacio para identificadores Un defecto adicional en el diseño del analizador léxico TINY es que las cadenas que for- man el token sólo pueden tener como máximo 40 caracteres. Esto no es un problema para la mayoría de los tokens, ya que sus tamaños de cadena son fijos, pero es un problema pa- ra los identificadores, porque los lenguajes de programación a menudo requieren que iden- tificadores arbitrariamente extensos se permitan en los programas. Aún peor, si asignarnos un arreglo de 40 caracteres para cada identiticador, entonces se desperdicia gran parte del espacio, debido a que la mayoría de los identificadores son breves. Esto no ocurre en el có- digo del compilador TINY, porque las cadenas que forman el token se copian utilizando la Función utilitaria cogystring, que asigna de manera dinámica sólo el cspacio necesario, como veremos en el capítulo 4. Una solución para la limitación de tamaño de tokenString sería semejante: asignar espacio sólo con base en las necesidades, utilizando posiblemente la función estándar de C realloc. Una alternativa es asignar un arreglo inicial grande para todos los identificadores y después realizar una asignación de memoria "a mano" dentro de este arreglo. (Éste es u n caso especial de los esquemas de ~tdniinistraci6n de memoria ~linámica estáudar que se analizaron en iil capítulo 7.)

Page 85: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Uro de iex para generar automatitamente un analizador Iéxico

2.6 USO DE Lex PARA GENERAR AUTOMATICAMENTE UN ANALIZADOR LEXICO

Eii esta seecicín repeiiriios el desarrollo tle un a i ia l i~ador Iéxico p m el Icnguaje 'I'INY que se realizó en lu seccibn anterior. pero ahora u t i l i~aremos el gerierxlor del :iiializxlor Iéxico Lex para gcnci-;ir uii iitiali/,ador a partir de iioa descr ipc ih de los tokens de T I N Y ~.orrro cxpresiorics regulares. Corno existe tina variedad de versiories diferentes de Ldex, lirtr~tareinos nuestro ~uiál isis a aquellas caracteristicas que soti comunes a todas o ;i la ma- yoría cic las versiones. L a versi6n más populrtr de Lex se conoce com« flex (por "F;rst 1.e~"). Se distribuye coino parte del paquete compiiador (;nu que produce la Frec SOTI- ware Foundation, y tamhién se encuentra disponible gratuitamente en ~ i ~ u c l i o s sitios de Internet.

L,cx es un progratiia que torna como entrada un archivo de texto que citntieite expresio- nes regulares. jririto con las acciones que se tmn:triri cuando se iguale ciicta expresi6n. Este programa produce un archivo de s:ilitla que contiene un código fiiente en C que define un procedimiento yylex que es una itnplerneiitación controlailn por labla (le un DFA correspondiente a las expresiones regulares del archivo de entrada, y que funciona como un prctccdiniietito getToken. E l archivo de salida de Lcx, denorriinado por l o general 1ex.yy.c o 1exyy.c. se compila y l iga entonces a un programa principal para obtener un prograiria ejecutable. exactamente como se l igó el archivo scan.c con el archivo tiny . c en la sección anterior.

E n lo que sigue, analizaremos en primer lugar las corivericioiies tle [.ex para escrihir expresiones regulares y el formato de un iirchivo de entrada de Lex, p:rni continuar con e l ani l isis del archivo de entrada de Lex del analizador léxico T I N Y que se dio cn el apéndice B.

2.6.1 Convenciones de Lex para expresiones regulares

Las convenciories de [,ex so11 muy similares a I;is que se analiz:iron en la secciítn 2.2.3. t i 1

vez de enumerar todos los inctacitracteres de Lex y describirlos de manera iiitiivitlual, clarc- iiios una perspectiva general y posteriormente propctrci«naueiiios iris conveiiciones de [..ex en una tabla.

[.ex permite l a correspoiidericia be caracteres simples, 0 cadenas de caracteres, con s6lo escribir los caracteres eii secuencia, coino lo hiciriios en secciories ariteriores. [.ex tarnbién permite que los inctacirracteres se igualen como caracteres reales errceixáridolos entre cumillas. Las cor i~ i l las tarnbién se pueden escribir en torno de caracteres que no son metacaracteres, donde no tienen efecto alguno. De este modo, tiene sentido escrihir co- millas para encerrar a todos los caracteres que se vayan a igtialar directamente, sean o rto metacarneteres. Por ejemplo, podernos escrihir if o "if" para igu:tlar la palabra reser- vada if que da inicio a una sentencia "if" o condiciorial. ['or otra parte, para igunliir un paréntesis izquierdo, debeinos escribir *' ( ' l , puesto que es un metacarieter. Una alterna- t iva es util izar e l nietacaricter de diagonal invertida \. pero esto s6lo iunciona p i r a n ~ - tacaracteres simples: para igualar la secuencia de caracteres ( * tendríainos que escrihir \ ( \ *, repitiendo dicha diagonal invertida. Evideittcrrrente, es prcferiblc cscr-ibir " ( * 'I Tainbién e1 LISO de la tli:igo~ial iiivcrtitla con caracteres regulares puede tcncr ti11 h ig t~ i l i - c;ido especial. Por ejeruplo. \n citri-esponde a un retorno de línea y \t corrcspotide a uria t;thulacicín (éstas son ctiiivcnciorics típicas de C, y I i i rii;tyoría de tales coiivertcioircs \e IIcvnii a Lex).

1-cx irilcrpreia 10s i~~ct:iciil-;ick~rcs *. +, (. ) y i tlc la iriniicra hühiui;il. T : t i i~h i~ i r uiiliz;t e l sigtio de iriterrr)gnci(íri como 1.111 ~iict:i~.:ir:íctcr porü iiidiciii- LIII;~ piirtc opc i~~ i i : i I . (:o1110

Page 86: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

ejeniplo ile la notación de Lex malizada hasta ahora, podetnos escribir tina expresión re- gular para el conjunto de cadenas de las (1 y h que comienzan con ciri o hh y tienen una c opcional al find como

La convención de Lex para clases de caracteres fconjuntos de caracteres) es escribirlas entre corchetes (paréntesis cuadrados). Por ejemplo, [abxzl significa cualquiera de los caracteres a, b, x o i , y podríamos escribir la anterior expresión regular en Lex corito

(aa l bb) [abl *c?

También se pueden escribir intervalos ile caracteres en esta forma empleando un guión. De este modo, la expresión [O-91 significa en I.ex cualquiera de los dígit6s desde el cero hasta el nueve. Un punto es un metacarácter que también representa un conjunto de carac- teres: representa cualquier carácter, excepto un retorno de línea. También se pueden escri- bir conjuntos complementarios (es decir. conjutitos que rco contienen ciertos caracteres) en esta notación utilizando el signo carat como el primer carácter dentro de los corchetes. De esta manera, [AO-9abc] significa cualquier carácter que no sea un dígito y no sea tina de las letras u. h o c.

Como ejemplo escribamos una expresión regular para el conjunto de números con signo que pueden contener tina parte fracciona1 o un exponente comenzando con la letra E (esta expresión se escribió en torma un poco diferente en la sccción 2.2.4):

IJna curiosa característica de Lex es que dentro de los corchetes (que representan una clase de caracteres), la mayoria de los metacaracteres pierden su estatus especial y no ne- cesitan estar entre comillas. Incluso el guión se puede describir como un carácter regular si está enumerado primero. Así, podríamos haber escrito [ -+] en lugar de ( " + I r 1 " - " ) en la expresión regular anterior para números (pero no [+- 1 debido al uso del n~etacarácter de - para expresar un intervalo de caracteres). Conio otro ejemplo, [ . " ? 1 significa ctialquiera de los tres caracteres, punto, comillas o signo de interrogación (los tres caracteres perdieron su significado de metacarácter dentro de los corchetes). Sin embargo, algunos caracteres son todavía ntetacaracteres incluso dentro de los corclietes, y para obtener el carkcter real debemos anteceder al cardcter con una diagonal invertida (las comillas no se pueden utilizar porqne perdieron su significado de nietacarácter). De esta manera, [ \" \ \ 1 significa cualesqnizra ile los caracteres reales A o \ .

Una importante convención adicional de metacarácter en Lex es el uso de llaves para denotar nombres de expresiones regulares. Recordenlos que a una expresión regular se le puede dar un nombre, y que estos nombres se pueden utilizar en otras expresiones regulares mientras que no haya rcfcrencias recursivas. Por ejen~pb, defininros naturalconSigno cn la sección 2.2.4 corno sigue:

nat = [O-91+

natconsigno = ( " + " I " - " ) ? n a t

Page 87: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Uso de Lex para generar automaticamente un analizador léxico 83

tiri éste y otros ejeinplos utili~ain»s cursivas « itilic:is para distinguir los notnbres de las se- cuencias ordinarias de caracteres. Siri ernbargo, los archivos de Lex son iirchivos de texto ordinarios, así que no se dispone tle letras ikÍlic;rs. En su lugar, Idex riiilira la cotiveiici611 de que los ttomhres previatneitte definitlos se cilcierrcri ~tietliarite llaves. De este titodo. el ejem- plo anterior aparecería cotitci sipie ctl Ixx (Lex iaiithién prescinde del signo de igwldad eri la ileSiiiici6n de nuinbres):

n a t [O-91+

natconSigno ( + l - ) ? h a t >

Advierta que las llaves no aparecen cuando se define un notnhre, sólo cuando se utiliza. La tabla 2.2 contietie tina lista resutnida de las coiivenciones de metacaracteres de Lex

que hemos analirado. Existen varias otras coirvciiciones de metacaracteres en L.cx que no utilizareinos y 110 analizawntos aquí (véatise las rcfereticias al fitial tld c~tpítiilo).

Tablr 1 2 Convenciones de Patrón Significado metacaracteres en tex

a el carácter a

"a,, el carácter a. incluso si <i es un inetacarácter \a c. ,rr,icter .' rr cuarido 11 cs u n rrietacai.áctcr a* cero o más rcpeticioiies de rr a + una o m;is repeticiones de u

( a ) 11 niisrna 1 abc 1 cualquiera de los caractcrcs o, 1) o c

[a-dl crialquieríi de los caractcrcs o. h. c o rl

iAabl cualquier cnr5cter exccpto o o h

cualquier carácter excepto iiii retorno (le línea i xxx > la expresión regular que representa el nombre crv

2.6.2 El formato de un archivo de entrada de Lex

Uri archivo de entrada tle [,ex se compotre de tres partes. tina coleccióii de definiciones, una colección de reglas y una coleccicín de rutinas auxiliares o rutinas del usuario. Las tres secciones están separadas por sigiios de porcentaje dobles que aparecen en líneas sepamdas cornenrando en la pri~iiera colunina. Por coiisiguictite. el diseño de tiii archivo de entrada de 1.ex es coino se tniiestra a coiitinuación:

{ d e f i n i c i o n e s ) %%

{ r e g l a s ) "9%

( r u t i n a s a u x i l i a r e s 1

Page 88: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 1 RASTREO O ANÁLISIS LÉXICO

Para entender adecuadamente cómo interpreta Lex un archivo de entrada de esta clase debernos tener en mente que algunas partes del archivo serán iuformación de expresión LE-

gular que Lcx utiliza para guiar su construcción del código de salida en C, mientras que otras partes del archivo serán cóiiigo C real que estamos suministrando a Lex, y que Lex in- sertará textualmente en el código de salida en la localidad apropiada. Dareinos las reglas exactas que Lex emplea para esto después de que hay:nnos analizado cada tina de las tres secciones y proporcionado ~Igiinos ejemplos.

1.a sección de definición se presenta antes del primer %%. Contiene dos elementos. En primer lugar, cualquier código C que se deba insertar de manera externa a cualquier función debería aparecer en esta sección entre los delimitadores %{ y %}. (¡Fíjese en el orden de es- tos caracteres!) En segundo lugar, los nombres para expresiones regulares tanibién se deben definir en esta sección. Un nombre se define al escribirlo en una línea por separrtdo, comen- zando en la primera column;~ y continuando (después de uno o más espacios en blancoj con la expresión regular que representa.

La segunda sección contiene las reglas. Éstas se componen de una secuencia de expre- siones regulares seguidas por el ccídigo en C que se va a ejecutar citando se iguale la expresión regular correspondiente.

La tercera y última sección contiene código C para cualquier rutina auxiliar que se in- voque en la segunda sección y que no se haya definido en otro sitio. Esta sección también puede contener un programa principal, si queremos compilar la salida de Lex coino un pro- grama autónomo. Incluso se puede omitir, en cuyo caso no se necesita describir el segundo %%. (El primer %% siempre es necesario.)

Daremos una serie de ~jetnplos para ilustrar el formato de un archivo de entrada de ILex.

Ejemplo 2.20 1.3 siguiente entrada de Lex especifica un analizador léxico que agrega números de línea al texto y envía su salida a la pantalla (o a un archivo, si es redirigido):

% {

/ * un programa de Lex que agrega números de línea a las líneas de texto, imprimiendo el texto nuevo en la salida estándar

* / #include <stdio.h>

int lineno = 1;

%l line .*\n

%%

(line) { printf ("%5d %S", lineno++, yytextf ; 1 %%

main()

{ yylex0; return O; > Por qjemplo, ejecutar el programa ohtenido de Lex en este mismo archivo de entrada pro- porciona la .;alida siguiente:

1 %{

2 / * un programa de Lex que agrega números de línea

Page 89: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Uso de Lex para generar automáticamente u n analrzador Iéxtto

3 a las líneas de texto, imgrimiendo el texto nuevo

4 en la salida estándar

5 * / 6 #include <stdio.h>

7 int lineno = 1;

8 %)

9 line .*\n

10 %%

11 (line) ( printf ("%5d %s",linenott,yytext); 1 12 %%

13 maint)

14 i yylex0; return O; 1

1-laremos comentarios en este archivo de rtitrada de Lex utiliründo estos nlírneros de Iínea. En primer lugar. las líneas I hasta la X se ericiieritran entre los deliinitadores %{ y % } . Esto provoca que estas Iíiieas se irisertcn direcintnerite en el código C protliicido por Lex, externo u cualquier procedimiento. En ~xirtictilar, el comeiitario cti las Iíneiis 2 a la 5 se insertará cerca del principio del progratna, y la directiva #include y la definición de la variable entera lineno en las líneas 6 y 7 se insertará externamente. de rnoikt que lineno se corivierte en una variable global y su valor se inicia1ií.a a 1 . La otra definí- cióii que aparece antes del primer %% es 1;1 definición del nctinhre line, qiie se define cn- nio la expresión rcgttlar ". *\nU. cpe iguala a O o más caracteres (sin iticliiir un rctorno de Iínea). seguida por u n reiortio de Iíiicn. En otras palabras, la cxpresicín regular clet'i- nida por line iguala cada Iínea de entrada. A coritiiiuacibri del %% en la Iítiea 10, la Iínea 1 I coiiiprende la sección de accióii del archivo de entrütla de L e s En este caso es- crihiinos una acción simple por reali~arse sieinpre que se iguale una line (line esti encerrada entre llaves para distinguirla como un noinbre, de aciicrdo con la convcnci6n de ¡.ex). Despiit5s de la expresibn regulx c s ~ i la acción, es decir, el código cn C que se

i va a ejecutar siempre que se iguale fa cxpresi6ri regular. En este ejernplo la accibn con- siste en una sentencia simple en C, que se encuentra contenida dentro de las llaves de tin

hloclue de C. (No olviile que las llaves que encierran CI nombre line tienen una función por completo diferente de la que tienen las llaves que forinai~ un bloqrte en el código de C (le la siguiente acción.) Esta sentencia en C es para imprimir el niimero tic Iínen (en campo de cinco espacios, justificado a la derecha) y la cadena yytext, después de la cual se i t i -

creinerita lineno. El nombre yytext es e1 nombre interno que Lex le da a la cadena igtialada por la expresih regular, que en este caso se compone de cada Iínea de entrada (incluyendo el retorno de línea).' Fi~ialtnente se inserta el código en C después del seguti- do síniboio de porcentaje doble (líneas 13 y 14), cotno estlí al Final del código C produ- cido por Lxx. En este ejcrnplo el código se compone de la definición de ttri procedirnierito main que llama a In fiinción yylex. Esto permite que el código C protlucitlo por Lex sea coinpilaclo en un programa cjeciitahlc. (yylex es cl nonibre que se da al procedi- rnieiito construido por Lcx que itnplenietita el DFA asociado con las eupresioiics regula- i-cs y acciones tlnrlas m la sección de :iccióti del archivo de entrada.) 5

Page 90: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejemplo 2.2 1 Considere el siguiente archivo de entrada de Lcx:

% (

/ * un programa de Lex que cambia todos los números de notación decimal a notación hexadecimal,

imprimiendo una estadística de resumen hacia stderr

* / #include <stdlib.h>

#include tstdio.h>

int count = 0;

%)

digit [O-91

number (digit)+

06%

{number) { int n = atoi(yytext); printf ( "%x" , n); if (n > 9) count++; 1

%%

main( )

I n l e x 0 ; fprintf (stderr,"número de reemplazos = %d",

count ) ;

return O;

1

Éste es semejante en estructura al ejemplo anterior. excepto que el procediniiento main im- prime el conteo del número de reemplazos para s t d e r r después de llamar a yylex. Este ejemplo también es diferente en el sentido de que no todo el texto es igualado. En realidad, sólo los números se igualan en la sección de acción, donde el código C pard la acciún primero convierte la cadena igualada (yytext) a un entero n, luego lo imprime en fornia hexadecimal (p r in t f ('r%x", . . . )), y finalmente incrementa count si el número es mayor que 9. (Si e1 número es niás pequeño que o igual a 9, entonces no tendrá aspecto diferente en hexadecimal.) Así, la única acción especificada es para cadenas que son secuen- cias de dígitos. Lex todavía genera un programa que también iguala todos los caracteres no numéricos, y los pasa hasta la salida. &te es un ejemplo de una acción predeterminada por Lex. Si un carácter o cadena de caracteres no se iguala por ninguna de las expresiones regu- lares en la sección de acción, Lex lo hará de manera predeterminada y duplicará la acción hacia la salida (hará "eco"). (También se puede obligar a Lex a generar un error de ejecución, pero aquí no estudiaremos cómo se hace.) La acción predeterminada también se puede irtciicar en forma específica tilediante la macro ECHO de Lex definida internamente. (Estudiaremos este uso en el siguiente ejemplo.) 9

Ejemplo 2.22 Considere el \iguiente archivo de entrada de Lex:

% (

/ + Selecciona solamente líneas que finalizan o

comienzan con la letra 'a'.

Elimina todo lo demás.

* /

Page 91: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Uso de Lex pan generar automáticamente un analizador Iixico

#include <stdio.h>

%)

finaliza-con-a .*a\n

comienza-con-a a.*\n

86%

(finaliza-con-a? ECHO;

(comienza-con-a) ECHO;

.*\n ;

%%

main ( )

f yylex0; return O; )

Esta entrada de Lex provoca que todas las líneas de entrada que comiencen o fiitalicen con el carácter a se escriban hacia la salida. Todas las otras líneas de entrada se suprimen. La eliminación de la entrada es causada por la regla bajo las reglas ECHO. En esta regla la acción "vacía" se especifica mediante la expresión regular . *\n al escribir un signo de punto y coma para el código de acción en C.

Existe una característica adicional en esta entrada de Lex que vale la pena resaltar. Las reglas enumeradas son ambiguas en el sentido de que una cadena puede igualar más de una regla. De hecho, cualquier línea de entrada iguala la expresión . * \n, sin tener en cuenta si es parte de una línea que comience o finalice con una a. Lex tiene un sistema priorita- rio para resolver tales ambigüedades. En primer lugar, Lex siempre iguala la subcadena más larga posible (de modo que Lex siempre genera un analizador léxico que sigue el principio de la subcadena más larga). Entonces, si la subcadena más larga todavía iguala dos o más reglas, Lex elige la primera regla en el orden en que estén clasificadas en la sección de acción. Es por esto que el archivo de entrada de Lex anterior enumera primero las acciones ECHO. Si tuviéramos clasificadas las acciones en el orden siguiente,

.*\n ;

(finalizacon-a) ECHO;

(comienza-con-a} ECHO;

entonces el programa producido por Lex no generaría ninguna salida para cualquier archivo. porque toda línea de la entrada se reconocería por medio de la primera regla. 9

Ejemplo 2.23 En este ejemplo, Lex genera un programa que convertirá todas las letras mayúsculas, excepto las letras dentro de los comentanos estilo C (es decir, cualquier texto dentro de los delimi- tadores / * . . . */):

% (

/ * Programa en Lex para convertir letras mayúsculas a letras minúsculas excepto dentro de los comentarios

* / #include <stdio.h>

#ifndef FALSE

#define FALSE O

#endif

#ifndef TRUE

#define TRUE 1

Page 92: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

#endif

%)

%%

[A-z ] (putchar(tolower(yytext[Ol));

/ * yytext [ O ] es un solo carácter

en letras mayúsculas encontrado * / )

,S/ * ,p ( char c;

int done = FALSE;

ECHO;

do

( while ((c=inputO)!='*')

putchar(c);

gutchar(c);

while ((c=inputO)=='*')

putchar(c);

gutchar(c);

if (C == ' / ' ) done = TRUE;

) while (!done);

1 %%

void main(void)

( yylex0;)

Este ejemplo muestra cómo se puede escribir el código para esquivar expresiones regulares difíciles e implementar un pequeño DFA directamente como una acción de Lex. Kecuer- de que en el análisis que se realizó en la sección 2.2.4 se determinó que una expresión re- gular para un comentario en C es muy compleja de escribir. En su lugar, escribirnos una expresión regular sólo para la cadena que comienza un eomenrario tipo C (es decir, " /*") y después suministramos un código de acción que buscará la cadena de terrnina- ción " * / ", mientras proporcionamos la acción apropiada para otros caracteres dentro del comentario (en este caso precisamente para duplicarlos sin procesamiento adicional). Hacemos esto al imitar el DFA del ejemplo 2.9 (véase la figura 2.4, página 53). Una vez que liemos reconocido la cadena "/*", estamos en el estado 3, de modo que aquí elige nuestro código el DFA. La primera cosa que hacemos es recorrer los caracteres (duplicán- dolos hacia la salida) hasta que vemos un asterisco (correspondiente al bucle o t r o en el estado 3). como sigue:

while ((c=inputO)!='*') gutchar(c);

Aquí todavía utilizamos otro procedimiento interno de Lex denominado input . El tiso de este procedimiento, más que una entrada directa utilizando getchar, asegura que se utilice el buffer de entrada de Lex, y que se conserve la estructura interna de la cadena de entrada. (Advierta, sin embargo, que empleamos un procetiimierito de salida directo gutchar. Esto se analizará con nxís detalle en la secci6n 2.6.4.)

El siguiente paso en nuestro código para el DFA corresponde al estado 4. Repixireirtos c1 ciclo de nuevo hasta que rlo veamos un asterisco, y entonces, si el mricter es uria diagonal. Icrnlini~nios: dc otrit manera, regresarnos al estado 3. 5

Page 93: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Uso de Lex para generar automi!itameiite ttn aoalrrador léxrto 89

Firializainos csra subseccicín con riri resumen de 13s conve~icio~ics (le l.cx que present:tnriis m los ejemplos.

RESOLUCIÓN DE AMBIGÜEDAD !La salida de Lxx sieiiipre iguala en priiiier lugar la subcadena iitJs larga posihlc pasa una rcgla. Si dos o más reglas provocan que re recotiozcnri s~ibcadcniis tic igtial Iotq$titl. e11- tonces la salida de I.ex elegirá la rcgla clasilicada en primer lugar en la sección de nc- cicín. Si ninguna rcgla coincide con cualquier siibcailena no vacía. errtotices la acción pretleterminada copiii el siguiente carácter a la salida y cotitiiiúa.

INSERCI~N DE CÓDIGO c I ) Citalquier texto escrito entre %{ y %} en la sección de definicicíri se copiará directa- mente hacia el programa de sdida externo para c~talquier procedimicnto. 2) Cualquier texto en la sección de procedimientos auxiliares se copiará directamente al pmgratna de salida al final del código de Lex. 3) Cualquier ccídigo que siga a tina cxpresicín regular (por al rnenos un espacio) en la secciíin de acción (después del prirner %%) se insertará en el lugar ;ipropiatlo en el procedimiento de reconocimiento yylex y se ejecutará ctiantlo se presente una coi~icidencia de la expresión regular correspondiente. E l c k i i g o C que representa una acción puede ser una sentencia simple en C (1 una senteticia com- puesta en C compuesta por cualesquiera declaraciones y sentencias erieerratlas entre llaves.

NOMBRES INTERNOS L a tabla 2.3 enumera los nornhres internos de Lex que analizamos en este capítulo. L a mayor parte de ellos ya se estudiaron en los ejemplos anteriores.

Tnblc 1.3 Algunos nombres internos en Lex Nombre interno en Lex SignificadolUso

1ex.yy.co 1exyy.c Noriihre de archivo de sdlida de Lex yylex Rutina de iiniilisis léxico de Lex yytext Ctdenü reconoctda en le :iccicín actud

yyin Arch~vo de entrada de Lex (predeterin~ndo. stdin) =out Aichivo de d i d a de Lex (predetermin,ido stdoutf input Rutitla de cntrada con hutfer de Lex ECHO AcciC>n pretletermlnada de [.ex (itl~prinle yytext a yyout)

0b.iervamoí una caracterktica dc eíta tabla que no \e había mencionado, la de qtic Lex t i m e sus propios nornhres internos para los archivos. (le los cuales torna la entrada y h,i.'. . LI,~ 1 »S . ~ua les . ciivía la salida: yyin y yyout. Por medio (le la rutina de entrada están- tkir de Lcx input autornálic;inientc tornará la e11tr:ida del archivo yyin. Sin eilihorgo. en los ~!jempl«s ariteriores. cvit;iiiios e l archivo de salida interno w o u t y sólo cscrihiiiros 1. ,I 4 i d a . estándiir uti l izaiit lo printf y putchar. Una rncjor iinplernent;icitiii, q iw pcririitc la asigtiacicín tlc la s ~ l i d a a ui i ;irchivo arbitnirio' reernpi:i/xía estos irsos con

Page 94: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 1 RASTREO O ANALISIS L ~ X I C O

2.63 Un analizador léxico TINY que utiliza Lex

El apéndice B ofrece un listado de un archivo de entrada en Lex tiny. 1 que generará un analizador Iéxico para el lengtiaje TINY, cuyos tokeiis se describieron en la sección 2.5 (véase la tabla 2.1 ). A continuación haremos varias observaciones acerca de este archivo de entrada (líneas 3000-3072).

En primer lugar, en la sección de definiciones, el código C que insertamos directainen- te en la salida de Lex se compone de tres directivas #include (globals . h, util. h y sean. h) y la definición del atributo tokenstring. Esto es necesario para proporcionar la interfaz entre el analizador Iéxico y el resto del compilador TINY.

El contenido adicional de la sección de definición comprende las definiciones de los nombres para las expresiones regulares que definen los tokens de TINY. Observe que la definición de number utiliza el nombre previamente definido digit, y la definición de identifier utiliza a letter definida con anterioridad. Las definiciones también dis- tinguen entre retornos de línea y otros espacios en blanco (espacios y tabnlaciones, líneas 3019 y 3020), porque un retorno de Iínea provocará que lineno se incremente.

La sección de acción de la entrada de Lex se compone del listado de los diversos tokens, junto con una sentencia retum que devuelve el token apropiado como se define en glo- b a l ~ . h. En esta definición de Lex enumerantos las reglas para palabras reservadas antes que la regla para un identificador. Si se hubiera enumerado primero la regla del identifica- dor, las reglas de resolución de ambigüedad de Lex hubieran podido ocasionar que siempre se reconociera un identificador en lugar de una palabra reservada. También podríamos es- crihir código como en el analizador Iéxico de la sección anterior, en la que sólo se recono- cían identificadores, y posteriormente se buscaban las palabras reservadas en una tübla. Esto sería en realidad preferible en un compilador real, puesto que palabras reservadas reconocidas por separado ocasionan que las tablas en el código del analizador Iéxico gene- rado por Lex crezcan enormemente (y, en consecuencia, la cantidad de memoria utilizada por el analizador).

Una peculiaritlad de la entrada de Lex es que tenemos que escribir un código para reco- nocer comentarios con el fin de aseguramos de que lineno se actualiza de manera correc- ta, aun cuando la expresión regular para los comentarios de TINY sea fácil de escribir. En realidad, la expresión regular es

"(M [ A \ ) ] * " ) "

(Advierta el uso de la diagonal inversa dentro de los corchetes para eliminar el significado de metacarácter de la llave derecha: las comillas no funcionürjn aquí.)'

Notamos también que no hay código escrito para regresar el token EOF al encontrar el final del archivo de entrada. El procedimiento yylex de Lex tiene un comportamiento pre- determinado al encontrar EOF: devuelve el valor O. A esto se debe que el token ENDFILE se haya escrito primero en la definición de TokenType en globals. h (línea 179), de manera que tendrá el valor 0.

Finalmente, el archivo tiny . 1 contiene una definición del procedimiento getToken en la sección de procedinlientos auxiliares (líneas 3056-3072). Aunque este código con- tiene algunas inicializaciones ex profeso de los procedimientos internos deLex (tales como yyin y yyout) que sería mejor efectuar directamente en el programa principal,

6. Algunas versiones de Lex tictien una variable yylineno definida intcrnainente cluc se :iciu:i-

lim de manera autom2itica. El uso rle esta vm-iahle en lugar de lineno permitiría eliniinar i.1 ciídigo espec'i;il.

Page 95: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

nos permi te u t i l i zar de manera directo e l anal izx ior I i x i c o generado po r Lcx, sin cambiar n i i igún ot ro ;irchivo en e l corrtpilador de TINY. En realidad. ilesptiés (le generar e l i t rchivo

t iel analizador léx ico <le C 1ex.yy. c (o lexyy. c), este ;irchivo se puede coi t ip i l ;~r y li- gar tlirectarxiente con los otros ;irchivos Sitente de TINY para p t i ~ l u c i r imii vcrsii ir i hns;itl:i

en l,cx del compilador. S i n eni'irgo, esta versi611 del cotnpi lador c;irece de u n servicio de 1, ,I L L I S I O ~ . . .: e anterior. en la q ~ i e n o se proporciona ct ídigo I'uen~e <luplicaclo con los ni imeros

de línea (véase cl ejercic io 2 .35) .

EJERCIMS 2.i. t:.scrik cxpresictnes reguiares par& los siguientes conjuntos de caracteres. o explique las

ramnes por las que no se puedcii cscrihir espresio~ics regul;tres:

a. T«d;is las c:tdenas di: letras nli~iúsculas que cornienceii y liiralicen con ii .

b. Todas las cadetias de letras tui~iúscul;ts que i~ n~rnicriccn II firialiceii c m ir (o amhos).

c. Todas Ins cadenas de dígitos que no crlritenfan ceros al principio.

d. Todas las criden:is de dígilos que representen nú~rieros parcs.

e. Todas las c;rdcnas de dígitos tales quc todos los nú~ncms "7" se preserrtc~~ ~111tes que todos

los "Y. f. Todas las cadenas dc o y O que no contengan tres b consccoriv:is.

g. Totlas las c:deiias de I I y h qiic contengan un número impar de u o im núniero impar dc h

(O iuiihos). h. Todas las cciden;~\ de n y h que contcng;rn un ~iúrnero par (le <i y un núii ieri~ p:ir (le h. i. 'Totlns las cadenas ile ir y h que coittcligm exact:rniente t:int;is 11 L.OI~I~I h.

2.2 Escriba descripcio~ies cn español para lo\ Ic~~gii i iJc\ gene~tdos por las signicritcs cxpresiones

regulares:

a. ( a l b ) * a ( a l b l r . ) b. ( A I B I ... I Z ) ( a l b l ... l z ) *

c. ( a a l b ) * ( a l b b ) *

d. ( 0 1 1 1 ... I Y l A l B I C I D I E I F ) + ( x l X ) 2.3 a. Muchos sistc.rnas contienen una vcrsinn de grcp (glohd regular expression pririt, ir irpresih

de expresi6n regular global en iiiglés). uti programa de húsqueda de expl-csioncs ~reg~iluws

escritu original~neritc par:, [:NfX.' Encuentre UII d~,cciniento que descritxr su grcp Iocol. y

ilescriha sus ciinvcrici~~rics respecto a rrict:isínihi~lr~s. b. Si su editor acepta alguna clase de expresio~ies regul;i~-es para s t ~ s búsquc<ltrs de c;ideiras.

dexcriha sus convenciones de inetasinrholos.

2.4 En la definición (le las cxpresioties regulares describirnos In prcccdencia (le las i~pcrncioties.

pero no su asoci;ttividad. Por cjernplo, no especificamris s i a I b I c significiih:~ ( a I b ) I c o

a l (b l c ) , y lo niisnro para la conc:rtcnacidn. ;,A qué dchió esto? 2.5 I~cii iuesirc que LAi-"") - L(r") p;m cri;ilqoier exprcsi<in scgirlar r . 2.6 Al describir los tokens de un lenguaje de prograiiiación utilirantlo expresiones regulares. no es

rieces;ii.io icner los irietasíniholos <t> (p:im el conj~itito v:ick)) o r, (pirr;r la c;~dena v;icia). Expliqrie

por qué. 2.7 1)ihujc un !>FA que <'orrcspon<la 11 la cupresiiin regular <I>. 2.8 r>ihult »FA para cada uno de los conjuritiis ¡le caracteres del iiicisii a) ti1 inciso i i di.\ i jcrc ic i i i

2. l. o cstahlczcn por qc16 mi existe un I)F,\.

2.9 1)ihrijc iin L)F:;\ que ;i<icple l ; i cii;rlrii p;~lahi-.is r e s ~ ~ r b : ~ r l ; ~ case, char, const y continue

&l Icngt~aJc C .

Page 96: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 i RASTREO O ANAUSIS LEXICO

2.10 Vitelra a escribir el pseudocitdigo para la implementación del DFA para coi~ieiitarios en C

(sección 2.i.3) urilizatido el c:u;icter de entrda etimo prucba en el case extc?rno y el c ~ t ~ d o

mino prueba del cüse intcrno. Compare su pseudocódigo con el del texto. ¿,En qué circt~tistan-

cias preferiría iisted utilizar esta orgaiiimcióti püra irnplzrnentaciíin de código de tin l>t'i\'? 2.1 1 Proporcione una definición matemática de la cerradura c: para un conjiinto de cstatlos tle

un NFA. 2.12 a. lltilice In crinstrucción de Thompson para corivertir la expresión regular ( a I b) *a

( a l b l e ) en'u~i NFA. b. Convierta el NFA del inciso n cn un »FA utilizando la construcción de suhcoiijuntos.

2.13 a. Utilice la construcción de I'hompson para convenir la expresiitn regulx ( a a I b ) * (a I b b ) * en un NFA.

b. Convierta el NFA del inciso a en un DFA utiliraiido la cr)nstnicción de suhconjuiitos.

2.14 Convierta el NFA del ejemplo 2. I O (secci6n 2.3.2) en un DFA por medio de la constriicción

de subconjuntos. 2.15 En la sección 2.41 se i~ienciorió ulia siiriplitlcación paiz la constn~ccirin de Thtrmpson para la

coticatenación que elimina la transiciún E entre los dos NFA de las expresiones regulares que

se están concatenancio. También se mencionó que esta simplificaci6n necesitaba el hecho de

que no hubiera transiciones kera del estado de aceptaciitn en los otros pasos de la construcción.

Proporcione un ejemplo para demostrar a qu i se debe esto. (Sugermcia: considere una nuevit

constnicción de NFA por repetición que elimine los nuevos estados de inicio y aceptación, y

entonces considere el NFA pira rTs*.)

2.16 Aplique el algoritmo de niininiización de estado de la sección 2.4.4 a los siguientes DFA:

Page 97: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]
Page 98: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 2 1 RASTREO O ANALISIS LEXICO

2.32 Agregue comentarios estilo ,\da al analizador léxico de TINY. (Un comcntruio en Ada coinienza con dos guiones y continúa hasta el final de la Iínea.)

2.33 Agregue la búsqueda de palabras reservadas en una tdhla para el anülizador léxico de Lex pa- re TINY (puede utilizar búsqueda lineal como en el analizador léxico de 'TINY escrito a tnano o cualquiera de los tnétodos de búsqueda sugeridos en el ejercicio 2.27).

2.34 Agregue comentarios estilo Ada al código de Lex para el analizador Iéxico de TINY. (Un co- mentario en Ada comienza con dos guiones y continúa hasta el final de la línea.)

2.35 Agregue duplicación de Iínea de código fuente (utilizando la ~narca EchoSource) al cótligo de Lex pard el andizador Iéxico de TINY, de manera que, cuando la marca esté activada, cada Iínea del código fuente se imprima al archivo del listado con el número de Iínea. (Esto requiere un conocimiento más extenso de los aspectos internos de Lex de lo que se ha estudiado.)

NOTAS Y La teoría matemática tie las expresiones regulares y 10s autómatas finitos se analiza con de- REFERENCIAS talle en ~opc ro f t y ~ ~ i t n a i i [19791, donde se pueden encontrar algunas refirencias respecto

al desarrollo histórico de la teoría. En particular, e puede hallar una demostración de la equi- valencia de los autómatas finitos y las expresiones regulares (empleamos sólo una dirección de la equivalencia en este capítulo). También se puede encontrar ahí un análisis sobre el "le- ma de la extracción" y sus consecuencias para las limitaciones de las expresiones regulares en la descripción de patrones. Una descripción más detallada del algoritmo de minimización de estados también se puede encontrar allí, junto con una demostración del hecho de que ta- les DFA son esencialmente únicas. La descripción de una constmcción en un paso de un DFA a partir de una expresión regular (en oposición al enfoque de dos pasos descrito aquí) se puede encontrar en Aho, Hopcroft y Ullman [1986]. Un método para compresión de ta- blas en un anali~ador léxico controlado por tabla también se proporciona allí. Una descrip- ción de la construcción de Thompson por medio de convenciones bastante diferentes de NFA respecto a las descritas en este capitulo se proporciona en Sedgewick [1990], dotide se pue- den hallar descripciones de algoritmos de búsqueda de tabla tales como búsqueda binüria y cálculo de dirección para reconocimiento de palabras reservadas. (El cálculo de dirección también se analiza en el capítulo 6.) Las funciones de cálculo de dirección mínimas per- fectas, mencionadas en la sección 2.5.2, se comentan en Cichelli /1'980] y Sager 119851. lJna utilidad denominada gperf se distribuye como parte del paquete de compilador Gnu. Puede generar rápidamente funciones de cálculo de dirección perfectas incluso para conjun- tos grandes de palabras reservadas. Aunque aq"éllas por lo general no son mínimas, toda- vía son bastante útiles en la práctica. Gperf se describe en Schmidt [1990].

La descripción original del generador de analizador Iéxico Lex se encuentra en Lesk 119751, el cual todavía es hasta cierto punto preciso para versiones más recientes. Versiones mhs recientes, especiaimente Hex (Paxson [1990]), han resuelto algunos problemas de eficien- cia y son competitivas incluso con analizadores l6xicos manuscritos minuciosamente ajus- tados (Jacobson 119871). Una descripcicín útil de Lex también se puede hallar en Schreiner y Friedman 119851, junto con más ejemplos de programas sintples en Lex para resolver varias tareas de coincidencia de patrones. tina breve descripción de la familia de grep de recono- cedores de patrones (ejercicio 2.3) se puede encontrar en Kernighan y Pike [1984], y un es- tudio más extenso en Aho (19791. La regla de fuera de lugar mencionada en la secciún 2.2.3 (un uso del espacio en blanco para suministrar formato) se analira en Landin 119661 y 1-Iutton (19921.

Page 99: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Capítulo 3

Gramáticas libres de conlexlo y análisis sinláclico

3.1 El proceso del análisis 3.4 Ambiguedad sintáctico 3.5 Notaciones extendidas: EBNF

3.2 Gramáticas libres de contexto y diagramas de sintaxis

3.3 Árboles de análisis gramatical 3.6 Propiedades formales de los y árboles sintácticos lenguajes libres de contexto abstractos 3.7 Sintaxis del lenguaje TlNY

El análisis gramatical es la tarea de determinar la sintaxis, o estructura, de un programa. Por esta razón también se le conoce como análisis sintáctico. La sintaxis de un lenguaje de programación por lo regular se determina mediante las reglas gramaticales de una gramática libre de contexto, de manera similar como se determina mediante expre- siones regulares la estructura Iéxica de los tokens reconocida por el analizador léxico. En realidad, una gramática libre de contexto utiliza convenciones para nombrar y operaciones muy similares a las correspondientes en las expresiones regulares. Con la única diferencia de que las reglas de una gramática libre de contexto son recursivas. Por ejemplo. la es- tructura de una sentencia if debe permitir en general que otras sentencias if estén anidadas en ella, lo que no se permite en las expresiones regulares. Este cambio aparentemente elemental para el poder de la representación tiene enormes consecuencias. La clase de estructuras reconocible por las gramáticas libres de contexto se incrementa de manera importante en relación con las de las expresiones regulares. Los algoritmos empleados para reconocer estas estructuras también difieren mucho de los algoritmos de análisis léxico, ya que deben utilizar llamadas recursivas o una pila de análisis sintáctico explícitamente admi- nistrada. Las estructuras de datos utilizadas para representar la estructura sintáctica de un lenguaje ahora también deben ser recursivas en lugar de lineales (como lo son para lexemas y tokens). La estructura básica empleadaes por lo regular alguna clase de árbol, que se conoce como árbot de análisis gramatical o árbol sintáctico.

Como en el capitulo anterior, necesitamos estudiar la teoria de las gramáticas libres de contexto antes de abordar los algoritmos de análisis sintáctico y los detalles de los analizadores sintácticos reales que utilizan estos algoritmos. Sin embargo. al contrario de lo que sucede con los analizadores léxicos, donde sólo existe, en esencia un método algorítmico (representado por los autómatas finitos), el análisis sintáctico involucra el tener que elegir entre varios metodos diferentes, cada uno de los cuales tiene distintas - propiedades y capacidades. De hecho exlsten dos categorías generales de algoritrnos: de análisis sintáctico descendente y de análisis sintáctico ascendente (por la manera

Page 100: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

en que construyen el árbol de análisis gramatical o árbol sintáctico). Pospondremos un análisis detallado de estos métodos de análisis sintáctico para capítulos subsiguientes. En este capitulo describiremos de manera general el proceso de análisis sintáctico y poste- riormente estudiaremos la teoría básica de las gramáticas libres de contexto. En la sección final proporcionaremos la sintaxis del lenguaje TlNY en términos de una gramática libre de contexto. El lector familiarizado con la teoría de las gramáticas libres de contexto y los árboles sintácticos puede omitir las secciones medias de este capitulo (o utilizarlas como repaso).

EL PROCESO DEL ANALISIS SINTÁCTICO La tarea del analizador sintáctico es determinar la estructura sintáctica de un programa a par- tir de los tokens producidos por el analizador léxico y, ya sea de niaiiera explícita o implí- cita, constniir un árbol de análisis gramatical o árbol sintáctico que represente esta cstruc- tura. De este modo, se puede ver el analizador sintáctico como una función que toma como su entrada la secuencia de tokens producidos por el analizador Iéxico y que produce como su salida el &bol sintáctico

analiz;idor sint6ctico secuencia de tnkens , &bol sinrticrico

La secuencia de tokens por lo regular no es un parámetro de entrada explícito, pero el ana- lizador siritictico llama a un procedimiento del analizador léxico, como g e t T o k e n , para obtener el siguiente token desde la entrada a medida que lo necesite durante el proceso de aiiiílisis sintictico. De este modo, la etapa de aiialisis sintictico del compilador se reduce a una llarnada al analizador Léxico de la manera siguiente:

En un compilador de una sola pasada el analizador sintáctico incorporará todas las otras fases de un compilador, incluyendo la generación del código, y no es necesario construir uingún árbol sintáctico explícito (las mismas etapas del analizador sintáctico representarán de manera implícita al árbol sintáctico), y. por consiguiente, una llamada

lo hará. Por lo común, un compilador será de inúltiples pasadas, en cuyo caso las pasadas adicionales utilizaran el irbol sintáctico como su entrada.

La estructura del árbol sintáctico depende en gran medida de la estructura sintáctica particular del lenguaje. Este árbol por lo regular se define como una estructura de datos di- námica, en la cual cada nodo se compone de un registro cuyos campos incluyen los atrihu- tos necesarios para el resto del proceso de compilación (es decir, no sólo por aquellos que calcula el analizador sintáctico). A menudo la estructura de nodos será un registro variante para ahorrar espacio. Los campos de atributo también pueden ser estructuras que se asignen de tilanera dinámica a medida que se necesite, como una herramienta adicional para ahorrür espacio.

Un problema más difícil de resolver para el analizador sintríctieo que para el ;inalizad«r Iéxico es el tratamiento de los errores. En el analizador léxico, si hay un carácter que no pue- de ser parte de u n tokeii legal, cntorices es suficientemente siniple generar un token de error y consumir el carácter problemático. (En cierto sentido. al generar un roken tle error, el ana- li~ador Iéxico transfiere la dificultad hacia c1 analizador sintictic».) Por otra parte, el :uiali- /iidor 4niictico no sólo debe mostrar irii mensaje de error, sino que debe recuperarse tiel error y coniinti;tr el ;inálisis sinticrico (para encontrar tnntos errores conlo sea posible). En

Page 101: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Gramatctas Itbres de contexto 97

ocasioiies. un ~ rna l i~a i l v r sintácbm puede el;.ctuor reparación de errores, e11 la cual i~il'ie- re ii i ia posible versiíiii de código corregida a partir de lo versión incorrecta que se le 11;iyn prcseittado. (Esto por lo regular se hace sólo eri casos sirriples.) lJn :tspccr« piirticulrir~nerite iiiiportante de la recuperación de errores es LU exhihici6n de ii iei~sajcs de t.rrOrcs signif i- cativos y la reaoudncicíii del análisis siiitáctico tan próxitrio a l error real conlo se:, posible. Esto no es kíeil. puesto que el iinalizador sintrictico piictle rio ilescnbrir uri error sitio h : ~ r ~irricho dcspu6s tic que el error reir1 haya ociirritlo. Conict l;is téciiictis de recupcracitin de errores ilcperideri del algoritnio de ciiirilisis sintáctico que se tinyí~ utilir;alo eii ~iarticul;ir. se q~ lazará su estiidio Insta capítulos sirhsiguientes.

3.2 GRAMÁTICAS LIBRES DE C O N T E X T O Uiia grani i t ica libre de coiltexto es una especificacióri para La estructnr;~ sint;ii~tica de ~ i r i lenguaje de programaci6n. l i r ia cspecil'icación así es muy sirnilar a la especificacióri de la estritcctira léxica de un lenguaje utilizniido expresiones regulares, excepto que irm yrariiitica libre de coritexto i i i vo luc r~ reglas de recursividad. Como ejemplo de ~jecnciór i utilizarertios expresiones aritméticas siinples de enteros con operaciones (le stimi, resta y rnultiplicaciiíii. Estas eupresioties se puedeii dar mediante la gr;iriiática siguiente:

32.1 Comparación respecto a la notación de una expresión regular

Considerenios cónio se cornpara la gramática libre de contexto (le muestra anterior con las reglas de la expresióii regular dada por número del capítulo anterior:

número = d i g i t o d í g i to*

d i g i t o = 0111213141516171819

t n las reglas de la expresión regular bisica terieiiios tres operaciones: seleccióii (tlatla por el rnetasíinbolo de 13 barra veitical), concatenación (dada sin un iiietasíirtbolo) y repeticiiín (dada por e l inetasínibolo del asterisco). También criipleanios el signo de igualdad para re- presentar la defi t~ición de un nombre para una cxpresióii regular, y escrihiiiios e l n«iiibre en itálicas para d is t ingu i r l~ de una secuencia de cmacteres rcales.

Las reglas gramaticales util izan notaciones scnie,j;intes. Los noinbres se escriben en c~irsivas o itálicas (pero ahora con una fuente diferente, de modo que podamos distinguirlas de los nombres para l:ts expresiones regulares). L a barra vertic:rl todavía apdrece coiiio e l rnetasímbolo para selección. L a concatenación tarnhién se utiliza corno operación estándrir. Sin embargo, no hay ningún inetasínibolo para la repetición (co i~ io e l * de las expresioiies regulares), rin punto al tual regresarentos en hieve. Una diferencia adicional en la riotnci6n es que ahora util i iainos el síiiibolo de la flecha -. en lugar del de igualtlad para expresai- las definiciones de los iio~nbres. Esto se tlche :i que ahora los noinbre\ no p~ietlen sinipletiiente ser reeriiplamdos por srrs tlefitiiciorics, porque está irilplicatlo un proceso de tlefinicióti riiás

1 coniplejo, conio resultado de la ii~rturuleza recursiva de las definiciones. Ei i nuestro ejem- plo, la regla para erp es rccrirsiv;i. cri el scrititlo qric el iiciirrbrr c ' t p npxece ;I la dercclin de la flecha.

;Idvierta t ; t inhih que las reglas grmst icales u t i l imn i.xprcsitrtii.s reyl;ii-es conio coti~ponentcs. l in las rcgliih p r : i c v p y O/J se ticncri eri I-c;ilitlail seis expr-csioric\ regul;ri-es

Page 102: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 3 1 GRAMATICAS LIBRES DE CONTEXTO Y ANALISIS SINT~UICO

representando tokens en el lenguaje. Cinco de éstas son tokens de carácter simple: +, -. *, ( y ) . Una es el nombre número, el nombre de un token representando secuencias de dígitos.

De ntanera similar a la de este ejemplo, las reglas gramaticdes se usaron por primera vez en la descripción del lenguaje AlgolóO. La notación fue desarrollada por J«hn Backus y adaptada por Peter Naur para el informe AlgoL60. De este modo. generalmente se dice que las reglas gramaticales en esta forma están en la forma Backus-Naur, o BNF (Backus- Naur Form).

3.2.2 Especificación de las reglas de una gramática libre de contexto Como las expresiones regulares, las reglas gramaticales estiín definidas sobre un alfabeto, o conjunto de símbolos. En el caso de expresiones regulares, estos símbolos por lo regular son caracteres. En el caso de reglas grainaticales, los símbolos son generalmente tokens que repre- sentan cadenas de caracteres. En el último capítulo definimos los tokens en un analizador léxico utilizando un tipo enumerado en C. En este capítulo, para evitar entrar en detalles de la manera en que los tokens se representan en un lenguaje de impleinentación específica (como C), emplearemos las expresiones regulares en sí mismas para representar los tokens. En el caso en que un token sea un sínrbolo fijo, como en la palabra reservada wh i l e o los símbolos especiales como + o :=, escribiremos la cadena en sí en la fuente de código que se utilizó en el capítulo 2. En el caso de tokens tales como identificadores y números, que representan más de una cadena, utilizaremos la fuente de código en itálicas, justo como si el t6ken fuera un nombre para una expresión regular (lo que por lo regular representa). Por ejemplo, representaremos el alfabeto de tokens para el lenguaje TINY como el conjunto

en lugar del conjituto de tokens (como \e definieron en el analizador léxico de TINY):

Dado un alfabeto, una regla gramatical libre de contexto en BNF se compone de tina cadena de símbolos. El primer símbolo es un mntbre para una estructura. El segundo símbo- lo es el metasímbolo "+". Este sínibolo está seguido por una cadena de símbolos, cada uno de los cuales es un símbolo del alfabeto, un nombre para una estmctura o el metasímbolo "1".

En términos informales, tina regla grmatical en BNF se interpreta como sigue. La re- gla define la estructura cuyo nombre está a la izquierda de la flecha. Se define la estructura de manera que incluya una de las selecciones en el lado derecho separada por las barras ver- ticales. Las secuencias de símbolos y nombres de estructura dentro de cada selección defi- nen el diseño de la estructura. Por ejemplo, considere las reglas gramaticales de nuestro ejemplo anterior:

exp -. exp o p exp / ( exp ) / número o p + t 1 - l *

La primera regla define una estructura de expresión (con nombre e . y ) compuesta por una expresión seguida por un operador y otra expresión, por una expresión dentro de paréntesis, o bien por un número. La segunda tlefine nti opcrador (con nornhrc (y)) compum"t) irle uno de los síriibol«s t , - o *.

Los mctasínih~tlos y convenciones que wiinos aquí son semejanres a los di' uso geiie- rnli~:ido. pero es ncccsai-io :idvtrtir que no hay un estindar universal pc11.a estas conwnciones, F h realidad. las altcrnativrts wnirines 1~1t.n cl tnciasímholo iIe la flecha "4" incluyen " =" (cl

Page 103: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Gramatftas libres de ionrexto 99

sigrio de igualdtid), '.:" (el signo de dos puiitos) y "::=" (signo de dos puntos doble y e1 de igualdad). En archivos de texto normales imh ién es necesario hallar un rcerriplam para el uso de las itiilicas. Esto se hace í'reciientenietite encerrando los notiihres de estructura con "picoparéritesis" < . . . > y escribiendo los nombres de token en itálicas ~.oi i letras rnayúscu- las. De cste niodo. con diferentes coiivenciones, las reglas gramaticales anteriores podría11 aparecer coino

Cada autor también tendrá otras variaciones en estas notaciones. Varias de las inás iniportaii- les (:ilguiias de las c~tales utilizareinos ocasionaltnente) se analizaran t i ~ á s adelante en esta sección. Siti etnhargo, vale la pena cirralizar de innie~liato dos pequeñas cuestiones adiciona- les sobre la notación.

En ocasiones, ;iunque los paréntesis son íttiles para reasignar la precedencia cn Itis expre- siones regulares, conviene incluir paréntesis en los nietasínibolos de la notación BNF. Por ejemplo, se puede volver :i escribir las rcglas gramaticales anteriores coriio una sola regla gramatical de la rnanera siguiente:

e.xp + e.¿p ("+" / "-" / "*") czp / " (" e.rp " ) " / número

En esta regla los paré~itesis son necesarios para tigrupar las opciones de los operiidores en- tre las expresiones en el lado derecho, pucsto que la concatcnacitin tiene precedencia sobre la selección (con10 en las expresiones regulares). De este modo, la regla siguiente tendría un significado diferente (c incorrecto):

e r p i exp "+" 1 "-" / "*" exp 1 "("erp " ) " 1 número

Advierta tariibién que, cuando se iiicloyen los paréntesis cotno rnctasínih»lo, es necesario distinguir los tokens de paréntesis de los riietüsíii~holos. lo que hicimos poniéndolos entre comillas, como hacíamos en el caso de expresiones regulares. (Para mantener la consisten- cia también encerramos los síinbolos de operador entre comillas.)

Los paréntesis no son absolutttinente necesarios coino nietasímbolos en BNF. ya que siempre es posible separar las partes entre p&ntesis en una nueva regla grainatical. De hecho, la operación de selección que da el rnetasímbolo de la barra vertical tampoco es necesaria en reglas gramaticales, s i perniitinios que el mismo nombre aparezca cualquier número de veces a la izquierda de la flecha. Por ejemplo, nuestra gramática de expresión simple podría escribirse corno se aprecia a continuaci6r1:

Page 104: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 3 / GRAMATICAS LIBRES DE CONTEXTO Y ANÁLISIS ~INTACTICO

En ocasiones, por siinplicidad, &renios ejemplos de reglas gramaticales en una notacicín abreviada. En estos casos utilizaremos letras en mayúsculas para nombres de estructura y le- tras en minúsculas para símbolos de token individuales (que con frecuencia son sólo carac- teres simples). De este modo, nuestra gramática de expresión simple podría escribirse en es- ta f«mia abreviada de la manera que sigue

En ocasiones también sirnplificürernos la notación cuando estemos utilizando solamente ca- racteres como tokens y los estemos escribiendo sin utilizar una fuente de código:

3.2.3 Derivaciones y el lenguaje definido por una gramática Ahora volvamos a la descripción de cómo las reglas gramaticales determin~n un "lengua- je", o conjunto de caden~s legales de tokens.

Las reglas gramaticales libres de contexto determinan el conjunto de cadenas sintáctica- mente legales de símbolos de token pwa las estructuras definidas por las reglas. Por ejem- plo, la expresión aritmética

corre5ponde a la cadena legal de tiete tokens

( número - número ) * número

donde los tokens de número tienen sus estructuras determinadas por el analizador léxico y la cadena misma es legalmente una expresión porque cada parte corresponde a selecciones tleterminadas por las reglas gramaticales

exp 4 exp op rxp / ( exp ) / número o p + + l - l *

Por otra parte, la cadena

(34-3*42

no e i una expresión legal, porque se tiene un paréntesis izquierdo que no tiene TU correipon- diente paréntesis derecho y la segunda selección en la regla gramatical para una expreTión exp requiere que los paréntesis se generen enpares.

Las reglas gramaticales determinan las cadenas legales de sínlbolos de token por medio de derivaciones. Una derivación es una secuencia de reemplazos de nombres de estructura por selecciones en los lados tlerechos de las reglas gramaticales. Una derivaciún comienza con un nombre de estructura simple y termina con una cadena i e símbolos de token. En ca- &a etapa de una derivación se hace un reemplazo simple utilizando una selección de una re- zla grarnalical. "

Page 105: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Gramáticas l~bres de contexto 101

Como ejemplo, 1;i figura 3.1 proporciona una deriviiciiin para la eupresi611 ( 34-3 ) *42 utilizando las reglas grainatic:iles como ?e dan en nitestra expresióii gramatical sirtiple. En cada paso se proporciona a la dereclia la sclecci6n dc la i-eglzi gt-amatic;il u t i l imki para el 1-cemplaro. (T;imbién numer:inios los pasos para unir refereiicia ~~ostcririr.)

Una derivatian para la ( 2 ) * exp o() número expresión aritmética

(3) * q r * número ( 3 4 - 3 ) * 4 2

(4) =1 ( uxp ) * número

[ r i p -+ f7 i p op vi1

/erp -i número]

[op -+ * 1

[u\[) -+ ( o t p ) l

(6) * ( e i p op número) * número [erp - número]

(7) * ((erp - número) * número [op -+ - 1

(81 =+ (numero - número) *número [ekp -, número]

Advierta que los pasos de derivación utilizan una flecha diferente al rnetasítiibolo de flecha que se emplea en las reglas gramaticales. Esto se debe a que existe una diferencia entre un paso de derivacih y una regla gramatical: las reglas gramaticales definen, mientras que los psosúe derivación construyen mediante reemplazo. En el primer paso cie la figura 3.1, la r x p simple se reemplaza por la cadena rwp o p e . ~ p del lado derecho de la regla e.xp z;p -ter/? op exp (la prirnerti selecci6n en la BNF para rxp). En el segurido paso, la e.xp del extremo derecho en la cadena eup o p u p se reemplaza por el símbolo número del lado derecho de la selecci6n urp -+ número para obtener la cadena e.xp o p número. En ese paso el op (operador) se reemplaza por el sÍmh«lO * del lado derecho de la regla o[? -+ * (la tercera de las selecciones en la BNF para o p ) para obtener la cadena evp * número. Y así suce- sivamente.

E l conjunto de todas las cadenas de símbolos de token obtenido por derivaciones del símbolo exp es el lenguaje definido por la gramática de expresiones. Este lenguaje coiitie- ne todas las expresiones sintácticamente legales. Podernos escribir esto de niaiiera siiilb6lica como

donde C representa la gritmática de expresión, s representa iina cadena arbitraria de símbolos de token (en ocasiories dentminada sentencia), y los símbolos ** representan una deriva- ci6» compuesta de una secuencia de reemplazos como se describieron anteriormetite. (El asterisco se utiliza para indicar una secuencia de pasos, así conlo para indicar repeiici6n en expresiones regulares.) Las reglas gramaticales en ocasiones se conoceit corno produccio- nes porque "producen" las cadenas en LfG) niediaiite derivaciones.

Cada nombre de estructura en una gramiítica define su propio lenguaje de cadenas sin- ~ácticamente legaies de tokciis. Por ejemplo, el lenguaje definido por op en nuestra griimií- rica de expresión simple define el Icrigu:rje ( c . -, * J compuesto sólo tle tres sirriholns. Por lo regular estamos mis interesiidos en el lenguaje definido por la cstriictrira mis general en una gramática. La grarnitica pra un 1engu:tje de progromacicín ii menudo define una cstruc- tiira denominada pro,?rtrrttci, y el Icrigu;?je de esta estructura cs el ccrtrjunto tle todos los pro- gr:trnas sintácticamente lefalcs tíel lenguaje llc prog~iniitci6ri (atlvicrt;~ que ;~quí utili~;irntrs la ]>alahra "lenguaje" c ~ i dos witidric dilerente.;).

Page 106: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 3 1 GRANATICAS LIBRES DE CONTEXTO Y ANALISIS SINT~CTICO

Por ejemplo, un RNF para Pascal con ien~~rá con reglas graniat~cales tales como

programa -+ encabezarlo-pro,qr(lmu ; bloque-progrurna . encubezudo-programa -+ . . . bloque-progrumu -+ . . . . . .

(La primera regla dice que un programa se compone de un encabezado de programa, segui- do por un signo de punto y coma, seguido por un bloque de programa, seguido por un pun- to.) En lenguajes con compilación separada, como C, la estructura más general se conoce a menudo como una uniúud de compilacidn. En todo caso, suponemos que la estructura más general se menciona primero en las reglas gramaticales, a menos que lo especifiquenios de otra manera. (En La teoría matemática de las gramáticas libres de contexto esta estructura se denomina símbolo inicial.)

Otro segmento de terminología nos permite distinguir con más claridad entre los nom- bres de estructura y los símbolos del alfabeto (los cuales hemos estado llamando símbolos de token, porque por lo regular son tokens en aplicaciones de compilador). Los nombres de estructuras también se conocen como no terminales, porque siempre se debe reemplazar de más en una derivación (no terminan una derivación). En contraste, los símbolos en el al- fabeto se denominan tenninaies, porque éstos terminan una derivación. Como los temiinales por lo regular son tokens en aplicaciones de compilador, utilizarán ambos nombres, los que, en esencia, son sinónimos. A menudo, se hace referencia tanto a los terminales corno a los no terminales como símbolos.

Consideremos ahora algunos ejemplos de lenguajes generados por gramáticas.

Ejemplo 3.1 Considere la gramática G con la regla gramatical simple

Esta gramática tiene un no terminal E y tres terminales (,) y u. Genera, además, el lenguaje L(G) = (u,(u), ((a)), (((a))), . . .) = ((" u)" I n un entero > O ] , es decir, las cadenas compues- tas de O o más paréntesis izquierdos, seguidos por una a, seguida por el mismo número de paréntesis derechos que de paréntesis izquierdos. Como ejemplo de una derivación para una de estas cadenas ofrecemos una derivación para ((a)):

Ejemplo 3.2 Condere la gramática G con la regla gramatical iimple

Esta resulta ser igual que la gramática del ejemplo anterior, sólo que se perdió la opción E (1. Esta gramática no genera ninguna cadena, de modo que su lenguaje es vacío: L(G) = { ). La causa es que cualquier derivación que comienza con E genera cadenas que siempre contienen tina E. Así, no hay manera de que podanios derivar una cadena conipues- tn s d o de tcrtninales. En realidad, corno ocurre con todos los prtrccsos recursivos (como las iler~iostr:icioiies por intlucción o las Suncioncs recursivas). una regla grarn;itic:il que define

Page 107: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Gramaticas libres de contexto 1 03

Ejemplo 3.3

Ejemplo 3.4

trnit cstrnctura de iriunera recursiva sienipre debe tener por lo inenos un caso no recursivo (al que podríamos tlenoriiiiiar caso base). La regla grainatical de este ejemplo rio lo tiene, y cciiilqnier tleriv:ición potencial cst i condenada a tina recursiítn infinita. 9

Consicfere la graniitica (? con la regla grantatical simple

Esta grainática genera todas las catlenas compuestas de tr separarlas por signos de "tnis" ( t ):

Para ver esto (de nianera informal), considere el efecto de la regla E -+ E -t (1: esto provo- ca que In cadena -t (1 se repita sobre la derecha en una derivación:

Finalmente, dehemos reemplazar la E a la izquierda utilizando el criso base E -+ a. Podernos demostrtir esto r i i i s fortiialniente mediante inducci6n corno sigue. En primer

lugar, mostrainos que toda cadena (1 t ii i- ... + n está en L(G) por inducción en el núrne- ro de IUS a . LU derivación E * íi muestra que (1 está en Lfí;): suponga ahora que s = 11 + r r + ... + (1, con n -- I (1, está en L,(G). De este modo, existe una derivación E ** .S: ahora la derivación E =+- E -E ri- tr* s + (1 muesli'a que la cadena s + u, con n + a, esti en L(G). A la inversa, tamhién mostrantos que cualquier cadena .S de L(G) debe ser de la forma a t ir t ... + (1. Mostrainos esto mediante intlucción sobre la longitud de una derivación. Si In derivación tiene longitud 1, entonces es de la hrrna E * 11, así que .S es de la forrna correc- la. Ahora. suponga la veracidad de la hipótesis para todas Ias cadenas con derivaciones de Iongitiid 11 - 1, y sea E ** s tina deriv~ci6n de loiigitud n > l. Esta tierivnciítn debe co- menzar con el reemplazo de E por E + ti, y de esta manera será de la forma E =1 E t t z =* .Y ' + a = s. Entonces, S' tiene una clerivación de longitud n - 1, y de este n i d o será de la

Iorrnatr + n + ... + rr. Por lo tanto, .S misma debe tener esta misma forma. 5

Considere In siguiente gramática muy sirnplificada de sentencias:

senrenc icr -+ seni-if / otro .srnt-$-+ if ( erp ) .sentenrict

/ if ( e.rp ) .senrencia else sentencia etp O j 1

El Icnguaje de esta gramática se compone de sentencias i f anidadas de nianera semejante iil Icirg~iqje C. (Siniplificatnos Iiis expresiones de prueba lógica ya sea ;i O o a 1 , y iigrupatrios Lod:is I:LS otras scntcrici;~.; aparte cle las scntcncins if en cl tcrininal otro.) Ejemplos tic cadenas cn cstc Icngui~je soii

o t r o

if (O) o t r o

if (1) o t r o

Page 108: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 3 1 GRAMATICAS LIBRES DE (ONTEKIO Y ANALISIS 1lflTbCTlC0

if ( O ) o tro else otro

if (1) otro else otro

i f ( O ) if ( O ) otro

if ( O ) if (1) otro else otro

i f (1) otro else if ( O ) o tro e lse otro

Advierta cómo la parte else opcional de la sentencia if está indicada por medio de una sc- lección separada en la regla gramatical para smt-$ 5

Antes advertimos que se conservan las reglas gramaticales en BNF para concatenación y selección, pero no tienen equivalente especíllco de la operación de repetición para el * de las expresiones regulares. Una operación así de hecho es innecesaria, puesto que la repericióti se puede obtener por medio de recursión (como los programadores en lenguajes funciorta- les lo saben). Por ejemplo, tanto la regla gramatical

corno la regla gramatical

generan el lenguaje {u" I n un entero Z I ) (el conjunto de todas las cadenas con una » más u), el cual es igual a1 que genera la expresidn regula a+. Por ejemplo, la cadena untu pue- de ser generada por la primera regla gramatical con la derivación

Una derjvacidn similar funciona para la segunda regla gramatical. La primera de estas re- glas gramaticales es recursiva por la izquierda, porque el noterminal /l. aparece como el primer símbolo en el lado derecho cie la -&la que define A.' La segunda regla gramatical es recursiva por la derecha.

El ejemplo 3.3 es otro ejemplo de una regla gramatical recursiva por la izquierda, que ocasiona la repetición de la cadena "+ o". Éste y el ejemplo anterior se puede geueralizar como sigue. Considere una regla de la h n i a

donde a y p representan cadenas arbitrarias y P no comienza con A. Esta regla genera todas las cadenas de la forma p, Bol, paa, paaa, . . . (todas las cadenas comienzan con una p, seguida por O o nlás a). De este niodo. esta regla gramatical es equivalente en su efecto a la expresitín regular Bol". De manera similar, la regla gramatical recursiva por la derecha

Page 109: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Grambticas lhbrer de contexto 105

Ejemplo 3.5

Si queremos escribir inia grariiitica que genere el mimo lenguaje qtie la expresih regu- lar a*, entonces debemos Lener una ~iotación para una rcgla gramatical que genere la catle- ila vacía ipwque la expresii6~egular a* incluye la cadena vacía). Una rcgla gramatical así tlebe tener un Iado derecho \ ;icío. Poilernos sirnplernente iio escribir nada en el lado <lereclio, con10 ocurre en

pero eniplearemos más a riienrtd« el rnetasímholo épsilon para la cadena vacía (corno se uscí en las expresioiies regulares):

Una regla grarriatical de esva clase se conoce como producci6n E (una "protiuccii>n épsi- Ion"). tina gramática que genera u n lenguaje que contenga la cadena vacía dehe tener por lo menos una proclt~ccií,n e.

Ahora podemos escribir tina grariiitica que sea equivalente a la expresión regular a*, ya sea como

Ambas gramaticas generan el lenguaje ((1" I 1 1 u n entero 2 O ] = Lía*). Las pr«(iuccictnes E

también sori útiles :i1 definir cstntcturas que son opcionales, c«mo pronto verernos. Concluiremos esta s~ibseccitín con algurm otros <jcn~plos mis.

Considere la gramitica

Esta gramática genera las c.adenas de todos los "paréntesis balancettttos". Por e,jernplo, la ca- dena (( )(( )))( ) es genertiila por la derivaci6n siguierite (la prnducci6n 8 se utiliza para ha- cer qiie desaparezca A cu:indo sea necesario):

Ejemplo 3.6 La grarnitica de sentencia del ejemplo 3.4 se puede escribir de la siguiente rn;incra alterna- iiva iitilimido una producciiin E:

Page 110: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

1 O6

Ejemplo 3.7

CAP. 3 1 GRAMÁTICAS LIBRES DE CONTEXTO Y ANAUSIS IINTACIICO

Considere la siguiente gramática C; para una secuencia de sentencias:

Esta grriinática genera secuencias de una o más sentencias separadas por signos de plinto y conia (las sentencias se extrajeron del terminal simple S):

Si queremos permitir que las secuencias de sentencia también sean vacías, podríamos escrihir la siguiente gramática G":

secuencia-sent -+ serit ; secnenciu-sent / 8

srnt -+ s

pero esto coiivierte al aigno de punto y coma en un terminador de sentencia en vez de en un separador de sentencia:

Si deseamos perntitir que las secuencias de sentencia sean vacías, pero también mantener el signo de punto y coma como un separador de sentencia, debemos escribir la graniática como se muestra a continuación:

Este ejemplo muestra que debe tenerse cuidado en la ubicación de la producción 8 cuando se construyan estructuras opcionales. $

1.3 ÁRBOLES DE ANÁLISIS GRAMATICAL Y ARBOLES SINTACTICOS ABSTRACTOS

331 Árboles de análisis gramatical Una derivación proporciona un método para construir una cadena particular de terminales a pxtir de un no terminal inicial. Pero las derivaciones no sólo representan la cstructiira de Las cailerias que construyen. En generd, existen muchas derivaciones püra la misma cadena. Por ejemplo, constmyantos la cadena de tokeiis

( número - número ) * número

ii partir de nuestra gramática de euprcsih simple utilimido la rlcrivación de la figura 3.1. llrru wgtinth ilerivacií,ii para d a caderia se proporciona cit la figiirn 3.2. La iinica cfifereiicia

Page 111: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

~,.: , , i<

Árboles de análisis gramatical y arboles sintácticos abstractos

~ i g u r n 3 1 Otra derivacion para la expreston < 3 4 - 3 ) * A 2

entre las (los dcrivaci<~iies es el orden en el cual se surriiriistrün los reeiiiplazos, y 6st;t es de hecho una dilerencia superficid. Püra aclarar esto necesitamos una reytrcscntacióri pa- ra 1;i estructura de tina cadena de terminales que abstraiga las carücterí&xs escriciales tle una <leriv;ición niieittras se factorizan las dikrenci;rs skipcrficinlcs de orden. L a repre- sentación que hace e.;t« es i i i ~ a estriicturli de irhol, y se conoce coino árbol de m í l i s i s grí- ~natici i l .

( O ) -=, (número - número) op a p l r tp -+ número]

(7) =, (número - número) * r r p [or, -. * ] (8) * (número - número) * número [e tp -+ número]

l l n Brhol de análisis gramatical correspoiictiei~le 21 una deriviiciiíri es un árhd etique- tado en el cual los nodos interiores esti i i etiquetados por t i « teriiiinales, los nodos hoja es- tán etiquetxkos por teriiiiiiales y los hijos (le cada ticiclo interno represent:rti el ree~i ip lü io del no terriiirial asociado c'n un paso de la deriv;ición.

Parü &ir un ejemplo sinrple, la decivüción

corresponde al lirbtil de análisis grrmatical

número + número

Page 112: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 1 I GRAMATICAS LIBRES DE CONTfKlO Y ANALISIS IIHTÁCIICO

1 I l número + número

Advieria que esta numeración de los nodos internos del árbol de análisis gramatical es en realidad una numeración preorden.

Este mismo árbol de análisis gramatical también corresponde a las derivaciones

exp d exp op exp -=, exp o p número d exp + número =, número + número

exp d exp o p exp * exp + exp a número + exp * número + número

pero se aplicarían diferentes numeraciones de los nodos internos. En realidad la primera de estas dos derivaciones corresponde a la numeración siguiente:

número t número

(Dejamos al lector construir la numeración de la otra.) En este caso la numeración es la in- versa de una numeración postorden de los nodos internos del árbol de análisis gramatical. (Un recorrido postorden visitada los nodos internos en el orden 4, 3, 2, 1 .)

Un árbol de análisis gramatical corresponde en general a muchas deriv:~ciones, que en conjunto representan la niisrna estructura básica para la cadena de terminales analizada gra- inaticalrnente. Sin emhar_go, se pueden distinguir derivaciones particul:ires que están asocia- das de manera única con el árbol de ;uiálisis gramatical. Una derivación por la izquierda cs acluclla cn la cual se reemplaza cl no termiiral niás a la izquicrcki en cada p;rso cn la deriva-

Page 113: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Árboles de analtris gramat~cal y árboles rintácticos abstrattor 1 09

cióri. Por consiguiente, una derivaciún por la derecha es aquella en la cual el no terniinal más a la derecha se reemplaza en cada paso de la derivación. Una derivación por la izquier- da corresponde a la numeración preorden de los nodos internos de su árbol de análisis gra- matical asociado, mientras que una derivación por la derecha corresponde a una numeración postorden en reversa.

En realidad, vimos esta correspondencia en las tres derivaciones y el irbol de análisis gramatical del ejemplo más reciente. La primera de las tres derivaciones que dimos es una derivación por la izquierda, mientras que la segunda es una derivación por la derecha. (La tercera derivación no es por la izquierda ni por la derecha.)

Como un ejemplo más complejo de un árbol de análisis gramatical y de las derivacio- nes por la izquierda y por la derecha, regresemos a la expresión ( 3 4 - 3 ) *42 y las deriva- ciones que dimos en las figuras 3.1 y 3.2. El árbol de análisis gramatical para esta expresión está dado en la figura 3.3, donde también nunieramos los nodos de acuerdo con la deriva- ción de la figura 3.1. Ésta es de hecho una derivación por la derecha, y la numeración corres- pondiente del árbol de anlílisis gramatical es una numerüción postorden inversa. La derivación de la figura 3.2, por otra parte, es una derivación por la izquierda. (Invitamos al lector a pro- poreionar una nuineraci6n preorden del árbol de análisis gramatical conespondiente a esta derivación.)

Figura 3.3 Arbal de análisis gramatical para la expresión aritmética ( 3 4 - 3 ) * 4 2

número - número

3.3.2 Arboles sintácticos abstractos Un árbol de análisis gramatical es una representación útil de la estructura de una cadena de tokens, ya que los tokens aparecen conlo las hojas del arbol de análisis gramatical (de izquier- da a derecha) y los nodos internos del árbol de anáiisis gramatical representan los pasos en una derivación (en algún orden). Sin embargo, un árbol de análisis gramatical contiene mucha información más de la que es absolutamente necesaria para que un coinpilador produzca código ejecutable. Para ver esto, considere el árbol de análisis granlatical para la expresión 3+4 de acuerdo con nuestra gramática de expresión siitiple:

Page 114: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 1 1 GRAMATICAS LIBRES DE CONTEXTO Y ANÁLISIS IINTACIICO

Éste es el árbol de análisis gramatical de un ejemplo anterior. Aunientamos el árbol para mostrar el valor numérico real de cada uno de los tokens número (éste es un atributo del token que se calcula mediante el analhdor léxico o por medio del analizador granlatical). El principio de la traducción dirigida por sintaxis establece que el significado, o senián- tica, de la cadena 3 + 4 deberka relacionarse directamente con su estrticlura sintáctica como se representa mediante el árbol de análisis gramatical. En este caso el principio de la traduc- ción dirigida por inedio de sintaxis significa que el árbol de análisis gramatical debería pre- suponer que el valor 3 y el valor 4 se van a sumar. En realidad, podemos ver que el árbol da a entender esto de la siguiente manera. La raíz representa la operación de suma de los vdo- rks de los dos subirboles e,tp hijos. Cada uno de estos subárboles, por otro lado, representa el valor de su hijo número único. Sin embargo, existe una manera mucho más simple de representar esta misma información, a saber, mediante el hrbol

Aquí, el nodo raíz simplemente se etiqueta por la operación que representa, y los nodos hoja se etiquetan mediante sus valores (en lugar de los tokens número). De manera simi- lar, la expresión ( 3 4 - 3 ) *42 , cuyo árbol de análisis gramatical está dado en la figura 3.3, se puede representar en forma más simple por medio del árbol

En este árbol los tokens de parentesis en realidad han desaparecido, aunque todavía repre- sentan precisamente el contenido semántico de restar 3 de 34, para después multiplicarlo por 42.

Tales árboles representan abstracciones de las secuencias de token del código fuente real, y las secuencias de tokens no se pueden recobrar a partir de ellas (a diferencia de Los árboles de análisis gramatical). No obstante, contienen toda la información necesaria para traducir de una forma más eficiente que los árboles de análisis gramatical. Tales árboles se conocen como árboles sintiicticos abstractos, o para abreviar árboles sintácticos. Un anali- zador sintáctico recorrerá todos los pasos representados por un árbol de análisis gramatical, pero por lo regular sólo constmirz un árbol sintáctico abstracto (o su equivalente).

Los árboles sintácticos abstractos pueden imaginarse como una representación de irbol de una notación abreviada denominada sintaxis abstracta, del mismo modo que un árbol de análisis gramatical es una representación para la estructura de la sintaxis ordiriaria (que también se denomina sintaxis concreta cuando se le compara con la abstracta). Por qjemplo, la sintaxis abstracta para la expresión 3+4 debe escribirse coino OpE:.rp(Pll~s, Cl)ri.srExp(:3),Ci~tzstE.r~)~4)) y la sintaxis abstracta para la cxpresih ( 3 4 - 3 ) * 4 2 se puede cscribir como

Page 115: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Arbales de anál~ris gramat~tal y arboles smtactttor ~bstractos 1 1 1

Ejemplo 3.8

EII realidad. 13 hiniaxis abstracta ptiedc proporcioiiar una i l e f i i i i c i h lorti ial uti1iz;iirtli) una n o t a c i h scriie;aitte a la HNF. <le1 rnistiro tirudo que la i i i t ax i s concreta. Por ejc~i iplo, potlríaiiios escrihir las siguientes reglas tipo B N F para la sintaxis íibstract:~ tlc nuestras exprcsiories arittit6ticas hiniples cotirrt

N o investig;trelrios iiiás 31 respecto. Nuestro priricip:il interés r;idica en la estructura (le árbol sii it ictico 1-ea1 que utilirará el ;tn;iliz;idor hiiitáctico, La cii:tI se detei-minará por ~ i ic t l io de una declaración tle tipo de datos.' Por ejemplo, los árboles siiiticticos ahstnictos para r~ues t r ;~~ c ~ p r ~ s i o r i e s ar i t inét ic :~~ si~iiples pueden ser tletcr~nin;i<los ~iicdii inte Iss tlcclarncio- iics de tipo de tlnros cn C

typedef typedef typedef

I

}

typedef

enum {~lus,Minus, Times) OpKind; enum {OpKind,ConstKindl ExpKind; struct streenode ExpKind kind; OpKind op; struct streenode *lchild,*rchild; int val; STreeNode; STreeNode *SyntaxTree;

Advierta que enrpleamos tipos enuinerndos para las dos diferentes c1;ises de nodos de *bol sinliclico (opc~tciones y constantes ciitct.as), así coriio pnrn las operaciones (suiita. resta, irrultiplicaci6n) ri i isiria~. De hecho. prob;iblenientc risarí:iiiios los tokcns p i ra represcncar llts operaciones, m i s que definir un nuevo Lipo enunierado. TaitihiCn podríairios haber usíido u11 tipo union dc C p i ra nliorrar espacio. priesio que un nodo ito puede scr al rriis~rio ticrirpo un nodo de operador y un notlo de const;intc. Fiiialtiiente, observa~iios que estas trcs decla- rüciorres de nodus sólo incluyen los atributos clue son ilirectarricnte necesarios para este ejeriiplo. En situacioires prácticas habrá iiiuchos t i l is catripos para los atributos de tiempo de corripilnción, tales coino tipo de datos, infor~ii;icióii dc Iah l i~ de sínitmlos y :rsí iiccsiv;inicn- te. como se dqjar i claro con los ejeiiiplos que se piiiserrtíirán triás adelante en este cnpítulo y en eapít~los subsig~iientes.

C«ncloiirr«s esta sección con vx ios ejeiitplos de irboles de análisis gramatical y árho- les sintácticos utilizcindo gmiri6tic;ts que ya consideratrios en ejemplos :iiiteriores.

Corisidcre la gr;iiiiitica pnr:i las sentencias il siitiplii'icadas del ejemplo 3.3:

Page 116: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

El árbol de análisis gramatical para la cadena

if ( O ) otro else otro

es coino se presenta a continuación:

o otro otro

Al utilizar la gramática del ejemplo 3.6,

sentencia -t sent-if 1 otro sent-if -t if ( exp ) ~entencia parte-else parte-&e -t else sentencia / F

exp -t O / 1

esta misma cadena tiene el siguiente árbol de andisis gramatical.

otro

Un árbol sintáctico abstracto apropiado para las sentencias if desecharía todo, excepto las tres estructuras subordinadas de la sentencia if: la expresi6n de pnieba, la parte de acción (parte "then") y la parte alternativa (parte "else"). De este modo, un árbol sintáctico para la catlena anterior (usando la gramática del ejemplo 3.4 o la del ejemplo 3.6) sería:

o otro otro

Page 117: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Arboles de analir~s gramatical y arboles sintacticos abstractos 113

Aquí empleainos los ~okens rcslairtes if y otro como cticluetos para distinguir la clssc de1 noclo de senteilcia eii el i r bo l Siiitiictico. Esto se efectuaría inás apropiadan~ente ei~rpleantlo rili tipo enuirrer;do. Por ejciliplo. un conjunio de tieclaraciones en C' : r p r q h i o p;11.'1 la cs- i~mctiira de las seritcnciss y cxprcsiones en este cjerirplo sería coirio cI que se iiiueslra S Y CI)II- tintlaci6n:

Ejemplo 3.9

typedef enum {ExpK,StmtK} NodeKind;

typedef enum (Zero,One} ExpKind;

typedef enum (IfK,OtherK} StmtKind;

typedef struct streenode

{NodeKind kind;

ExpKind ekind;

StmtKind skind:

struct streenode

*test,*thenpart,*elsepart;

STreeNode;

typedef STreeNode * SyntaxTree;

Consiclere la gratniitica dc una secnencia de sentencias separadas por signos de pritito y ct~tna tiel ejempl» 3.7:

La cadeila S ; s; s ticrre e1 siguiente i rbo l de mí l is is gritniatical respecto ;I esta gramática:

Un posible árbol sintáctico para csta misma cadena es

Page 118: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

En este árbol los nodos de punto y coma son semejantes a los nodos de operador (como los nodos + de expresiones ~rritméticas), salvo por la diferencia de que éstos s6lo "funcionan" 31 unirse entre sí las sentencias en una secitencia. En su lugar podríamos intentar unir todos los nodos de sentencia en una secuencia con solamente un nodo, de modo que el árbol sin- táctico anterior se convertiría en

El problema con esto es que un nodo seq puede tener un número arbitrario de hijos y es di- fícil tomar precauciones al respecto en una declaración de tipo de datos. La soluciih es uti- lizar la representación eseándar con hijo de extrema izquierda y hermanos a la derecha para un árbol (esto se presenta en la mayoría de los textos sohre estructuras de datos). En esta representación el único vínculo físico desde el padre hasta sus hijos es hacia el hijo de la extrema izquierda. Los hijos entonces se vinculan entre sí de izquierda a derecha en tina lista de vínculos estándar, los que se denominan vínculos hermanos para distinguirlos de los vínculos padre-hijo. El árbol anterior ahora se convierte en el arreglo de hijo de extrema izquierda y herrnanos a la derecha:

Con este arreglo también se puede eliminar el nodo seq de c«nexi<ín, y ciitonces el árbol sintáctico se convierte sitriplemente en:

Ésta, corno es evidente. es la representación más sencilla y más fácil para una secuencia de cosas en un hbol sintáctico. La complicación es que aquí los vínculos son vínculos her- manos que se deben distinguir de los vínculos hijos, y eso requiere u11 nuevo campo en la declaración del irbol sititáctico. (i

34 AMBIGUEDAD 3.4.1 Gramáticas ambiguas Los árboles de anhlisis graniatical y los árboles sintácticos expresan de manera única la estructura de la sintaxis, y efectúan derivaciones por la izquierda y por la derecha, pero no Jerivaciunes en general. Desgraciadamente, una gramática puede permitir que una cadena tenga más de un árbol de análisis gramatical. Considere. por ejemplo. la gramática de la arit- mética entera simple que hemos estado ~itilizando como ejemplo estánctar

Page 119: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ambigüedad

< ,p ,J /I ? t i ? * número

número - número

número - ~ , V / J iip <'.rp

l i l número número

corresporitlierites ii las dos ~Ierivacio~ies por la izquierda

[ekp -+ erp O[) l>l/J 1

[e tp l't/J O[> l'i/) 1

[e\/> + número 1

* numero - erp op erp ((JP + - 1 * número - número op etp [erp - número

=1 número - número * eip [op -+ * j

3 número - número * número [e kp -+ número

Y

Page 120: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Los árboles sintácticos asociitdos son

Una gramática que genera una cadena con dos árboles de análisis gramatical distintos se denomina gramática ambigua. Una gramática de esta clase representa un serio problema para un analimdor sintáctico, ya que n o especifica con precisicín la estructura sintáctica de un programa (aun cuaudo las mismas cadenas legales, es decir, los miembros del lenguaje de 1:) gramática, estdn completamente determinados). En cierto sentido, una gramática am- bigua es como un autómata no determinístico en el que dos rutas por separado pueden acep- tar la misma cadena. Sin embargo, la ambigüedad en las gramáticas no se puede eliminar tan fácilmente como el no determinismo en los autómatas finitos, puesto que no hay algorit- 'tno püra hiicerlo así, a diferencia de la situación en el caso de los autómatas (la construcción del subconjunto analizada en el capítulo anterior).'

Una gramática ambigua debe, por lo tanto, considerarse como una especificación in- completa de la sintaxis de un lenguaje, y como tal debería evitarse. Afortunadamente las gramáticas ambiguas nunca pasan las pruebas que presentamos más adelante para los algo- ritmos estándar de análisis sintáctico, y existe un conjunto de técnicas estándar para tratar con ambigüedades típicas que surgen en lenguajes de programación.

Para tratar con las ambigüedades se utilizan dos métodos básicos. Uno consiste en esta- blecer una regla que especifique en cada caso ambiguo cuál de los árboles de análisis gra- matical (o árboles sintácticos) es el correcto. Una regla de esta clase se conoce como regla de no ambigüedad o de eliminación de ambigüedades. La utilidad de una regla de esta naturaleza es que corrige la ambigüedad sin moúitlcar (con la posible complicación que es'- to implica) la gramática. La desventaja es que ya no sGlo La gramática determina la estnic- -tura sintáctica del lenguaje. La alternativa es cambiar la gramática a una forma que obligue a construir el árbol de análisis gramatical correcto, de tal manera que se elimine la ambigüe- dad. Naturalmente, en cualquier método primero debemos decidir cuál de los árboles es el correcto en un caso ambiguo, Esto involucra de nueva cuenta al principio de la traduccicín dirigida por sinraiis. El árbol de análisis granmtical (o árbol sinticrico) que buscamos es aquel que refleja correctamente el significado posterior que aplicaremos a la construcción a iIti de tratfucirlo a código objeto.

Page 121: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ambigüedad

;,Cuál de los dos ái-boles sintácticos antes preseritados reprcsenian Iii ii itcrpret;icih co- rrecta de l a ca&na 34-3*42? E! pri~irer irbi) l . i~idica, al hacer e l nodo de resta un h i jo del nodo de niultiplicacicín, epc teneii~os la intencicín de ev~tluar la expresióii ev;ilriando priine- ro la resta (34 -- 3 = 3 1) y despnés I i i ~rtultiplicecicín ( 3 I " 1 2 = 130¿). E l segundo irhol. por otra parte, iriclica que primero se realimrá la rirultiplicación (3 '* 42 = 126) y después la resr;i (34 - 126 - -92). L a cr~estión de cuál árbol elegireinos depende de cuál de estos cálculos visualicenios comtt correel». L a convenciJii iiiateiixítica estáirdar dicta tgue la segtincla interpretación es la correcta. Esto se eiebe 21 que se dice que la tnultiplicación tiene precedencia sohre la resta Por l o regular. tanto la inult ipl icaci~ri como la divisicín tienen nre- ccdencia tanto sobre La suma coiiio sobre la resta.

Para eliminar la ainbigüediid dada en nuestra gramática de e x p r e s i h simple, ahora pctdríuinos simpleiriciite establecer una regla de eliininacicín tle ambigüedad que establezca las precetleiicias relativas de las tres t~peraciories-representacias. [.a solución estáiidar es darle a la suriia y a la resta 1;i niisrria precedeiicia, y proporcionar a lo niultiplicacicín tina precedencia niás alta.

Desgraciatlarnente, esta regla aún no elimina por coimpleto la n~nbigüedacf cle la grairiáti- ca. Consiclere la cadena 34-3-42. L a cadena también tiene tios posibles :írh«les sinticticos:

E l primero representa el cálculo (34 - 3) - 42 = -- 1 1 , mientras el segundo representa el cálculo 34 - (3 - 43) = 73. De nueva cuenta, la cuestibn de cuzíl cálculo es el correcto es un asunto de convencicín. La rnaternática estándar clicta que la primera selección es la e«- rrecra. Esto se debe a que la resta se consiclera conio asociativa por la izquierda; es decir, que se realizan una serie de operaciones (le resta de izquierda a tlerechti.

De este nioclo. una rtrttbigiiedacl adicioiial que reqiiicre de ttria regla tle eliniin;icitiri tle ~inibigüedades es la asociatividad de cada ima de las opernciows de suma, resta y ~ i i u l ~ i p l i - cacicín. Es coniún especificar que estas tres opemcio~ies w n asoci;ttivas por la i~cpierda. Es- to en i.cülitlad elimina las nii~bigiicclades rcsmites cn nuestra grti i~iáticii de exprcsidir siniple ( ; i i inye no podrcrnos ~leniostr~rr esto zino h;i\ta rnás ;tdCl;mte).

T m h i é n se puede clcgir especificar queuna opcriicirín cs no ;rsociativ:i. CII c l sctititlo de c ~ u ~ ~ n ; ~ c e ~ ~ e n c i a de ~nás de u11 opcri~clor es i i r i ; i c xp res ih que no está permitida. Por cje~i iplo, podríairros haber escrico ni~cscr;i gr;rill:ítica tic euprcsitiri siiriplc de I;i iii;~iicr:i q11c \e i ~ i i i cs~ ra ;i c011titru;tci6n:

Page 122: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 3 1 GRANATICAS LIBRES DE CONTEXTO Y ANÁLISIS SINTACTICO

ewp -+ fncror op tu'cictor 1 jactar fuctor -t ( erp ) / número op++ 1 - 1 *

En este caso cadenas como 34-3 -42 e incluso 34-3*42 son ilegales, y en cambio se tiehen escribir con p~réntesis, tal como ( 3 4 - 3 ) -42 y 34- (3*42 ) . Estas expresiones completaniente entre paréntesis no necesitan de laespecilicación de asociatividüd o, en realidad' de precedencia. La gramática anterior carece de ambigüedad como está escrita. Naturalmente, no sólo cambiamos la gramática, sino que tamhién cambiamos el lenguaje que se está reconociendo.

Ahora volveremos a métodos para volver a escribir la gramática a Sin de eliminar la ambigüedad en vez de establecer reglas de no mhigiiedad. Advierta que debernos hallar métodos que no modifiquen las cadenas hásicas que se están reconociendo (como se hizo en el ejemplo de las expresiones completamente entre paréntesis).

3.42 Precedencia y asociatividad Para manejar la precedencia de las operaciones en la gratnritica debemos agrupar los opera- - .

dores en grupos de igual precedencia, y para cada precedencia debemos escribir una regla diferente. Por ejemplo, la precedencia de la multiplicación sobre la suma y la resta se puede agregar a nuesira gramática de expresión Gmple conlo se ve a continuación:

exp -+ exp rq~swxn ex,? / term opsurnci -+ + 1 - terrn -+ term opntult tenn / j¿zctor opmult -+ * jbctor -+ ( exp ) 1 número

En esta gramática la inultiplicación se agrupa hajo la regla term, mientras que la suma y la resta se agrupan bdjo la regia exp. Como el caso base para una exp es un terrn, esto significa que la suma y la resta aparecerán "más altas" (es decir. niás cercanas a la raíz) en los árbo- les de análisis gramatical y sintáctico, de manera que reciben una precedencia más baja. IJna agrupación así de operadores en diferentes niveles de precedencia es un método estándar pa- ra la especificación sintáctica utilizando BNF. A una agrupación de esta clase se le conoce como cascada de precedencia.

Esta última gramática para expresiones aritméticas simples todavía no especifica la aso- ciatividad de los operadores y aún es ambigua. La causa es que la recursión en ambos lados

. del operador permite que cualquier lado iguale repeticiones del operador en una derivación (y, por lo tanto, en los árboles de análisis gramatical y sintáctico). La solución es reemplazar una de Las recursiones con el caso base, forzando las coincidencias repetitivas en el lado en que está la recursión restante. Por consiguiente, reemplazando la regla

exp -t e,~p opsirma exp / term

por

se hace a la suma y a la resta asociativas por la izquierda, mientras que al cscrihir

Page 123: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ambigüedad

se hacen asociativ:ts por la derecha. E n otras palabras, una regla recursiva por l a izquiercla hace a sus operaclores astriados a la i~qkiicrtla, mientras que una regla recursiva por la de- recha los hace asociados a la derecha.

Para completar la e l in i inac ih de ambigüedades en las reglas BNF en el caso de nuestras expresiones aritméticas siiiiples escrihimcts las reglas que pesinileii Iiacer todas las operacio- ries asociativas por la izquiercta:

Ahora el árbol de a n á l i w gramatical para la expre\ií,n 3 4 - 3 * 4 2 e\

número número

y el árbol de análisis gramatical para la e x p r e s i h 3 4 - 3 - 4 2 es

número

i \dvierta que las cascadas de prccctlcncia provocan clue los ái-holes tle ni~ál is is gi.aiti;itic:il se v w l v a n mucho iiiáh cotiiplcjos. Sin enihai-go. i io afect;tn :L los árboles sintácticos.

Page 124: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

3.4.3 El problema del else ambiguo Considere la granática del ejemplo 3.4 (página 103):

sentencia -+ sent-$1 otro sent-if-, if ( e-rp ) sentencia

/ i f ( exp ) sentencia el se sentencia exp -i. O / 1

Esta grarntitica es ambigua como resultado del else opcional. Para ver esto corisidere la cadena

if ( O ) if (1) otro else otro

E\ta cadena tiene los dos árboles de análisis gramatical:

sent-if

! I 1 otro

I otro

¡.a ctieslicín de cnál es el correcto depende de si queremos iisociür la parte clse cori In pri- mera o la icgtincla sentencia if: el primer árhol de antilisis grarnatic;il asocia I;i parte clse con la pr-itncra iciit~ncia i f ; C I sepnrlo it-bol de :~iiiílisis graniatic;d la ;isoci:i wti I;i 5cg1itiJa

Page 125: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ambigüedad

sentencia i f . Esta anihigiieilad se tienomina problema del else ambiguo. Para ver cuál i r b o l de análisis gr;ttn:itical es correcto, debenios considerar las iritplicncioiies para el sig- itificado de la sentencia if. Para ohtener iinn idea m i s clara de esto considere el sigtiicnte segiiicnto de cúdigo en C

if (x != 0)

if (y = = l/x) ok = TRUE;

else z = l/x;

E n este código, siempre que x sea 0, se presentar5 nti error de divisióti entre O s i la parte else está asociada con la primera sentencia i f . De este modo. la i~itpl icaciúi i de este código (y en realidad la implicaciúii de la saiigría de la parte else) es que una parre clse sieit~pre tleheria estar asociada con la seittencia il' rn5s ccrcritla que todab.ía no tenga una parte else asocit~la. Esta regla de eli ininación de anihigiiedad se conoce conio regla de la anidacinn más cercana para el problema del else amhiguo, e impl ica que e l segundo 6rhol de análisis grainatical anteriur es el cotrectit. Advierta que, s i quisi6ramos, podrícrnlos asociiir la parte else con la priinerri sentencia il- ~nedia~i te e l uso de lltivcs (. . .) en C. corito cri

iJna solución para la ainhigiieclad del else ambiguo cti la BNF ntisina es i n i s di f íci l que las ariihigüedades previas que heinos v i ~ t o . Una soliicióii es como sigue:

Esto fuiiciona :il permitir que llegue solainente una .seiirr~nc~icr-i~~i~fI~~~I~i antes que un else en una sentencia il, con l o que se obliga a qire todas las partes else se crtipaten tan pronto conlo sea posible. Por ejerilplo, el k h o l tle análisis grmat ica l asociado para nuestra c;idena (le niuestra se convierte ahora en

1 o t ro o t ro

Page 126: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

122 CAP. 3 I GRAMATICAS LIBRES DE CONTEXTO Y A N ~ L I S I S IINTACTICO

Por lo regular no se emprende la construcción de la regla de la anidación más cercana en la BNF. En su lugar, se prefiere la regla de eliminación de ambigüedad. Una razón es la complejidad agregada de la nueva gramática, pero la razón principal es que los métodos de análisis sintáctico son fáciles de configurar de una manera tal que se obedezca la regla de la anidación más cercana. (La precedencia y la asociatividad son un poco más difíciles de con- seguir automáticamente sin volver a escribir la gramática.)

El problema del else ambiguo tiene sus orígenes en la sintaxis de Algol60. La sintaxis se puede diseñar de tal manera que no aparezca el problema del else ambiguo. Una forma es requerir la presencia de la parte else, un método que seha utilizado en LISP y otros len- guajes funcionales (donde siempre se debe devolver un valor). Otra solución es utilizar una palabra clave de agrupación para la sentencia if. Los lenguajes que utilizan esta solución incluyen a Algo160 y Ada. En Ada, por ejemplo, el programador escribe

if x /= O then

if y = l/x then ok := true; else z := l/x;

end if;

end if;

para asociar la parte else con la segunda sentencia if. De manera alternativa, el programa- dor escribe

if x /= O then

if y = l/x then ok :3 true; end if;

else z := l/x;

end if;

para asociarla con la primera sentencia if. La correspondiente BNF en Ada (algo simplifi- cada) es

sen t -$4 if condiciín then secuenciu-de-sentencius end if / if condición then secl~enciu-de-sentencias

el se secuencia-de-sentencias end i f

De este modo, las dos palabras clave en& if son La palabra clave de agmpación en Ada. En Algo168 la palabra clave de agmpación es fi (if escrito al revés).

3.4.4 Ambigüedad no esencial En ocasiones una gramática puede ser ambigua y aún así producir siempre árboles sintácti- cos abstractos únicos. Considere, por ejemplo, la gramática de secuencia de sentencias del ejemplo 3.9 (página 113), donde pdríamos elegir una simple lista de hermanos como el &bol sintáctico. En este caso una regla gramatical, ya fuera recursiva por la derecha o recursiva por la izquierda, todavía daría como resultado la misma estructura de árbol sintáctico, y po- dríamos escribir la grarnitica ambiguamente como

Page 127: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Notaciones extendidas: E B N F y diagramar de sintaxis 123

y aún obtener árboles sintácticos únicos. Una ambigüedad tal se podría llamar ambigüedad no esencial, puesto que la semántica asociada no depende de cuál regla de elirninación de ambigüedad se emplee. tina situación semejante surge con los operadores binarios, ccinio los de la suma aritmética o la concatenación de cadenas, que representan operaciones asociativas (un operador binario . es asociativo si (u . b) c = u (b - c ) para todo valor u, h y c). En este caso los irboles sintácticos toliavía son distintos, pero representan el mismo valor semántica, y podenios no preocuparnos acerca de cuál utilizar. No obstante, un algo- ritmo de análisis sintáctico necesitará aplicar alguna regla de no ambigüedad que el escritor del cornpilador puede necesitar suministrar.

NOTACIONES EXTENDIDAS: EBNF Y DIAGRAMA5 DE SINTAXIS 3.5.1 Notación EBNF Las construcciones repetitivas y opcionales son muy comunes en los lenguajes de pro- gramación, lo mismo que las reglas de gramática de la BNF. Por lo tanto, no debería sor- prendernos que la notación BNF se extienda en ocasiones con el fin de incluir notaciones .especiales para estas dos situaciones. Estas extensiones comprenden una notación que se denomina BNF extendida, o EBNF (por las siglas del tén ino en inglés).

Considere en primer lugar el caso de la repetición, como el de las secuencias de senten- cias. Vimos que la repetición se expresa por la recursión en las reglas gramaticales y que se puede utilizar tanto la recursión por la izquierda como la recursión por la derecha, indica- das por las reglas genéricas

A 4 A a / p (recursiva por la i~quierda)

4 -, a A / p (recursiva por la derecha)

donde a y $ son cadenas arbitrarias de terminales y no terminales y donde en la primera re- gla @ no comienza con A y en la segunda $ no finaliza con A.

Se podría emplear la niisma notación para la repetición que la que se utiliza para las ex- presiones regulares, a saber, el asterisco * (también conocido como cerradura de Kleene en expresiones regulares). Entonces estas dos reglas se escribirían como las reglas no recursivas

En vez de eso EBNF prefiere emplear llaves (. . . ) para expresar Ia repetición (así hace clara la extensión de la cadena que se va a repetir), y escribimos

A--, B {el

p:tra la\ reglas

Page 128: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 3 1 GRAMATICAS LIBRES DE CONTEXTO Y ANALISIS IINTACTICO

El problema con cualquier notación de repetición es que oculta cómo se construye el ár- bol de asiilisis gramatical, pero, como ya se expuso, a menudo no iiriporta. Tonternos por ejemplo el caso de las secuencias de sentencias (ejemplo 3.9). Escribimos la gramática co- mo sigue, en forma recursiva por la derecha:

secuencia-sent -+ sent ; rectiencia-sent / wnt xent 4 s

Ehta regla tiene La forma A -+ a A / P, con A = recuencia-renr, cr = rent ; y P = wnt. En ERNF esto aparecería como

,secuencia-sent -t ( sent ; ] sent (forma recursiva por la derecha)

Tanibién podríamos haber uwio una regla recursiva por la izquierda y obtcnido la EBNF

secuenciu-sent 4 sent ( ; sent ) (forma recursiva por la izquierda)

De hecho, la segunda forma es la que generalmente se utiliza (por razones que anaiizaremos en el siguiente capítulo).

Un problema más importante se presenta cuando se involucra la asociatividad, como ocurre con las operaciones binarias como La resta y la división. Por ejemplo, consideremos la primera regia gramatical en la gramática de expresión simple de la anterior subsección:

exp -+ exp opsunm terrn / term

Esto tiene la forma A -+A a 1 p, con A = exp, a = opsuinu term y p = term. De este mo- do, escribimos esta regla en EBNF como

exp -+ term { opsuma term )

Ahora tamhibn debemos suponer que esto implica asociatividad por la i~quierda, aunque la regla misma no lo establezca explícitamente. Debemos suponer que una regla asociatsva por la derecha estaría implicada al escribir

e,rp 4 ( terrn opsuinu } term

pero éste no es el caio. En cambio, una regla recursiva por la derecha como

.secuencia-sent -+ sent ; secuencicc-sent / sent

se visualiza corno si fuera una sentencia seguida opcionalmente por un signo de punto y co- rna y una .xecirencicz-senr.

Las construcciones opcionales en EBNF se indican encenándolas entre corchetes [. . .J. Esto es similar en eseilcia a la convención de la expresión regul~rr de colocar un signo de in- terrogación despuis de una parte opcional, pero tiene la ventaja de encerrar la parte opcional sin rcyuerir pür611tesis. Por ejemplo, las reglas gramaticales para las sentencias if con partes iqxionaies clse iejctrtplos 3.4 y 3.6) se escribirían en ERNF corno se ilcscrihc a cor~tirirr;ici(,ti:

Page 129: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Notaciones extendidas: EBNF y diagramas de sintaxis

U11a regla recur\iv;r por la derecha tal como

tanibik $e escribe como

(compare esto con el uso de las llaves anterior para escribir esta reglaen Iortna recursiva por la izquierda).

Si deseáramos escribir una operacidn aritmética tal como la suma en forma asociativa derecha, escribiríamos

en lugar de utilirar llaves

3.5.2 Diagramas de sintaxis

Las representaciones gráficas para simbolizar de manera visual las reglas de la EBNF se denominan diagramas de sintaxis. Se componen de cajas que representan terminales y no terminales. líneas con flechas que simbolizan secuencias y selecciones, y etiquetas de no ter- minales para cada diagrama que representan la regla gramatical que define ese no terniinal. Para indicar terminales en un diagrama se emplea una caja de forma oval o redonda, mientras que para indicar no terminales se utiliza una caja rectangtilar o c,uadrada.

Considere como ejemplo la regla gramatical

Ésta se escribe como un diagrama sintáctico de la siguiente manera:

Advierta yue~irctor no está situado en iinrt caja, pero se utiliza como rma etiqueta para cl di:igrama tle sintaxis. lo cual indica que el <li;tgr;ini;t representa la dcFinici61i de la cstriictn- r3 de cse nombre. !\rlvierta tmbién el tiso de los líneas con flechas para iixlic:~r la wlcccih y la sccuenciaci61i.

Page 130: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 3 1 GRAMATICAS LIBRES DE CONTEXTO Y ANALISIS IINTÁCTICO

Los diagramas de sintaxis se escriben desde la EBNF mds que desde la BNF, de modo que necesitamos diagramas que representen construcciones de repetición y opcionales. Da- da una repetición tal como

el ciiagratna de sintaxis correspondiente se dibnja por lo regular como sigue:

Advierta que el diagrama debe tener en cuenta que no aparezca ninguna B. Una construcción opcional tal como

se dibuja como

Concluiremos nuestro análisis de los diagramas de sintaxis con algunos ejemplos en los que se utilizan ejemplos anteriores de EBNF.

Ejemplo 3.10 Considere nuestro ejemplo de ejecución de expresiones aritméticas simples. Éste tiene la BNF (incluyendo asociatividad y precedencia).

exp -+ exp opsuma term / term opsuma -+ + 1 - term -+ term opmult fbctor 1 factor opmult -+ * factor -+ ( exp ) 1 número

La EBNF correspondiente es

exp -, ternl ( opsuma term ) opsurna -+ + / - term 4 jzctor ( opmult juctor ) opmult -+ * fLictor -+ ( e up ) 1 número

Los tiiagramns de sintaxis correspondientes se proporcionan en In tlgnra 3.1 (el Jiagrasna de sintaxis parqfiictor \e dio con anteriorid;iti).

Page 131: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Notaciones extendidas: EBNF y diagramar de sintaxis

Ejemplo 3.1 1

rer rn w

Jm tor

Considere la g r~ rná t i c~ de bis sentencias if sirnplific:t<las del ejeinplo 3.4 (pllgioa 103). És- .te tiene la BNF

y la EBNF

Page 132: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

128 CAP. 3 I GRAMATICAS LIBRES DE CONTEXTO Y A N ~ L I S I I I IINTÁCTICO

Fisura 3.5 serit-if

Diagramar de sintaxis para la gnmátita del ejemplo 3.1 1 srma.i<i

3.6 PROPIEDADES FORMALES DE LOS LENGUAJES LIBRES DE CONTEXTO 3.61 Una definición formal de los lenguajes libres de contexto Pre5entarnos aquí de una manera más formal y matemática algo de la terminología y defini- ciones que presentamos anteriormente en este capítulo. Comenzaremos por establecer una de- finición formal de una gramática libre de contexto.

Definición

Una gramática libre de contexto se compone de lo qiguiente:

Un conjunto T de terminales. Un conjunto N de no terminales (disjunto de 7J. Un conjunto P de producciones, o reglas gramaticales, de la forma A -+ a. donde A es un elemento de N y cu es un elemento de ( T U Nf* (una secuencia posiblemen- te vacía de terminales y no terminales).

J. Un símbolo inicial S del conjunto N.

Sea G una p tn i t i ca detinida del modo anterior, de manera que G -- (T. N. P, S). Un paso o etapa de derivación sobre G es de 13 forma a y * cr P y, donde u y y son ele- mentos de (l'u N)" y .? 4 /i; está en P. í1.a mitin T U N de 10s ~oiijimtos de terminales y

Page 133: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Propiedades formales de los lenguales l~bres de contexto 129

no tcrrninales se conoce en ocasiones como conjunto de símbolos de C;. y una cadena cu en ( T U N)* se conoce como forma de sentencia u oracional.) 1.a relación tu =* fi se definc como la cerradura traiisitiva de la relación de paso de la derivacitin 3: cs dccir, a 3% fi s i y stilo si, existe una secuencin (le O o inás pasos de derivación (11 2 O)

tal que u = al y P = a,,. (Si I I = O, entonces a = P.) Una derivati611 sohre la gramática G es de la forina S 3" cv. donde w E T+ (es decir, w es una cadena de sólo tern1in:iles dcnominada sentencia) y S m el síntbolo inicial de G.

El Lenguaje generado por G, denotado por '(G). se dcfine como el conjunto L ( G ) - { w E 7-1 existe una derivación S ==+ * I ~ V de G ) . Es decir, L(G) es el conjunto de sentencias derivable de S,

Una derivacich por la izquierda S *E,, iv es una derivación en la cual cada paso de de- rivación a A y * a p y es de tal nianera que a E p; es decir, u se compone sólo de tcrmina- les. De manera similar, una derivaci6n por la derecha es aquella en la cual cada paso de deriv:tción u A y =r a p y tiene ID propiedad de que y E F .

./ Un árbol de análisis gramatical sobre la gramática G es un árbol etiquetado desde La r a í 7 que tiene las siguientes propiedades:

l. Cada nodo está etiquetado con un terminal o un no terminal o E .

2. E l nodo raíz está etiquetado con el símbolo inicial S. 3. Cada nodo hoja está etiquetado con un terminal o con e.

4. Cada nodo no hoja está etiquetado con un no terminal. 5. Si un nodo con etiqueta '4 E N tiene n hijos con etiquetas XI, .Y2, . . . , Xn (los cuales

pueden ser terminales o no terminales). entonces A -+ X, & . . . X,, E P (una pro- ducción de la gramática).

Cada clerivación da origen a un árbol de :inálisis gramatical tal que cada paso a A y I-,

u p y en la derivación, con fl = ,Yl X2 . . . X,, conesponde a la construcción d e n hijos con etiquetas XI, X2, . . . , X,, del riodo coi1 etiqueta A. En general, muchas derivaciones pueden (lar origen al mismo irbol de análisis gramatical. Sin ernbargo, cada árbol de aniílisis gra- niatical tiene una única derivación por la izquierda y por la derecha que le da origen. La de- rivación por 1ü izquierda corresponde a un recorrido preorden del i rbol de análisis grainati- cal, mientras la derivación por la derecha corresponde a un recorrido inverso postorden del árbol de análisis graniatical.

Se dice que un conjunto de cadenas L es un lenguaje libre de contexto s i existe una gramática G libre de contexto tal que L = L(G). En general, muchas grarnáticas diferentes generan el mismo tcnyuaje libre de contexto, pero las cadenas en el lenguaje tendrin árbo- les de análisis gramatical diferentes dependiendo de la gramática utilizada.

Una gramática G es ambigwa s i existe una cadena w E L(G) tal que w tenga dos árboles de anilisis gramatical distintos (o derivaciones por la izquierda o por la derecha).

3.6.2 Reglas gramaticales como ecuaciones

A l principio de esta seccitin advertimos que las reglas gramaticales u t i l i ~an c l símbolo dc la tiecha en lugar de un signo de igualdad para rcprescntar la <lefinicititi (le los tioinbrcs de 1:ts estructuras (110 terlnin;iIes). ;i diferencia de nuestra ~iotaciiii i par:i las eupresiones rcgul;ircs, cioritle ilcfiníaritr)~ los nonihres de Cst;is nietlimrc el signo [le igmldatl. 1.a ra~ i ín q ~ c sc dio kie yuc la n:itiit-de/a rcct~i-sita de 13s ~rcgl;is gr~iii;iti~:~lcs hitce l a dc(irlici61l dc LIS

Page 134: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

relaciones (las reglas gramaticales) menos parecidas a la igualdad, y vimos que en reali- dad las cadenas definidas por reglas gramaticales resultan de derivaciones, donde se utiliza un método de reemplazo de izquierda a derecha que sigue la dirección de la flecha en la BNF.

Sin embargo, hay un sentido en el que aún se mantiene la igualdad de los lados izquier- do y derecho en una regla gramatical, pero el proceso de definición de lenguaje que este punto de vista da como resultado es diferente. Este punto de vista es importante para la teoría de la semántica de lenguajes de programación y vale la pena estudiarlo brevemente por su penetración en los procesos recursivos, como el análisis sintáctico, aun cuando los algoritmos de análisis sintáctico que estudiamos no se basen en ello.

Considere, por ejemplo, la siguiente regla gramatical, que se extrajo (en forma simpli- ficada) de nuestra gr'mática de expresión simple:

exp -+ exp + exp / número

Ya vimos que un nombre de no terminal como exp define un conjunto de cadenas de termina- les (el cual es el lenguaje de la gramática si el no terminal es el símbolo inicial). Suponga que nombramos a este conjunto E y que N es el conjunto de los números naturales (corres- pondiente al nombre de la expresión regular número). Entonces, la regla gramatical dada Fe puede interpretar como la ecuación de conjuntos

donde E + E es el conjunto de cadenas ( u + v / u,v E E ) . (No estamos sumando las cadenas u y v aquí, sino haciendo la concatenación de ellas con el símbolo + entre las mismas.)

Ésta es una ecuación recursiva para el conjunto E. Considere cómo puede definir esto a E. En primer lugar, el conjunto N está contenido en E (el caso base), puesto que E er, la unión de N y E + E. Acto seguido, el hecho de que E + E también esté contenido en E implica que N + N está contenido en E . Pero entonces, como tanto N como N + N están contenidos en E, también lo está N + N + N, y así sucesivamente. Podemos visu a 1' izar eqto como una construcción inductiva de más y más cadenas largas y más largas, y la unión de todos estos conjuntos es el resultado deseado:

De hecho, puede demostrarse que esta E satisface la ecuación en cuestión. En realidad, E es el conjunto más pequeño que lo hace. Si observamos e1 lado derecho de la ecuación para E como una función (conjunto) de E, de manera que definamos f ( s ) = (S + S) u N, entonces la ec&ción para E se convierte en E= f ( E ) . En otras palabras, E es un punto fijo de la función f y (de acuerdo con nuestro comentario anterior) en realidad es el punto fijo más pequeño. Decimos que E, de la manera en que está definido por este método, está propor- cionando una semántica de punto Ftjo mínimo.

Puede demostrarse que todas las estructuras de lenguaje de programación definidas recursivamente, como la sintaxis, tipos de datos recursivos y funciones recursivas, tienen se- mántica de punto fijo mínimo cuando se implementan según los algoritmos habituales que estudiaremos más adelante en este libro. Esto es un punto importante, ya que los niétodos como éste se pueden emplear en el futuro para verificar la exactitud de los compiladores. Actualmente es raro que se pruebe si los compiladores son correctos. En su lugar sólo se realizan pruebas del código del cornpilador para asegurar una aproximación de su exactitud, y a menudo se mantienen errores sustnnciales, incluso en compiladores producidos coiner- ci~lincute.

Page 135: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Prop~edades formales de los lenguajes libres de contexto

3.M La jerarquía de Chomsky y los límites de la sintaxis como reglas libres de contexto

Cuando se representa la estructura sintlíctica de u n lenguaje de progrmación, las grarnáti- cas libres de contexto en BNF o EBNF son una herramienta út i l y poderosa. Pero tatribién es importante saber lo que puede 0 debería ser representado por la BNF. Ya vimos una situación en la cual la gramática se puede mantener ambigua de tnanera intencional (el problema del else ambiguo), y de ese modo no expresar la sintaxis completa direciamente. Pueden surgir otras situaciones donde podamos intentar expresar demasiado en la grarnáti- ca, o donde pueda ser imposible expresar un requerimiento en la gramática. En esta sección analizaremos algunos de los casos comunes.

Una cuestibn que surge con frecuencia cuando escribimos la BNF para un lenguaje es la extensión para la cual la estructura léxica debería expresiuse en la BNF más que en tina descripción separada (posiblemente utilizando expresiones regulares). El análisis anterior mostró que las grarniíticas libres de contexto pueden expresar concatenación, repetición y selección, exactamente como pueden hacerlo las expresiones regulares. Por lo tanto, podría- mos escribir reglas gramaticales para la construcción de todos los tokens de caracteres y prescindir del todo de las expresiones regulares.

Por t$einplo, consideremos La definición de un número como una secuencia de dígitos titilizantio expresiones regulares:

d í g i t o = 0111213141516171819 número = d í g i t o d í g i t o *

En cambio, podemos escribir esta definición utilizando BNF como

Advierta que la recursión en la segunda regla se utiliza sólo para expresar repetición. Se dice que una gramática con esta p p i e d a d e s una gramática regular, y las graniáticas regulares pueden expresar todo lo que pueden hacer las expresiones regulares. IJna con- secuencia de esto es que podríamos diseñar un analizador sintáctico que podría aceptar ca- racteres directamente del archivo fuente de entrada y prescindir por completo del analizador léxico.

¿Por qué no es una buena idea hacer esto? Porque podría quedar eomprornetida la eficiencia. Un analizador sintictieo es una miquina más poderosa que un analizador léxico, pero es proporcionalniente menos eficiente. Aun así, puede \er razonable y útil ineluir una definición de lo\ toketi\ en la BNF en 5i: la gramática expreiaria entonces la estructura cintúctica completa, incluyendo la eitructura Iéxica. Naturalmente, el ~mplementador del lenguaje esperaría extraer estas definiciones de la gramática y convertirlas eri un analizador léxico.

Una situación diferente se presenta respecto a las reglas de contexto, las cuales se pre- sentan frecuentemente en lenguajes de prctgramaci6n. Hemos estado utilizari<lo el término "libre de contexto" sin explicar por qué tales reglas son en efecto "libres de contexto". La razón mis simple es que los no terniinales aparecen por sí mismos a l,a izquierda de la flecha en las reglas libres de contexto. De este modo, una regla

Page 136: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 3 1 GRAMATICAS LIBRES DE CONTEXlO Y ANALISIS IINTACTICO

dice que /1 se puede reemplazar por a en c~ulyuier sitio, sin impowar dónde se presente. Por otra parte, podnarnos definir un contexto de manera infornial como un par de cadenas (de terminales y no terminales) p, y, tales que una regia se aplicaría sólo si P se presenta antes y y se presenta después del no terminal. Po<l&amos escribir esto como

Una regla de esta clase en la que cu f E se denomina regla gramatical sensible al contexto. Las gramáticas sensibles al contexto son más poderosas que las gramáticas libres de con- texto, pero son mucho más difíciles de utilizar como la base para un an~lizador sintáctico.

¿,Qué clase de requerimientos en los lenguajes de prograrnación requieren de reglas sensibles al contexto'? Los ejemplos típicos involucran el uso de nombres. La regla en C que requiere dectaraciún antes del uso es un ejemplo común. Aquí, un nombre debe aparecer en una declaración antes que sea permitido su uso en tina sentencia o una expresión:

Si intentáranios abordar este requerimiento utilizando reglas de BNF, tendríamos que, en prinier lugar, incluir las cadenas de nombre inistnas en las reglas gramaticales en vez de in- cluir todos los nombres como tokens de identificador que son indistinguibles. En segundo lugar, tendríamos que escribir para cada nombre una regla estableciendo su declaración an- tes de un uso potencial. Pero en muchos lenguajes la longitud de un identificador no está restringida, de modo que el número de posibles identificadores es (al menos potencial- mente) infinito. Incluso si permitiéramos que los nombres fueran de sólo dos caracteres de longitud, tendríamos el potencial para cientos de nuevas reglas gramaticales. Evidentemen- te, esta situación es imposible. La solución es semejante a la de una regla de eliminación de ambigüedad: simplemente estabiecemos una regla (declaración antes del uso) que no está explícita en la gramática. Sin embargo, existe una diferencia: una regla así no se puede im- poner por el analizador sintictico mismo, puesto que está inás allá del poder de expresar de las reglas libres de contexto (razonables). En su lugar, esta regla se vuelve parte del análisis semántica, porque depende del uso de la tabla de símbolos (que registra cuáles identifica- dores se han declarado).

El cuerpo de las reglas de lenguaje, al que se hace referencia como semántica estática del lenguaje, está más allá del alcance de verificación del analizador sintáctico, pero todavía puede ser verificado por el cornpilador. Esto incluye verificacibn de tipo (en un lenguaje con tipos suministrados estáticamente) y reglas coino declaración a&es del uso. De '1 , h ora en adelante consideraremos como sinraxis sólo aquellas reglas que se puedan expresar mediante reglas BNF. Todo lo deniás se considerará como semántica.

Existe otra clase de gramática que es incluso ~nás general que las gramáticas sensibles al contexto. Estas gmnkicas se denominan gramáticas sin restricciones y tienen reglas gramaticales de la forman + p, donde no hay restricciones en la forma de las cadenas cu y P (excepto que u no puede ser e). Las cuatro clases de gramáticas (sin restricciones, sensi- bles al contexto, libres de contexto y regulares) también se conocen como gr;~máticas de ti- po 0. tipo 1, tipo 2 y tipo 3, respectivatneute. A las clases de lenguaje que se construyen con ellas se les conoce tairihién como Jerarquía de Chomsky, en honor de N o m Chomsky. quien kie cl prinrero en utilizarlas para describir los lenguajes naturalcs. Estas grarixíticas rcpresentm ~tivcles ilistiritos rle potencia computacioiial. E k realidad. las p~n;ític:is sin rci-

Page 137: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Itntaxis del lenguaje TlNY 133

tricciones (o de tipo O ) son equivalentes a las i r iáyui~~as de I'uring, de l a inisiiia iriimcra cpc las gramáticas regril;ires son equivalentes a los autómatas finitos, y. por consigtiieiite. rcpre- senian la clase mí$ general de computación cotiocida. Los graiiriíticas libres Je contexto t;tmbién tienen un equiv:ilctite de tiiácjniiia corresy>o~itliente. clenonriiinda ant6rnat:i "de pila" (pirshdown), pero no iiecesitiiremos todo el poder de uim rn iq i i im así pura nucstros ülgorir- nios de análisis sintáctico. así que no los ;inaliz;iremos inás.

, . l ambién dcherí~nios ser cttidadosos respecto a que ciertos problemas coiiiput:icii>- tialmente intratables están asociados con grainiílicas y lenguajes libres de coritex~o. Iior qjeriiplo, al tratar coi1 gramátic;is ambiguas, sería excelente que pudiCranios c u h l e c e r riti ;tlgoritmo que convirtiera una gramática ambigua en tina no aiiibigua sin inoil if icar el Icn- guaje subyacente. Desgraciatlamente, se s:~hr que &te es un problema que aún no se ha re- suelto, de manera que posihleincnte iio exista un algoritmo así. De hecho, existeri incluso lenguajes lihres de coriiextu para los cuales no existe una gr;inrática no ambigua (éstos se cle- nominan lenguajes inherentemente ambiguos), e incluso no se ha resuelto e l problcnia de la deteroiinaciórt de si un lenguaje es inherentemente ambiguo.

Por suerte, las complicaciones tales como nna ainhigiicdad inherente no surgcii como una regla en los lenguajes de programación, y las ticnicas elaboradas ex profeso para la eliminación de la anihigüedad que descrihiinos por lo regular han probado ser adectiadiis en casos prácticos.

3.7 SINTAXIS DEL LENGUAJE TINY 3.1.1 Una gramática libre de contexto para TINY L a gramática para TINY está diida en B N F en la figura 3.6. A partir de &í hacenios las siguientes observaciones. U n programa TINY es simpleinerite una sectieiicia de sentencias. Existen cinco clases de sentencias: sentencias if o condicionales, sentencias repeat o de repeticicín, sentencias r e d o de Lectrira, sentencias write o de escritura y sclltc~icias assign o (le asignación. Estas tienen una. sintaxis tipo Pascal, sólo que la senterici~i i f u t i l i za end como palabra clave de agrupación (de manera que no hay anibigüedad de else m h i g u o en TINY) y que las sentencias i f y las sentencias repeat permiten seciiencias de sentencim co- mo cuerpos, de modo que no s o ~ necesarios los corchetes o pares de beg in -end (e incluso

F i t u r d 3.b programa-, secuenciu-.?en1 Gramática del lenguaje secuencia-sent -+ secuencia-sent ; sentencia / serztmciu TlNY en BNF sentencia -+ sent-if / sent-repeat / sent-assigo / sent-reíid / serzt-write

.sent-if-+ if exp then secitencLiu-sent end / i f exp then ,recuenciu-se~zi'else secuencia-sent end

setit-repeut + r e g e a t secrtencicl-sent unt il exp .sent-assign + i d e n t i f i c a d o r : = e,rp sent-reud + r e a d i d e a ti f i c a d o r sent-write -+ write exp rsp -+ exp-simple op-cornpurucicin rxp-simple / e,rp-simple opcornparucicin -+ < = e,rp-simple -+ e.xp-sitnple opsicniu trnn / term ucla'op + + / - lefm -+ ferm oprnn[t,f«cfor 1 frtcior oprrnilt + * 1 / firctor i ( e.ip ) / número i identificador

Page 138: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

begin no es una palabra reservada en TINY). Las sentencias de entraddsalida comienzan mediante las palabras reservadas read y write. Una sentencia read de lectura puede leer sólo una variable a la vez, y una sentencia write de escritura puede escribir solamente una expresión a la vez,

Las expresiones TINY son de dos variedades: expresiones booleanas que utilizan los operadores de comparación = y < que aparecen en las pmebas de las sentencias if y repeat y expresiones aritméticas (denotadas por medio de exp-simple en la gramática) que incluyen los operadores enteros cstándar +, -, * y / (este último representa la división entera, en oca- siones conocida como div). Las operaciones aritméticas son asociativas por la izquierda y tienen las precedencias habituales. Las operaciones de comparación, en contraste, son no asociativas: sólo se permite una operación de comparación por cada expresión sin parénte- sis. Las operaciones de comparación también tienen menor precedencia que cualquiera de las operaciones aritméticas.

Los identificadores en TINY se refieren a variables enteras simples. No hay variables estructuradas, tales como arreglos o registros. Tampoco existen declaraciones de variable en TINY: una variable se declara de manera implícita a1 aparecer a la izquierda de una asigna- ción. Además, existe sólo un ámbito (global), y no procedimientos o funciones (y, por lo tanto, tampoco llamadas a los mismos).

Una nota final acerca de la gramática de TINY. Las secuencias de sentencias deben con- tener signos de punto y coma separando las sentencias, y un signo de punto y coma después de la sentencia final en una secuencia de sentencias es ilegal. Esto se debe a que no hay sen- tencias vacías en TINY (a diferencia de Pascal y C). También escribimos la regla BNF pa- ra secuencia-sent como una regla recursiva por la izquierda, pero en realidad no importa la asociatividad de las secuencias de sentencias, porque el propósito es simplemente que se ejecuten en orden. De este modo, tíunbién podríamos simplemente haber escrito la regla secuencia-sent recursivamente a la derecha en su lugar. Esta observación también se repre- sentará en la estructura del árbol sintáctico de un programa TINY, donde las secuencias de sentencias estarán representadas por listas en lugar de árboles. Ahora volveremos a un análisis de esta estructura.

31.2 Estructura del árbol sintictico para el compilador TINY En TINY existen dos clases básicas de estructuras: sentencias y expresiones. Existen cinco clases de sentencias (sentencias if, sentencias repeat, sentencias assign, sentencias read y sentencias write) y tres clases de expresiones (expresiones de operador, expresiones de constante y expresiones de identificador). Por lo tanto, un nodo de árbol sintáctico se cla- sificará principalmente como si fuera una sentencia o expresión y en forma secundaria respecto a la clase.de sentencia o expresión: Un nodo de árbol tendrá un máximo de tres es- tructuras hijo (solo se necesitan las tres en el caso de una sentencia if con una parte else). Las sentencias serán secuenciadas mediante un campo de hermanos en lugar de utilizar un campo hijo.

Los atributos que deben mantenerse en los nodos de árbol son como sigue (apate de los campos ya mencionados). Cada clase de nodo de expresión necesita un atributo especial. Un nodo de constante necesita un c a m p para la constante entera que representa. Un nodo de identificador necesita un campo para contener el nombre del idcntificador. Y un nodo de ope- rador necesita un campo para contener el nombre del operador. Los nodos de sentencia ge- neralmente no necesitan atributos (aparte del de la clase de nodo que son). Sin embargo, por simplicidad, en el caso de las sentencias assign y read conservaremos el nombre <le la varia- ble que se está asignando o leyendo justo en el nodo de sentencia rnisnio (mis que como un nodo hijo de expresih).

Page 139: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Sintaxis del lenguaje TlNY 135

1.21 estructura de nodo de árbol que acabamos ile describir se puede obtener mediante las declaraciones en C dadas en la figura 3.7, que se repiten del listado correspondiente al ar- chivo globals .h en el apéndice B (líneas 198-217). Advierta que utilizanios uniones en estas declaraciones para ayudar a conservar e1 espacio, Éslas también nos ayudarin a recor- dar cuáles atributos van con cada clase de nodo. Dos atributos adicionales que aún no he- mos mencionado están presentes en la dec'ración. E! primero es el atributo de contabilid~d lineno; éste nos permite imprimir los nún~eros de Iíneas del código fuente con errores que puedan ocurrir en etapas posteriores de la traducción. El segundo es el campo type, el cual después necesitaremos para verificar el tipo de las expresiones ( y solamente expresiones). Está declarado para ser del tipo enumerado ExgType; esto se estudiará con todo detalle en el capítulo 6.

Fiyura i? Oedaracionos en C para un nodo de árbol iintactico

TlNY

Ahora queremos dar una descripción visual de la estructura del lírbol sintáctico y mos- trar visualmente este árbol para un programa de muestra. Para hacer esto utilizamos cajas rectangulares para indicar nodos de sentencia y cajas redondas u ovales para indicar nodos de expresión. La clase (le sentencia o expresión se proporciona& como una etiqueta dentro de la caja, mientras que los atributos adicionales también se enumerarán allí entre paréntesis. Los apuntadores hermanos se dibujarán a la ~Icrechü de las cajas de nodo, mientras que los apuntadores hijos se dibujarán ahajo de las cajas; También indicaremos otras estructuras de rirbol que no se especifican en los diagranias mediante triángulos y Iíneas punteailas para in- ilicar estructuras que puedan o no aparecer. Una secuencia de sentencias coiiect~das tnedian- te campos de hermanos e vería entonces como se muestra a continuacicín (los subrirholes potenciales cstán intlicados ine<li;uitc la.; Iíne:is piinteiidas y l os triringtilos).

Page 140: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 3 1 GRAMATICAS LIBRES DE CONTEXTO Y ANALISIS SINTACTICO

Una sentencia if (con potencialmente tres hijos) tendrá un aspecto como el siguiente.

prueba parte then parte else

Una sentencia repeat tendrá dos hijos. La primera es la secuencia de sentencias que repre- sentan su cuerpo; la segunda es la expresion de prueba:

cuerpo prueba

Una sentencia de asignación ("assign") tiene un hijo que representa la expresión cuyo valor se asigna (el nombre de la variable que se asigna se conserva en el nodo de sentencia):

expresión

[Jna sentencia de escritura ("write") tamhién tiene un hijo, que representa la expresión de la que se eicrihir;i el valor:

Page 141: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Iintaxis del lenguaje TINY

expresión

Una expresión de operador t ime dos Iii.jos, que representan las cxpresioiies de i>per;i~ido iiquierdo y dereclio:

'focios los otros nodos isentencias reid, expresiones de identiI'icndor y expresiones de cons- tante) son iiodos hoja.

Finaltneiltc estamos listos piirn mostrar el irbol de iin programa TINY. El pri)graim di: inriestra del up í tu lo 1 que calcula el I'actorial de ttii entero se repite en la figura 3.8. Su :irbol sintictico se in~iestrü eii la figura 3.0.

f iourd 1 8 í Programa de muestra

Programa de muestra en el en lenguaje TINY-

lenguaje TINY calcula el factorial

1 read x; ( entrada de un entero

if O < x then ( no calcula si x := O )

fact := 1;

regeat

fact :P fact * x;

x : = x - 1

until x 5 O;

write fact í salida fsctorial de x > end

Page 142: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Fisura 3.9 Arbol rindctico para el programa TlNY de la figura 3.8

CAP, 3 1 GRAMATICAS LIBRES DE CONTEXTO Y A N ~ L I S I S SINTACTICO

{S;, 8;s; . S;S;S;, . . . t . b. D<S una derivación por la izquierda y por la dcrecha para la cadena S; S; utilizando su gra-

3.2 Dada la gramática A -+ AA / ( A ) / E ,

a. Describa el lenguaje que genera. b. Muestre que es ambiguo.

3.3 Dada la gramática

e.rp -+ exp opsuma term / tertn

opsutna -+ + / - term -+ term opmult,fac.tor /factor

opmult -+ * factor -t ( exp ) / número

escriba derivaciones por la izquierda, árboles de análisis gramatical y árboles sintácticos abs- tractos para las siguientes expresiones:

a. 3+4*5-6 b. 3 * (4 -5+6 ) c. 3 - ( 4 + 5 * 6 )

3.4 La gramitica siguiente genera todas las expresiones regulares sobre el alfabeto de letras (utilizn- mos las comillas para encerrar operadores, puesto que la barra vertical también es un operador además de un metusímbolo):

re.rp -+ rexp " / " rerp j re.xp rexp

/ rexp "*" / " ( " rerp " ) "

1 letra

Page 143: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

a. Proporcione ima derivación para la expresiún regular (ab I b) * uti l imnil i i esta grümática.

b. Muestre que esta gramática es ambigua.

c. Vuelva a escribir esta gramática para establecer Ias precedencias ~orrectas'~;ir;i los opera-

&>res (véase el ciipitulo 2). d. ¿,Qué asociatividad da su respuesta en c l inciso c para los operadores binarios? Explique

su respiicsta.

3.5 Escriha una graiuátic;~ para expresiones hooleanas que incluya las co~istantes t rue y f a l se ,

los operadores and. o r y not, adernás de los paréntesis. Asegúrese de darle a or nna prcce-

deiicia niás haia que a and, y a and una precedencia rnás baja que a not, además de permitir wrese la repetición del operador not como en la expresión boolema not not t rue. Ase&'

también de que su gr~mática no sea ambigua.

3.6 Considere Iü gramática siguiente que representa expresiones simplificadas tipo IJSP:

lerp -+ t r t m / list utoin 4 número / i d e n t i f i c a d o r

list -+ ( lexp-sec ) krp-.rec -t kxp-.sec le.rp / lrrp

a. Escriha nna derivación por la iyuierda y por la derecha para la cadena ( a 23 ( m x y) ) . b. Diht je un árhol de análisis granuticnl para la cadena del inciso a.

3.7 a. Escriba declaracictnes ~ i p o C para una estructura de árhol sintictic« :ibstract« correspon-

diente a la gramática del ejercicio 3.6. b. Dihuje el árbol sintáctico para la cadena ( a 23 ( m x y) ) que result;iríit de sus tlecla-

~ ic iones del inciso a.

3.8 Dada la graluática siguiente

.serite~z~.ia -. .ven!-if / o t r o / F

.serir-$-. if ( r q ~ ) sentencitr pirrr-else ptrrte-else 4 e l s e sentenciu / E

exp -) 0 / 1

a. Dibuje un L h o l de an6lisis grainatical para la cadena

i f ( 0 ) i f ( 1 ) otro e lse e lse otro

b. ;Cuál es el propósito de los dos e l s e ?

c. ¿,Es un cóuigo similar permisible en C? Justifique su respuesta.

3.9 (Aho, Scthi y Ullinan) Muestre que el siguiente intento para resolver la ambigüedtid del else

ambiguo todavía es ambiguo (compare con la soluciún de la página 121):

3.10 a. T r a i l w t ~ i I:I gr;im6tiw del ejercicio 3.6 a EHNF.

b. Dibuje di;igramas de sintaxis para la ENNF del inciso a.

3.1 1 Dada la ecuacih de conjiintos X = ( X i- X) u N (N es el conjunlo (le los riiirnen>s n:ituralcs:

&se I;i sección 3.6.2, p;igiii;r IL91.

Page 144: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 3 1 GRAMÁTICAS LIBRES DE CONTEXTO Y ANÁLISIS IINTACTICO

a. Muesue que el conjunto E = N u (N + N ) u (N + N t N) v (N N +- N + N ) u . . . satisface Iir ecuación.

b. Muestre que. dado cualquier c«njunt« E' que satisfaga la ecuación. E C E'. 3.12 Signos de nienos unitarios se pueden agregar de diversas formas a la gramática ilz expresión

aritmética simple del ejercicio 3.3. Adapte la BNF para cada uno de los cüsns que siguen de manera que satisfagan la regla establecida. a. Conto niáximo se permite un signo de menos unitario en cada expresiitn, y dehe :ipiirrcer

al principio de una expresión, de manera que -2 - 3 es se evüluü a -- S) y - 2 - ( - 3 ) es legal, pero - 2 - - 3 no lo es.

b. Corno máximo se permite un signo de menos tinitario antes de cierto número o paréntesis izquierdo, de manera que - 2 - - 3 es legal pero - - 2 y - 2 - - - 3 no lo son,

c. Se permite iin número arbitrario de signos de menos antes de números y paréntesis i ~ -

pierdas, de modo que cualquier caso de los aritericxes es legal.

3.13 Considere la ~iguiente simplificación de la gramática del ejercicio 3.6.

lexp -r número / ( o p 1e.r)~-sec )

q + + 1 - l * lexp-rp-src + lexp-íec le.rp 1 lexp

Esta gramática puede pensarse como representaciitn de expresiones aritméticas enteras simples en forma de prefijo tipo LISP. Por ejemplo, la expresión 3 4 - 3 * 4 2 se escribiría en esta gra- ~nfiticacor~io ( - 34 ( * 3 4 2 ) ) . a. ¿Qué interpretación debería darse a las expresiones legales ( - 2 3 4 ) y ( - 2 ) ? ¿,Cid1

respecto a las expresiones ( + 2 ) y ( * 2 ) ?

b. ¿Son un problema la precedencia y la asociatividad con esta gramática? ¿,La gramática es ambigua'?

3.14 a. Escriba declaraciones tipo C para una cstructirra de árbol sintictico para la gr:irnática del ejercicio anterior.

b. Dibuje el jrbol sintáctico para la expresión ( - 34 ( * 3 4 2 ) ) utilizando su respuesta para el inciso a.

3.15 Se mencionó en la página 1 1 1 que la descripción tipo BNF de un árbol sintiictico abstracto

e i p -t OpExp(op,exp,e.rp) / ConstExp(iritrger) op i Plus / Minr~s / Times

esth cerca de una declaración tipo real en algunos lenguajes. Dos lenguajes de esta clase son ML y Haskell. Este ejercicio es para aquellos lectores que conocen alguno de esos dos lenguajes. a. Escriba declaraciones de tipo de datos que implemeriten la sintaxis abstracta anterior. b. Escriba una expresiún sintáctica abstracta para la expresión ( 34 * ( 4 2 - 3 ) ) .

3.16 Vitelva a escribir e1 irbol sintáctico typedef al principio de la secckn 3.1.2 (pfigina l I I ) para i~tilizar una union

5. Advierta que CI segundo higno dt: rnenos en zsta expresión es u n tireriiis Di~rrrrio, no un menos 11ilil;ll-io.

Page 145: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejercicios

3.17 1)enriiestrc que la graiii5tica del -jeiriplo 1.5 (p;igiiia 105) genera el conjunt~r de to<i:is las d e - n;is de par6ntesis hilance;irli)s, doiide w es una c:alcna de pi~réntcsis halancetttlos \iempre que tengan las siguientes dos propied:ides: 1. 11, contieiic cxact:riircntc el ~ni\itio n h e r o de pxentesis irqoicr<Io\ y ilcreclim.

2. ('adu prel'ijo ir de ii. i i v = r1.v para algiinii i ) tieiie al menos taiitos p:irénte\is izquienlos cii-

!no pai-6rttcsis derechos. tSir,qwerii.kr: de~iiucsire por iitducci6n en la Iongiti~d de tina deriv:rcií,n.)

3.18 a. Fscriha iirtii gramítica seticihle n l contexto que geiiere ci~deiias (te la lijrnin w r , il~mile r es

iina cadena de c r y h.

b. ;,Es posible csct-ihir iinit pi-;rrnática libre de contexto p:ir;r Iiis d e n a s del iitciso a? Explique

por qué.

3.19 En itlgunos lengi~ajcs (por .jtmplo Modiila-7 y Ada), se espera que una declaraciiln de procedi-

inicnto \e termine iiiedinnte sintaxis que iiicltiya el noinbre del procediinieiito. Por ejctrrplo. en Modula-2 un pnicediiiiiento se declara corno se muestra a coiitinuacih:

PROCEDURE P;

BEGIN

END P;

Advierta el i~so del iiotnbre de procedimiento P después del END de cierre. ;Se puedc verificar

un requeriniieiito así mediante rin analirador siiitáctico? Justifique su respuesta.

3.20 a. Escriba rmi expresión regular que genere el mismo lenguaje que la griirniítica ~iguierite:

b. Escriba una grmática iloe genere el m i m o lenguaje que la expresih regular quc se muestra a contitiuacicin:

3.21 I!m prnduccih simple es iina selección de regla grntnatical dc la forma A -t H, donde tanto

a. hlue9re que las prodricciones siinples pueden ser sistern6iicüntente elimittn<lai de una gra-

mAtica para eritregar una gra~iilitica siii pr«duccioiies sirtiples que genere el iiiismo lengua-

;e que la gramática origiiial. b. i,lísted esperaría que itpareciernti proclucciones siniplcs con freciicncia en la definición de

los lenguajes de pngrarnaeión? Jtistifique su respuesta. 3.22 Una grani6tica cíclica es aquella en la que existe una deriv:teión ,i -=.* A parti algún no

tcriiiinal A.

a. Mticstre que una grariiática cíclica es iimhigua.

b. i,Usled esper:iría que las gr:im:iticas que definen lenguajes de progr:im;icititi fueran a tnc- niido cíclicas'! JiistiEiquc su respuesta.

3.23 Vuelva a escribir la gniirxitic;~ de I'INY de l:i figura 3.6 en EHNF. 3.24 P:i¡.;i el prtigntnia TINY .

read x;

x := x+l;

write x

Page 146: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 3 1 GRAMATICAS LIBRES DE CONTEXTO Y A N ~ L I S I S IINTACTICO

a. Dibuje el árbol de anúlisis gramiitical de TINY. b. Dibuje el árbol sint&crico de TINY.

3.25 Dibuje cl árbol sintáctico de TINY paa el programa siguiente:

read u;

read v; ( entrada de dos enteros 1 if v = O then v := O ( no hacer nada 1

else

repeat

temp := v; v := u - u/v*v; u := temp

until v = O

end;

write u { salida gcd de u y v original )

NOTAS Y Gran parte de la teoría de las gramáticas libres de contexto se puede encontrar en Hopcroft

REFERENCIAS y Ullman [1979], donde se pueden hallar demostraciones de muchas propiedades, tales como la indeterminación de la ambigüedad inherente. También contiene una relación de la jerarquía de Chomsky. Información adicional está en Ginsburg 11966, 19751. Chomsky [ 1956. 19591 fue responsable de gran parte de la teoría inicial, la que aplicó al estudio de los lenguajes naturales. Sólo mucho después se comprendió la importancia del tema pa- ra los lenguajes de programación, y el primer uso de las gramáticas libres de contexto llegó en la definición de Algo160 (Naur [1963]). La solución de la página 121 para el prohlema del else ambiguo utilizando reglas libres de contexto, así como el ejercicio 3.9, se tomó de Aho, Hopcroft y Ullman [1986]. El enfoque de las gramáticas libres de contexto como ecua- ciones recursivas (sección 3.6.2) se tornó de la semántica denotacional. Para un estudio de la misma véase Schmidt [1986].

Page 147: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Capítulo 4

Análisis sinlaclito descendente

4.1 Análisis sintáctico descendente 4.4 Un analizador sintáctico mediante método descendente descendente recursivo recursivo para el lenguaje TlNY

4.2 Análisis sintáctico LL(I) 4.5 Recuperación de errores 4.3 Conjuntos Primero y Siguiente en analizadores sintácticos

descendentes

Un algoritmo de análisis sintáctico descendente analiza una cadena de tokens de entrada mediante la búsqueda de los pasos en una derivación por la izquierda. Un algoritmo así se denomina descendente debido a que el recorrido implicado del árbol de análisis gramatical es un recorrido de preorden y, de este modo, se presenta desde la raíz hacia las hojas (véase la sección 3.3 del capitulo 3). Los analizadores sintácticos descendentes existen en dos formas: analizadores sintácticos inversos o en reversa y analizadores sintácti- cos predictivos. Un analizador sintáctico predictivo intenta predecir la siguiente construc- ción en la cadena de entrada utilizando uno o más tokens de búsqueda por adelantado, mientras que un analizador sintáctico inverso intentará las diferentes posibitidades para un análisis sintáctico de la entrada, respaldando una cantidad arbitraria en la entrada si una posibilidad falla. Aunque los analizadores sintácticos inversos son más poderosos que los predictivos. también son mucho más lentos, ya que requieren una cantidad de tiempo ex- ponencia1 en general y, por consiguiente, son inadecuados para los compiladores prácticos. Aquí no estudiaremos los andizadores sintácticos inversos (pero revise la sección de notas y referencias y los ejercicios para unos cuantos apuntes y sugerencias sobre este tema).

Las dos clases de algoritmos de análisis sintáctico descendente que estudiaremos aquí se denominan análisis sintáctico descendente recursivo y anális3 sintáctico LL(1). El análisis sintáctico descendente recursivo ademhs de ser muy versátil. es el mé- todo más adecuado para un analizador sintácdco manuscrito, por lo tanto, lo estudiaremos primero. El análisis sintáctico LL(1) se abordará después, aunque ya no se usa a menudo en la práctica, es útil estudiarlo como un esquema simple con una pila explícita, y puede servirnos como un preludio para los algoritmos ascendentes más poderosos (pero tam- bién más complejos) del capítulo siguiente. Nos ayudará también al formalizar algunos de los problemas que aparecen en el modo descendente recursivo. El método de análisis sintáctico LL(I) debe su nombre a lo siguiente. La primera "L" se refiere al hecho de que se procesa la entrada de izquierda a derecha. del inglés "Left-right" (algunos analizadores sintácticos antiguos empleaban para procesar la entrada de derecha a izquierda, pero eso es poco habitual en la actualidad). La segunda "L" hace referencia al hecho de que rastrea una derivación por la izquierda para la cadena de entrada. El número I entre paréntesis sig- nifica que se utiliza sólo un símbolo de entrada para predecir la dirección del análisis sintác- tico. (También es posible tener análisis sintáctico LL(k), utilizando k símbolos de búsqueda hacia delante. Estudiaremos esto a grandes rasgos más adelante en el capítulo, pero un símbolo de búsqueda hacia delante es el caso mas común.)

Page 148: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 4 I ANALISIS IINTACTICO DESCENDENTE

Tanto el análisis sintáctico descendente recursivo como el análisis sintáctico LL(I) re- quieren en general del cálculo de conjuntos de búsqueda hacia delante que se conocen como conjuntos Primero y Siguiente.' Puesto que se pueden construir analizadores sin construir estos conjuntos de manera explicita, pospondremos un análisis de los mis- mos hasta que hayamos introducido los algoritmos básicos. Entonces continuaremos con un análisis de un analizador sintáctico de TlNY construido de manera descendente recur siva y cerraremos el capitulo ron una descripción de los métodos de recuperación de errores en el análisis sintáctico descendente.

4.1 ANALISIS SINTÁCTICO DESCENDENTE MEDIANTE MÉTODO DESCENDENTE RECURSIVO

411 El mitodo básico descendente recursivo La idea del análisis sintjctico descendente recursivo es muy simple. Observamos la regl gramatical para un no tenninal A como una definición para un procedimiento qlie reconocer una A. E l lado derecho de la regla gratiiaticaf para A especifica la estructura tiel código par este procedimierito: la secuencia de terminales y no teniiinales en una selección coirespond a concordancias de la entrada y llamadas a otros procedimientos, mientras que las seieccio- ries corresponden a Las alternativas (sentencias case o it) dentro del código.

Como primer ejemplo consideremos la gramática de expresicín del capítulo anterior:

exp --+ exp cipsrmn term / temz 0/7F11mU -t + / - term --+ terrn oprnultfuctor /factor opmutt -3 * jilctor -+ ( exp ) / número

y consideremos la regla gramatical para un,fizctor. Un procedimiento descendente recursivo que reconoce un fiictor (y al cual llamaremos por el mismo nombre) se pue& escribir ei pseudocódigo de la manera siguiente:

procedure factor ; begin

case token of ( : rncitch( ( ) ;

exp ; mutch( ) ) ;

n d e r : r n u t c h ( n d e r ) ;

else error ; end case ;

end fiwtor ;

En este pseudocódigo partimos del Supuesto de que existe tina variable token que mantiene el siguiente token actual en la entrada (de manera que este ejemplo utiliza un símbolo (le hús- queda hacia delante). 'Tanihién dituos por hecho que existe un procediniicnto mutch que compara el token siguiente aciual coi] su p;irárnetro, av:inza la entrada s i tiene éxito y declara un error s i no lo tiene:

Page 149: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Analisis iintactita destendente mediante método descendente returiiva

1'01- el riionierrto tlejareriios sin cspccil'icar el procc<Iiinietito cJrror que es 1l:itnado tanto por iririrdi e01i10 por fiictor. Se puede suporier que se iinprinle tin nieiiszijc (le error y se sale del progrma.

Advierta que err las Il;iiir;~d;is ire~ttr.)i( ( ) y i r i c r t~h (numi~er ) eri/;rctor, se sahe que I;is va- riables <'.xpri~irdTok~~ci y ~okcr i MI las n~istn;is. Sin criibnrgo. e11 l;~ Ilainadii nierrch() ). n o se p w d e suponer q w i i ~ k r n sea un pnrt%te\is derecho, de manera que es necesaria una prucha. El c6tligo parafiit,tor tanrh ih supoiie qiie se ha definido i i r i procediniicnto erp qtlc ptiedc Ilatn~rrlo. ti1 rin iinalimdor siiitáctico ilesccndente rectirsivo p:ira I;i gKintática tlc expr.esi6n el procediitiieriro evp IIarnari íi tcnn. el proccdiiriicnto terin llamará a,f¿ictor y el procctli- m ie i i t o , f i ~ i~ t i~ r Il;irna~-á a iJ,rp, de ~n;iner;\ que todoseestos procedimientos deben ser. capaces de 1laln:irse entre sí. i>e.;gr;1ciucta111e11tc. escribir prticc~iiiriietitits desceridenies recursivos para las reglas restantes en la gr:irnática ilc cxpresi6n no es l a t i E c i l coriio para)rr.ror y requiere el iiso de ERNF. como verenros a eorrtinriacicíti.

41.2 Repetición y selección: el uso de EBNF C:orisiilcre coino un segundo cjcinplo la regla griiniatical isitnplificad:~) para una witencia if:

l h o se puede traducir c ~ i c l pr«cediniiento

Page 150: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

que ;i la BNF, donde los corchetes de la EBNF se traducen en una prueba en el c6digo para Serit-if De hecho, la notación EBNF está diseñada para reflejar muy de cerca el código real ile un analizador sintáctico descendente recursivo, de modo que una gran~cítica debería siem- pre traducirse en EBNF si se está utilizando el modo descendente recursivo. Advierta tanibieui que, aun cuando esta gramática es ambigua (véase el capítulo anterior), es natural escribir un analizador sintáctico que haga concordar cada token else tan pronto como se encuen- tre en la entrada. Esto corresponde precisamente a la regla de eliminación de ~imbigiiedad incís cercanamente anidada.

Considere ahora el caso de una exp en la gramática para expresiones aritméticas simples en BNF:

exp -t e.up opsirmcr renn / term

Si estamos intentando convertir esto en un procedimiento exp recursivo de acuerdo con nuestro plan, 10 primero que potlemos intentar hacer es llamar a e-rp niismo, y esto conducida a un inmediato ciclo recursivo infinito. Tratar de probar cuál selección usar fexp =t rrxp op- .suma term, o bien, u p -. terrn) es realmente problemático, ya que tanto e.q como terrn pue- den comenzar con los misnios tokens (un número o paréntesis izquierdo).

La solución es utilizar la regla de EBNF

cxp =t term { opsumn terrn )

en su lugar. Las llaves expresan la repetjción que se puede traducir al código para un ciclo o bucle de la manera siguiente:

procedure e.xp ; hegin

term ; while tokrrz = + or token = - do

tnntch (toketz) ; terrn ;

end while ; end exp ;

De inanera similar, la regla EBNF para terrn:

terrn -+ factor { opmultjactor ]

se convierte en el código

procedure terrn ; hegin

factor ; while token = * do

rncitch (token) ; fiictor ;

end while ; end terrn ;

Page 151: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Análisis sintáctico descendente mediante método descendente recursivo 147

Aquí eliminamos los nu tcrniinales o[~,siíirrcr y o p ~ i r l t conio procetliniientos separados, ya que su única función es igiialar a los operadores:

En su lugar haremos esta concord;tncia dentro dc'e.rp y t'rm.

Una cuestión que surge con este código es s i la asociatividad izquierda iniplicada por las llaves (y explicita en la BNF origiiial) todavía puede rnanlenerse. Suponga, por ejemplo, que deseamos escribir una calculadora descendente recursiva para la arit,niZtica entera simple de nuestra gramitica. Podemos asegurarnos de que las operaciones son wociativas por la izquierda realizando las operaciones a medida que circularitos a través del ciclo o bticle (suponemos ahora que los procedimientos de anilisis sintictic« son funciones q~te devnel- ven un resultado entero):

function exp : rrzte,qer ; var ternp : iriteqer ; be&

trtnp : = tern~ ; while token = + or tokrn = - do

case token of + : rrratt h (+) ;

terrrp : = terflp + trrrn ; - : r~ziztch (-) ;

tetirp : = teinp - terrrz ; end case ;

end while ; return terrip ;

end exJJ ;

y de manera similar para teirn. Empleanios estas ideas para crear una calculadora siniplc funcionando, cuyo código en C está dado en la figura 4. t (páginas 148-149), donde en lu- gar de escribir un analizador léxico completo, optamos por utilizar llarnadas a getchar y scanf en lugar de un procedimiento getToken.

Este método de convertir reglas gramaticales en EBNF a código es rriuy poderoso. y lo utilizaremos para proporcionar un analirador sintáctico completo para e l lenguaje TINY de la sección 4.4. Sin einbargo, existen algunas dificultades y debemos tener cuiciado al progra mar las acciones dentro del código. Un ejemplo de esto está en el pseudocódigo anterior para ctp, donde tiene que ocumr una concordancia de las operaciones antes de las llamadas repe- tidas a term (de otro modo term vena una operaciijn como su primer token, lo que generaría un error). En realidad, el protocolo siguiente para conservar la variable global rokerl actual debe estar rígidamente adherido: token debe estar eslablecido antes que comience el análisis sintáctico y getTokert (o su equivalente) debe ser Ilainado precisamente dcspués.de haber pro- bado con éxito un token (ponemos esto en el procedimiento »iutclz eli el pseudocódigo).

También debe tenerse c l misino cuidado al programar las acciones durante la construc- ción de un irbol sintictico. Viiiios que la asociatividad por la izquierda en tina EBNF con repetición puede rnanlenerse para propósitos de cálculo al realizar Zstc ;i nicdida que sc ejecuta el ciclo.

Page 152: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 4.1 í iniclo) Una calculadora descendente recurriva para aritmética

entera simple

Page 153: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Anal~s~s rintáct~co descendente med~ante metodo descendente recursivo 149

Page 154: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

. .

Sin embargo, esto ya no corresponde a una construcción descendente del árbol gramati- cal o sintáctico. En realidad, si consideramos la expresión 3+4+5, con árbol sintáctico ~j

+

el nodo que representa la suma de 3 y 3 debe ser creado (o procesado) antes del nodo raíz (,el ,:

nodo que representa su suma con 5). Al traducir esto a la construcción de árbol sintáctico real, :

tenemos el siguiente pseudocódigo para el procedimiento exp:

function exp : ~yntc~xfiee ; var temp, newtenzp : sqritmTree ; begin

temp := term ; white taken = + or toXen = - do

case token of + : match (+) ;

newtenzp := rnakeOpNode(+) ; /l-fiChild(newtrmp) : = temp ; r~ghtClzrld(ne~utrn~p) : = term ; temp := newternp ;

- : match (-) ; newtemp : = makeOpNode(-) ; IeflClzild(ncw.temp) : = remp ; rightChild(newtemp) := trrm ; temp : = newtemp ;

end case ; end while ; return temp ;

end exp ;

o la versión más simple

function eifp : syntaliTree ; var trmp, newtenzp : syntaTree ; begin

t m p : = term ; while token = + or token = - do

newtemp : = ~nukeOpNode(token) ; match (token) ; lLlfiClziltl(t~e>vten~p) : = temp ; I ~qht('/iilrl(nrwte~np) : = t e n ; fetnp : = t)~ewtemp ;

end while ; return trmp ;

end ~ i p ;

Page 155: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Análtsis sintáctico descendente medtante método descendente recursivo 151

En este código utilizainos una nueva función nrnkeOpNod', que recibe un token de opera- dor como un piirámetro y devuelve un nodo de árbol sintáctico recién construido. También indicamos la asignación de un árbol sintáctico p como un hijo a la izquierdas a la derecha de un árbol sintáctico t al escribir lefiCltild(t) := p o riglztChild(t) : = p. Con este pseudo- código el procedimiento mp en realidad construye el árhol sintictico y no el á r h l de análisis gramatical. Esto se debe a que una llamada a e.xp no construye invariableniente un nuevo nodo del árbol; si no hay operadores, exp sinlplemente pasa de regreso al árbol recibido desde la Ilaniada inicial a term como su propio valor. También se puede escribir el pseudocódigo co- l~espondiente para tcrm y , / ¿ ~ t o r (véame los ejercicios).

En contraste, el &bol sintictico para una sentencia if se puede construir de manera es- trictarnente descendente por medio de un analizador sintáctico descendente recursivo:

function if'Stutetnent : syntuxTwe ; var temp : .syntd'ree ; begin

rnatch (i f ) ; match ( ( ) ; temp := mctkeSttntNotlc(if) ; te.rtChikd(tcnt[~) : = exp ; inutch ( ) ) ; thenChi/c(temp) : = stutetnewt ; if token = else then

~ m t c h (else) ; elseChi/d(tetnp) : = .stutenterit ;

else elseCl~ild(ternp) : = ni1 ;

end if ; end i'fStatentent ;

La flexibilidad del análisis sintáctico descendente recursivo, al permitir al programador ajustar La programación de las acciones, 10 hace el mitodo de elección para analizadores sin- tácticos generados a mano.

41.3 Otros problemas de decisión El método descendente recursivo que describimos es muy potleroso, pero todavía es de propj- sito específico. Con un lenguaje pequeño cuidadosamente diseñado (tal como TINY o incluso C), estos métodos son adecuados para construir un malizador sintáctico completo. Dehedarnos estar conscientes de que pueden ser necesarios más métodos formales en situaciones com- plejas. Pueden surgir diversos problemas. En primer lugar, puede ser difícil convertir una gramática originalmente escrita en BNF en forma EBNF. Una alternativa para el uso de la EBNF se estudia en la siguiente sección, donde se construye una BNF transformada que en esencia es equivalente a la EBNF. En segundo lugar, cuando se formula una prueba para distinguir dos o niás opciones de regla gramatical

puede ser difícil decidir cuándo utilizar la selección A + w y cuándo emplear la selección ,;\ -t 0, si tanto a como p comienzan con no terminales. Un problema de decisión de esta naturaleza requiere el cálculo de los conjuntos Primero de u y P: e1 conjunto de tokcns que pueden iniciar legalmenle cada cadena. Formalizaretnos los detalles de este cálculo en la secciOn 3.3. En tercer lugar. 31 escribir el código para una protluccicín c:

Page 156: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

puede ser necesario conocer cuáles tokens pueden aparecer legalmente después del no ter- minal A, puesto que tales tokens indican que A puede desaparecer de manera apn~piada en este punto del análisis sintáctico. Este conjunto se llama conjunto Siguiente de A. El cálculo de este conjunto también se precisará en la sección 4.3.

Una inquietud tinal que puede requerir el cálculo de los conjuntos Primero y Siguiente es la detección temprana de errores. Considere, por ejemplo, el programa calculadora de la figura 4.1. Dada la entrada ) 3 -2 ), el analizador sintáctico descenderá desde exg hasta term has- ta factor antes de que se informe de un error, mientras que se podría declarar el error ya en exg, puesto que un paréntesis derecho no es un primer carácter legal en una expresión. El con,junto Primero de exp nos diría esto, lo que permitiría uua detección m& temprana del error. (La detección y recuperación de errores se analiza con más detalle al final del capítulo.)

42.1 El método básico del análisis sintáctico LL(1) El análisis sintáctico LL(1) utiliza una pila explícita en vez de llamadas recursivas para efec- tuar un análisis sintáctico. Es títil representar esta pila de manera estándar, de manera que las acciones de un analizador sintáctico LL(1) se puedan visualizar rápida y lijcilmente. En este análisis a manen de introducción usaremos la gramática simple que genera cadenas de pa- réntesis halanceados:

(véase el ejemplo 3.5 del capítulo anterior). La tabla 4.1 muestra las acciones de un analizador sintáctico descendente dada esta gra-

mática y la cadena ( ) . En esta tabla hay cuatro columnas. La primera columna numera los pasos para una referencia poslerior, en tanto que la segunda muestra el contenido de la pila de análisis sintáctico, con la parte inferior de &a hacia la izquierda y la parte superior hacia la derecha. Marcamos la parte inferior de la pila con un signo monetario ($). De este modo, una pila que contenga el no terminal S en la parte superior aparece como

y los elementos adicionales de la pila 5on empujados a la derecha. La tercera columna de la tabla 4.1 muestra la entrada. Los ~ímbolos de entrada están enumerados de i~quierda a de- recha. Se utili~a un signo monetario para marcar el final de la entrada [esto corresponde a un token de EOE (por sus siglas en inglés) o fin de archivo, generado por un analizador Iéxic La cuarta columna de la tahla proporciona una descripción abreviada de la acción toma por el analizador ~intáctico, lo cual cambiartí la pila y (posiblemente) la entrada, como se in- drca en el siguiente renglón de la tabla.

Tabla 3 1 Acciones de- anilisis rintáctico Pila de análisis

de un analizador sintáctico Entrada Acción

rintactico descendente I $ S O $ S - t ( S ) S 2 $ S S ( O match (concordar) 3 $ S ) S ) a; S+ 8 1 $ 5 ) ) 0, niatch (conc«rdar)

5 $ S S S -+ e

0 $ % :iwptar

Page 157: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Analiris iintactico LL(I) 153

Un rinalizador sintáctico descendente comienza al insertar el símbolo inicial sohre la pila. Acepta una cadena de entrada si, después de una serie de acciones. la pila y la entrada .;e que- dan vacías. De este ~iiodo, un esquema general para realizar con 6xito un análisis siutácieticti descendente es

$ $ aceptar

En el caso de nuestro ejeniplo. e1 símbolo inicial es S, y la cadena de entrada es ( ) . Un analizador sintáctico descendente realiza su análisis al reemplazar un no terniinal en

la pane superior de la pila por una de las seleccioiies en la regla gramatical (en BNF) para ese no terminal. Realiza esto con miras a producir el tokeii de enímh actual en la parte su- perior de la pila de análisis sintáctico, de donde ha reconocido el token de entrada y puede descartarlo tanto de la pila como de la entrada. Estas dos acciones,

l. Reemplazar un no terminal A en la parte superior de la pila mediante una. cadena a utilizando la selección de regla gramatical A -+ a, y

2. Hacer concordar un token en la parte superior de la pila con el siguiente tokeri de entrada,

son las dos acciones básicas en un analizador sinthctico descendente. La primera acción, que podría ser llamada generar, la indicümos al escribir la seleccicín de RNF utilizaika en el reemplazo (cuyo lado izquierdo debe ser el no terniinal que actualmente esrá en la parte su- perior de la pila). La segunda acción iguala o lrace concordar un token en la parte superior de la pila con el siguiente token en la entratla (y los desecha a anibos sacáitd«los de la pila y avanzando a la entrada); indicamos esta acción escribiendo la palabra rncitch (conrorri~ir). Es importante advertir que en la acción ,yerzerc~r, la cadena de reemplazo cx de la BNF se debe insertar en rrvrrrci en la pila,(y;i que eso asegura que la cadena a asrihará a la parte superior de la pila en el orden de izquierda a derecha).

Por ejemplo, en el paso l del análisis sintác~ico en la tabla 4.1. la pila y la entrada son

$ S O $

y la regla que usamos para reemplazar S en la parte superior de la pila es S -+ ( S ) S, de manera que la cadena S ) S ( se inserta en la pila para obtener

$ S ) S ( ()F.

Ahora generamos la siguiente terminal de entrada, a saber, un paréntesis izquierdo, en la parte superior de la pila, y efectuamos una acción de concordur o rnatch para obtener la si- tuación siguiente:

3 S ) S ) $

La lista de acciones generadas en la tabla 4. I corresponde precisamente a los pasos de una derivación por la izquierda de la cadena ( ) :

S-. ( 3 s [ S - + ( S ) S ]

;. ( ) S [S+e l

" o [.Y + el

Estci es caracteristico del análisis siritáctico desccntlente. Si queremos construir un árbol de análisis gramatical a inedKIü que iivanra el ~u~i l is i . ; sintáctico, podemos agregar acciones de construcción de nodo a ~nieditla que cad:i no termitial o tcrinirial se inscrta en la pila. De es- ie rnotio, cl i i o h raí l dcl 2rhol di- ;it~:ilisis gro~nntic;il (correspondiente al sítnhol« iriiciali se cimstriiyc a1 principio ilci ;in:ili4.; \i~ir;ictico. Y ~;inthiC~i i-n cl p;iw 2 de 1;i iahla 1. l . cti;tntlo

Page 158: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP 4 1 ANÁLISIS SINTÁCTICO DESCENDENTE

S se reemplaza por ( S ) S, 10s nodos para cada uno de los cuatro símbolos de reemplazo s constmyen a medida que se insertan los símbolos en la pila y se conectan como hijos respect al nodo de S que reemplazan en la pila. Para hacer esto efectivo tenemos que modificar la pil de manera que contenga apuntadores para estos nodos construidos, en vez de simplemente 1 no terminales o terminales mismos. Veremos también cómo se puede modificar este proces para producir una construcción del árbol sintáctico en lugar del ,ubol de análisis gramatical.

4.2.2 El algoritmo y tabla del análisis sintáctico LL(1) Al utilizar el método de análisis sintáctico apenas descrito, cuando un no terminal A está la parte superior de la pila de análisis sintáctico, debe tomarse una decisión, basada en el ken de entrada actual (la búsqueda hacia adelante), que selecciona la regla gramatical que va a utilizar para A cuando se reemplaza A en la pila. En contraste, no es necesario tomar u decisión cuando un token está en la parte superior de la pila, puesto que es el mismo que token de entrada actual, y se presenta una concordancia, o no lo es, y se presenta un error.

Podemos expresar las selecciones poslbles construyendo una tabla de análisis siniáctico LL(1). Una tabla de esa naturaleza es esencialmente un arreglo bidimensional indizado p no terminales y terminales que contienen opciones de producción a emplear en el paso piado del análisis sintáctico (incluyendo el signo monetario $ para repreientar el final entrada). Llamamos a esta tabla M[N, T]. Aquí N es el conjunto de no terminales de la gr tica, Tes el conjunto de terminales o tokens (por conveniencia, eliminamos el hecho de que F debe agregar $ a 7) y M ?e puede imaginar como la tabla de "movimientos". Suponemos qu la tabla M[N, ?1 inicia con todas sus entradas vacías. Cualquier entrada que permaneLca vaci después de la constnicción representa errores potenciales que se pueden presentar durante u análisis sintáctico.

Agregamos selecciones u opciones de producción a esta tabla de acuerdo con las regl siguientes:

1. Si A -+ u es una opción de producción, y existe una denvac~ón a ** a p, donde ri.

es un token, entonces se agrega A -+ u a la entrada de tabla M[A, u]. 2. Si A -+ u es una opción de producción, y existen derivaciones a ** e y S $ =+* f3

A u y, donde S es el símbolo inicial y u es un token (o $), entonces se agrega A -+ a a la entrada de tabla M[A, a].

La idea dentro de estas reglas es la siguiente. En la regla 1, dado un token u en la entra- da, deseamos seleccionar una regla A -+ a si u puede producir una u para comparar. En la regla 2, si A deriva la cadena vacía (vía A -+ a), y si u es un token que puede venir legal- mente después de A en una derivación, entonces deseamos ~eleccionar A -+ u para hae que A desaparezca. Advierta que un caso especial de la regla 2 ocurre cuando u = u.

Estas reglas son difíciles de implementar de manera directa, pero en la siguiente seceió desarrollaremos algoritmos que permitan hacerlo así, involucrando a los conjuntos Primero y Siguiente ya mencionados. Sin embargo, en casos muy simples, estas reglas se pueden rea- lizar a mano.

Considere como primer ejemplo la gramática para paréntesis balanceados utilizada en la wbsección anterior, hay un no terminal (S), tres tokens (paréntesis i~quierdo, paréntesis de- recho y $) y dos opciones de producción. Puesto que hay sólo una pr&ducción no vacía para S, a saber, S -+ ( S ) S, cada cadena derivable de S debe ser vacía, o bien, comenzar con un paréntesis izquierdo, y esta opción de producción se agrega a la entrada M[S, ( 1 (y sólo ;iIIí). Esto completa todos los casos bajo la regla l . Ya que S * ( S ) S , la regla 2 se aplica con u = e, B = (, A = S, u = ) y y = S $, de rnancra que S + E se agrega a M[S, )J. Puesto que S $ =1V S (la derivación vacía), S -+ e también se agrega a MIS, $1. Esto conipleta la

Page 159: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

construcción de la tabla de análisis sintáctico LL(I), la que podemos escribir de la manera siguiente:

Para completar el algoritmo de análisis sintáctico LL(I), esta tabla debe proporcionar op- ciones únicas para cada par de no terminal y token: De este modo, establecenius la siguiente

Figura 4.2 Algoritmo de análisis rintáctico LL(1) basado en tabla

Una gramática es una gramática I,L(l) si la tabla de análisis sintáctico LL(1) asociada tie- ne como máximo una producción en cada entrada de la tabla.

Una gramática LL(1) no puede ser ambigua, porque la definición implica que un ana- lizador sintáctico no ambiguo se puede construir empleando la tabla de análisis sintáctico LL(I). En realidad, dada una gramática LL(I), en la figura 4.2 se proporciona un algorit- mo (le análisis sintáctico que utiliza la tabla de análisis sintáctico LL(1). Este algoritmo produce precisamente las acciones descritas en el ejemplo de la subsección anterior.

Mientras que el algoritmo de la f igur~ 4.2 requiere que las entradas de la tabla de análi- sis sintáctico tengan conlo máximo una producción cada una, se pueden construir reglas de no ambigüedad en la construcción de la tabla para casos ambiguos simples tales como el problema del else ambiguo, de una manera similar a la descendente recursiva.

Page 160: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

IAP. 4 1 ANALISIS SINTÁCTICO DESCENDENTE

Considere, por ejemplo, la gramática simplificada de las sentencias if (véase el ejemplo 3.6 del capítulo 3):

sentencia -+ sent-if / otro senr-if+ if ( exp )sentencia parte-else parte-else + else sentencia / E

e.rp -+ O / 1 La construcción de la tabla de análisis sintáctico LL(1) da el resultado que se niuestra en la tabla 4.2, donde no enumeramos los terminales de paréntesis ( o ) en la tabla, ya que no producen ninguna acción. (La construcción de esta tabla \e explicará en forma detallada en la ~iguiente sección.)

En la tabla 4.2, la entrada Mrpcirte-else, else] contiene dos entradas, correspondientes a la ambigüedad del else. Como en el caso descendente recursivo, al construir esta tabla poúría- mos aplicar una regla de eliminación de ambigüedad que siempre preferiría la regla que genera el token de búsqueda hacia delante actual sobre cualquier otra, y, por consiguiente, se preferi- ría la producción

Tabla 4.2

parte-else -+ else sentencia

Tabla de análisis sintáctico M[N, T I LL(1) para sentencias if

.sentencia (ambiguas)

sent-if

parteelse

exp

en vez de la producción parte-else -+ E. Esto de hecho corresponde a la regla de eliminación de ambigüedad más cercanamente anidada. Con esta modificación la tabla 4.2 se hace no ambigua, y la gramática se puede analizar sintácticamente como ~i fuera una gramática LL(1). Por ejemplo, la tabla 4.3 muestra las acciones de análisis htáctico del algoritmo de análisis sintáctico LL(l), dada la cadena

if ( 0 ) if (1) otro else otro

if

sentencia --+ sent-if

sent-if+ if ( exp )

sentencia parte-else

(Para ser más concisos utiliramos las siguientes abreviaturas en esa figura: sentencia = S, .seni-if = I, purte-e1.w = L,, exp = E, if - i, else = e, otro = o.)

otro

sentencia -+ otro

else

parte-else -t else

sentencia purte else -, e

$

panee1.w - + E

O

exp -+ O

1

exp -+ 1

Page 161: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tabla 4.3 Aaianer de análisis

sintáctico LL( 1) para IentPntiar if utilizando la regla de eliminación anidada de ambigüedad

mas próxima

Pila de análisis sintáctico Entrada Acción -

$ .S i ( 0 ) i f l ) o e o ' G S 9 1 á 1 i ( O ) i ( l ) o e o $ l + i ( E ) Sl. $ l . S ) E ( i i ( O ) i ( l ) o e o F concornla (match) $ L . S ) E ( ( O ) i ( l ) o e o $ concuerda (match) FLS E O ) i ( l ) o e o $ E-+ O

$ ¿ S ) O O ) i ( l ) o e o$ concuerila (match) $ ¿ S ) ) i ( l ) o e o $ concuerda (match) L S i ( l ) o e o $ S-. 1 X l. 1 i ( l ) o e o $ 1 - + i ( E ) SL S L L S ) E ( i i ( l ) o e o $ 1 - - i ( E ) S L $Ll.S) E ( i i ( l ) o e o$ c«ncner&a (mttch) SL,LS)E( (1)oeo'G concucrtla (~i~atch) SL1.S) E l ) o e o $ E + 1 $ L 1- S ) ) o e o $ concuerda (match) ' G L L S oe o$ S -- o Y l. L o o e o$ concuerda (match) 'j L 1, e o$ L-eS $LSe e o$ concuerda (match) $ L S o 'j S -+ o $ L o O a concuerda (match) 6 L $ L + o

$ aceptar (accepr)

4.U Eliminación de recursión por la izquierda y factorización por la izquierda En la repetición y seleccicín en el análisis sintáctico L L ( I ) se presentan prohlemas similares a los que surgen en el análisis sintáctico descendente recursivo, por esa r ~ z ó n aún no hemos podido dar una tabla de ;inálisis sintáctico LL( I ) para la gramitica de expresión aritmética simple de secciones anteriores. Resolvimos estos problenias para descendente recursivo uti- lizando notacicín EBNF. No podemos aplicar las mismas ideas al análisis sintáctico LL(L); en vez de eso debemos volver a escribir la gramática dentro de la notación BNF en una forma que el algoritmo de análisis sintáctico LL( 1) pueda aceptar. Las dos técnicas estándar que aplicamos son la eliminación de recursión por la i~quierda y la factorización por la izquierda. Consideraremos cada una de estas técnicas. Debe hacerse énfasis en que nada garantiza que la aplicación de estas técnicas convertirá una gramática en una gramática LL(l), del m i m o modo que EBNF no garantizaba resolver todos los problemas al escribir un analirador sintáctico descendente recursivo. No ohstante, son muy útiles en la niayoría de las situaciones prácticas y tienen la venta,ja de que sus aplicaciones se pueilen autoniatizar, de rnanera que, suponiendo un resultado con éxito, se pueden irtilizar para generar a~it<tmá- ticarnente un analizador sintáctico LL( 1) (véase la seccicín de notas y referencias).

Eliminación de recunión por la izquierda La recursión por la izquierda se utiliza por lo regu- lar para hacer operaciones :isociativas por la i/,quicrda, como en la grarnitica de expresión siniple, donde

Page 162: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejemplo 4.1

CAP. 4 1 ANALISIS SINTACTICO DESCENDENTE

hace las operaciones representadas por (p.suma asociativas por la izquierda. Éste es el caso rnás simple de recursión por la izquierda, donde hay sólo una opción de producción recu siva por la izquierda simple. Otro caso ligeramente rnás complejo es aquel en que más d una opción es recursiva por la izquierda, lo que pasa si escribimos opsuma:

exp -+ exp + term / exp - term / terrn

Estos dos casos involucran la recursión por la izquierda inmediata, donde la recursión se presenta sólo dentro de la producción de un no terminal simple (como exp). Un caso más di- fícil es la recursión por la izquierda indirecta, tal como en las reglas

A - + B ~ / . . . B - - + A a I . . .

Tales reglas casi nunca se presentan en gramáticas de lenguajes de programación reales. pero incluiremos una solución para este caso con el fin de completar la exposición. Consideraremos primero la recursión por la izquierda inmediata.

CASO 1: Kecursión En este caso la recursión por la izquierda se presenta s61o en reglas gramaticales de la forma por la izquierda inmediata simple ~ - + ~ u / p

donde u y (?, son cadenas de terminales y no terminales y P no comienza con A. En la sec- ción 3.2.3 vimos que estas reglas gramaticales generan cadenas de la forma Bu", para n 2 O. La selección A -+ p es el caso base, mientras que A -+ A a es el caso recursivo.

Para eliminar la recursión por la izquierda volvemos a escribir estas reglas gramaticales divididas en dos: una que primero genera (3 y otra que genera las repeticiones de u utilizando recursión por la derecha en vez de recursión por la izquierda:

A - t p A ' A ' - . + ~ A ' / E

Considere de nueva cuenta la regla recursiva izquierda de la gramática de expresión ~imple: .1 exp -+ e.rp opsuma tenn / term I

La forma de esta regla es A -+ A u / P, con A = exp, u = opsuma ternz y f3 = term. Al vol- verla a ewibir para eliminar la recursión por la izquierda obtenemos que

e.rp 4 term exp' expt -+ opsuma term exp' / E

CASO 2: Recursión Éste es el caso en que tenemos producciones de la forma Ior la izquierda nmediata general i i - + ~ ~ , / l i ~ z l . . . ~ A u ~ / P ~ ~ P ~ / . . . ~ P , , ,

Page 163: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Análisis s~ntáct~to LL(I) 159

Ejemplo 4.2

doiide ninguna de las Pl, . . . , $,, coniien~iin con ,l. En este caso In solución es seniejante a la tlel caso simple, con las opciones extendidas en consecuencia:

Considere la regla gramatical

Eliminemos la recursifin por la i~cltiicrda de la nianera siguiente:

uxp rp terrn rxp' etpr rp + term eup' / - trrtn a p ' / E §

CASO 3: Reiursión El algoritnio que aquí describimos está garantizado para funcionar .;filo en el caso de gr:i- por la izquierda nit tic as sin produccioues E y sin ciclos, donde un cielo es una derivación de por lo menos general un paso que comienza y fir~alira con el mismo no terminal: A * LY -J* A. ES casi seguro que un

ciclo ocasionará que un analizador sintáctico entre en un ciclo infinito, y las gramáticas con ciclos nunca aparecen como gramáticas de lenguajes de programación. Estas gramá- ticas tienen producciones E , pero por Lo regulx en formas muy restringidas, de modo que es- te algoritmo casi siempre funcionará también para estas gramáticas.

E l algoritm« funciona al elegir un oden arbitrario para todas los no terminales del len- guaje, digamos, A,, . . . , A,,,, y posteriormente eliminar todas las recursiones izquiertlas que 110 incrementen el índice de las A,. Esto elimina to&s Las reglas de la forma Ai + Ai y con j 5 1. Si hacemos esto para cada i desde I hastam, entonces ningún ciclo recursivo puede ser izquierdo, puesto que cada paso en un ciclo así sfiio incrementaría el índice, y de este m& no se podría volver a alcanzar el índice original. E l algoritmo se proporciona detalladamen- te en la figura 4.3.

F ~ q u r a 4 1 for i := 1 tom do

Rlgor~tmo para ehminaoón forj := 1 to i- 1 do de retundn por la aquierda reemplazar cnda selección de regla grunzuticul de la f o n o A, + Al P por la regla general A , + c u l p I u Z P / . . . / u~@,don<leA,!,-tu,/cw,/ . . . / ukes

la reglcz actziul puro A,

Ejemplo 4.3 Conídere la gramática \iguiente:

(Esta gramática es couiplet~irrientc artit'icid, ya que esta situacicín rto se presenta en ningítri Iciigu;tje de programación e&r<lar.)

Page 164: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 4.4 Gramática de expresión aritmética simple con recunión por la izquierda eliminada

CAP. 4 1 ANALISIS SINTÁCTICO DESCENDENTE

Consideraremos que A tiene un número más alto que A para propósitos del algoritmo (es decir, A l = A y A2 = B). Como n = 2, el ciclo externo del algoritmo de la ftgura 4.3 se e,jecuta dos veces, una vez para i = 1 y una vez para i = 2. Cuando i = 1, el ciclo interno (con índice j ) no se ejecuta, de modo que la única acción es eliminar la recursión por la iz- quierda inmediata de A. La gramática resultante es

A + B n A 1 / c A ' A 1 + a A ' / E

~ + ~ b / ~ b / d

Allora el ciclo externo se ejecuta para i = 2, y el ciclo interno se ejecuta una vez, con j = 1 . En este caso eliminamos la regla B -+ A b reemplazando i\ con sus opciones de la prime regla. De este modo obtenemos la gramática

A + B a A r / c . 4 ' A ' + r i A ' / E

B + B ~ / B u A ' ~ / c A ' ~ / ~

Finalmente, eliminamos la recursión por la izquierda inmediata de B para obtener

A + B u i t ' / c ~ ' A ' + a A t l c B + c A ' b B f / d B ' B 1 + b B ' / a A ' b ~ ' / ~

Esta gramática no tiene recursión por la izquierda.

La eliminación de la recursión por la izquierda no cambia el lenguaje que se está reeo nociendo, pero modifica la gramática y, en consecuencia, también los iúboles de análisis gramatical. En realidad, este cambio ocasiona una complicación para el analizador sintáctico (y para el disefiador del analizador). Considere, por ejemplo, la gramática de expresión simple que hemos estado utilizando corno ejemplo estándar. Como vimos, la gramática es recursiva por la izquierda para expresar la asociatividad por la izquierda de las operaciones. Si elimin mos la recursión por la izquierda inmediata como en el ejemplo 4.1, obtenemos la gramática dada en la figura 4.4.

exp 4 f e m exp' exp' 4 opsumu term exp' / E

opsumu -+ + 1 - ferm -t factor term' term' + oprnulf factor ferni' / e opmult + * factor + ( exp ) / número

Ahora considere el árbol de análisis graniatical para la expresión 3+4+5:

Page 165: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Analiris sintáctico L L ( l )

número (4)

i número

( 5 )

Este árbol ya no expresa la asociatividad izquierda de la resta. No obstante, un analizador sintáctico todavía construiría el drbol sintáctico apropiado asociativo por la izquierda:

No es ccimpletamente trivial hacer esto utilizando la nueva gramática. Para ver cómo, con- sidere en su lugar la cuestión algo más simple de calcular el valor de la expresión utiliran- do el hbol de ctnálisis gramatical diido. Para hacer esto, se debe pasar el valor 3 desde el nodo raíz errp hasta su hijo a la derecha exp'. Este nodo cxp' debe entonces restar 4 y pasar el nuevo valor - 1 hacia .su hijo de extrema derecha (otro e.rpl). Este nodo exp' a su vez debe restar 5 y pasar el valor -6 hacia el nodo clup' final. Este nodo tiene únicamente un hijo E

y simplemente pasa el valor -6 de regreso. Este valor se devuelve entonces hacia la parte supetior del árbol, al nodo raíz e.rp y es el valor final de la expresión.

Considere cómo podría funcionar esto en un analizador sintácticodescendente recursi- vo. La gramática con esta recursión por la izquierda elirnin;i&a daría origen a los procedi- mientos e.rp y exp' de la manera siguiente:

procedure rxp ; hegin

ternz ; prp' :

end et;p ;

Page 166: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

procedure exp' ; begin

case token of t : mutch ( t ) ;

trrm ; exp' ;

- : niutch (-) ; term ; esp' ;

end case ; end e.rpf ;

Para obtener esos procedimientos que realmente permiten calcular el valor de la expresió volveríamos a escribirlos de la manera siguiente:

function exp : integer ; var temp : inteyer ; begin

tenzp : = term ; return e,pl( temp) ;

end exp ;

function exp' ( lialsofar : integer ) : integer ; begin

if token = + or token = - then case token of

+ : mutch ( t ) ; wls&r : = vulsofar + ternz ;

- : mutch (-) ; vulsofar : = valsofur - term ;

end caie ; return exp'(va1sofar) ;

else return vcilsofur ; end exp' ;

Advierta cómo el procedimiento exp' ahora necesita un parámetro pasado desde el proce- dimiento e.rp. Una situación semejante se presenta si estos procedimientos están por devol- ver un árbol 5intáctico (asociativo por la izquierda). E1 código que dimos en la ~ección 4.1 empleaba una nolución más rimple basada en EBNF, los cuales no requieren el parámetro extra.

Finalmente, notamos que la nueva gramática de expresión de la tigura 4.4 es en reali- dad una gramática LL(1). La tabla de análinis sintáctico LL(1) se proporciona en la tabla 4.4. Como con las tablas anteriore5, regresaremos a su construcción en la sección siguiente.

factorización por la izquierda La factorización por la izquierda se requiere cuando dos o más opciones de reglas gramaticales comprirten una cadeha de prefijo coniún, como en la regla

Page 167: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Analisis sintácttto k(I)

Talila 4 4 Tabla de anallsis stntactito M[N, T I

L L ( I ) para la gramat~ca - "\/)

de la figura 4.4

<'Y(,'

( número ) +

fu< tor -, fiic tor + ( eip ) número

Ejemplos de esto son las.reglas recursivas por la derecha para secuencias de sentencias (ejemplo 3.7 del capítulo 3):

y la siguiente versiítn de la sentencia if:

.sent-(f+ i f ( exp ) sentencia / i f ( exp ) sentencici e l se sentencici

Obviamente, un analizador sintáctico LL(1) no puede distinguir entre las opciones de pro- ducción en una situaci6n de esta clase. La solución en este caso simale es "factorizar" la n por la izquierda y volver a ehcribir la regla como dos reglas

(Si querernos utilizilr paréntesis como metasímbolos en reglas gramaticales, también podría- mos escribir A -+ n (@ 1 y), lo que se parece mucho a la factorizaci6n en witrnética.) Pxa que la t'actorizaciítn por la inluiertla trabaje atlecuatlamente, debemos estar seguros de que cu es de hecho I:I cxlena más larga compartida por los lados derechos. Esto tainbiéii es po- sihle cuando hay más de dos opciones que compartan un prefijo. Estahlecem»s el. algoritmo general cn la iigura 4.5 y entonces lo aplicamos a v;trios ejemplos. ,Advierta que a medida que cl algoritmo continh. el ttúmero de tipciiines de prodirccitín para cada no terrnin:il que

Page 168: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

puede compartir un prefijo se reduce en al menos uno en cada paso, de modo que el algo- ritmo garantiza llegar al tinal.

Fisura 4.5 hlgoritmo para factorirac~ón por la izquierda de una

gramática

Ejemplo 4.4 Considere la gramitica para las secuencias de sentencias eccrita en forma recuniva por la derecha:

1.a regla grlrmatical para secuencia-sent tiene un prefijo compartido que se puede factorizar por la izquierda de la manera que se muestra a continuación:

~ec~~enc i ( z -~en t -+ sent sec-sent' sec-sent' -+ ; secuencia-rent / E

Advierta que si habíamos escrito la regla secuenciu-sent recursivamente por la izquierda en lugar de recursivamente por la derecha,

secuencia-sent -+ secuencia-sent ; sent 1 sent

entonces la eliminación de la recunión por la izquierda inrnediata daría como resultado las reglas

secuencia-sent -+ sent sec-sent' sec-sed ; sent sec-sent' 1 e

Esto es casi igual al resultado que ohtuvirnos de la factorizaeión por la izquierda, y, efecti- vamente, al sustituir la secuencia-ient por'sint sec-sed en la últinta regla se hace que los dos resultados sean idénticos. 5

jemplo 4.5 Co~isidere la siguiente gramitica (parcial) p r a sentencias if:

Page 169: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejemplo 4.6

Ejemplo 4.7

Analiris sintáctico L L ( I )

[..a brma factorizada por la i~ipierda de esta grariilítica es

esta es precisamente la forina en In que la utilizanios en la sección 4.7.2 (véase la tahla 4.2). 5

Suponga que escribimos una grainática de expresióti ;irittiiética en la que damos rina asocia- tividad por la derecha de operación aritmética en lugur de una nsociatividad por la izquierda (aquí itsamos + precisamente para tener un ejemplo concreto):

rvp -. teirn + e.tp j term

Esta gramática necesita ser factorirada por la izqtiierda. y obtenemos las reglas

Ahora, continriando como en el ejemplo 4.4, snpotiga que sustituirrios teirn r q ~ ' en lugar de cxp en la segunda regla (esto es legal, porque, de cualqttier manera, esta cxpansicin tendría lugar en el siguiente paso de una derivlicicín). Entonces obtenemos

rup -+ term exp' exp' -+ + tevrn e.rpr / F

Esto es idéntico a Lu gramática obtenida de la regla recursiva por la izquierda mediante la eliminación de la recursión por la izquierda. Por consiguiente, tanto la Sactori/aciítn por la i~yuierda como la eliniinacicín de recursión por la izquierda pueden oscurecer la seniáii- tica de la estructura del lenguaje (en este caso, ambas oscurecen la asociatividad).

Si, por ejemplo, deseamos conservar la asociatividad derecha de 1;s oper~ición de las reglas gramaiicales anteriores (en cualquier forma), entonces debemos arreglar que cada operación + se aplique al final en vez de al principio. Dejanios al lector escribir esto paraprocediniieri- tos desccndentes recursivos. 5

Aquí tenemos un caso típico donde una gramAtica de lenguaje de programaciciri deja de ser LL(l), puesto que tanto las asignaciones como las llamadas de procedimientos comienzan con un identificador. Escribimos la representación siguiente de este problenia:

Esta grainiítica no es LI.( 1 ) porque el identificador es compartido como el prinier toketi tanto de .scnt-asigr~ctc~irírr corno tle smt-llcrtn~ufa, de mtdo que podría ser el token de búsque- da hacia delante para cti~lipicrü de ellas. Desgraciadaniente, la grartiática no está en una for- nia que se pueda factorimr por la i~quiertla. Lo (pie debenios hacer es recinplam- pritncro .srrir-~~,si,~ticici~jrt y ~etif-il(rrit(~rl<r por los latlos tkrcchos de srts protlucciones <lcfiiiid;ts de la in;tncra que w presenta a conrinii;iciti~i:

Page 170: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 4 1 A N ~ I S I S SINTÁCTICO DESCENDENTE

sentencia -;t identi f i cador : = ewp / identificador ( exp-list ) / otro

Entonces factorizamos por la izquierda para obtener

sentencia i iden t i f i cador sentencia' 1 otro

sentencia' i : = exp 1 ( exp-list )

Advierta cómo oscurece esto la semántica de la llamada y la asignación al separar el identific dor (la variable que se asignará o el procedimiento que se llamará) de la acción de asignaci o llamada real (representadas por senfencia'). Un analizador sintáctico LL(1) debe arreglar haciendo que el identificador esté disponible para la llamada o para la asignación de alg manera (por ejemplo, un parámetro) o bien, ajustando el árbol sintáctico.

Finalmente, advertimos que todos los ejemplos a los que hemos aplicado factorización la izquierda en realidad se convierten en gramáticas LL(1) después de la transformació Construiremos las tablas de análisis sintáctico LL(1) para algunos de ellos en la siguien sección. Las demás se dejan para los ejercicios.

4.2.4 Construcción del árbol sintáctico en análisis sintáctico LL(1) Resta comentar cómo se puede adaptar el análisis sintáctico LL(1) para construir árboles s tácticos en vez de árboles de análisis gramatical. (En la sección 4.2.1 describimos cómo se pue den construir los árboles de anáiisis gramatical utilizando la pila de análisis sintáctico.) Vim en la sección en la que se trató el análisis sintáctico Sescendente recursivo que la adaptac de ese inétodo a la construcción del árbol sintáctico era relativamente fácil. En contraste, analizadores sintácticos LL(1) son más difíciles de adaptar. Esto en parte se debe que, co ya hemos visto, la estructura del árbol sintáctico (tal como la asociatividad por la izquier puede ser oscurecida mediante la factorización por la izquierda y la eliminación de la recursi por la izquierda. Sin embargo, se debe principalmente al hecho de que la pila de análisis sin táctico representa sólo estructuras pronosticadas, no estructuras que se hayan visto en realida Por consiguiente, se debe posponer la construcción de nodos de árbol sintáctico hasta el pun en que se eliminen las estmcturas de la pila de análisis sintáctico, en vez de hacerlo cuando inserten por primera vez. En general, esto requiere que se utilice una pila extra para segui pista de los nodos del árbol sintáctico y que se coloquen marcadores de "acción" en la pila análisis sintáctico que indiquen cuándo y qué acciones deberían presentarse en la pila de1 ár bol. Los analizadores sintácticos ascendentes (el capítulo siguiente) son más fáciles de adapta a la construcción de árboles sintácticos utilizando una pila de análisis sintáctico y, por lo tan- to, son los que se' prefiere usar como métodos de análisis sintáctico basados en pilas y contro- lados por tabla. Así, proporcionamos sólo un breve ejemplo de los detalles de cómo se puede hacer esto para el análisis sintáctico LL(I).

Ejemplo 4.8 Utilizaremos una &rimarica de expresiones muy sencillas con 5610 la operaci(íii de suma ; o adición. La BNF es

~ - + ~ + n / n

Page 171: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Análisis iintáttico LL( i ) 167

Ebto provoca qiie 13 s~tiiia se aplique asociativaniente por la izquierda. La gratnática L.L(I) corrcsporidiente con i-ecursi6ri por kr izquierda elirninada es

Mostrarnos ahora c6ino se puede ulilizaT esta grarriática para crilcular el valor aritriiético de la cxpresih. 1.ii conslruccióii de tin irbol siritictico es seiiicjante.

p.. 'tia ~alcular . u n valor del resultado de una expresih, ulilirarenios tina pila por separado en la que se pueda alriiaccnar los vrilores interinetlios del cálculo, a la cual Ilnriiaremos pila de valores. Debeinos clasificar dos operaciones eii esa pila. La priinera operaciciri consiste en insertar riri tiúrricro ciiando es igualado en Ir1 entrda. La scgrinda es s~irriar dos números en la pila. La primera se puede realim ~rieiIi:iiite el procediinien~o tririrch !basada en el tokcn que esth en coricordancia). La segunda iiecesita ser organizada eri la pila de análisis sintáctico. Efectuaremos esto inseitando un síniholo especial en la pila de análisis sintáctico, el cual, cuando se extraiga, indicará que se va a realizar una suma. El sírnholo que utilizaremos para este propiisito es el signo de número (#). Este síitibolo ;¡hora se convierte en un nuevo sírnbo- 10 de pila y también se debe agregar a la regla grairialical que iguala a un +, a saber, la regla para E':

,Advierta que la suma es programada precisamente des[~i&s del riúinero siguiente, pero ;in- les de que se procese cualquier otro iio terminal E'. Esto garantiza la asuciatividad por la iquierda. Aliora verenios c h o tiene lugar el cálculo del valor para la expresih 3+4+5. Indicamos la pila de airálisis sinticticci, entrada y accih corno antes. pero acietnb presenta- rnos la pila de valores a la derecha (que crece hacia la izquierda). Las acciones del analizador sintáctico se proporcioriaii en la tabla 4.5.

Tabla 1.5 Pila de análisis sintáctico con Pila de análisis Pila de acciones de la pila de valorer sintáctico Entrada Acción valores

para el ejemplo 4.8 S E 3 + 4 + 5 $ E + n E ' S S E' n 3 + 4 + 5 $ conc«rdar/insertar $

$ E ' + 4 + 5 6 E ' - + + n # E 1 3 6 % E r # n + + 4 + 5 3 ci)nc«rtlar 3 $

S E ' # n 4 + 5 Y cortcordarlinsertar 3 $ $ E! # + 5 $ agregar a pila J ? $ S Ei + 5 S E' -+ + n # E' 7 $ E ' # n + + 5 $ concortlar 7 S $ E ' # n 5 S ~oncordarlinsutar 7 $

$ E : # $ agregar a pila 5 7 '$

9; E' S E ' 4 e l ? S $ Y; ttceptar 12 $

Observe que cuando tiene lugar riria unla, los oper:rn<los están cri ortlcn iri\ersr> en ki pila tic wlorcs. Esto cs típico de tales csqueiiias de i.v:rliiricii>tr hiisaclos cri pila. 9

Page 172: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

4.3 CONJUNTOS PRIMERO Y SIGUIENTE Para completar el algoritmo de análisis sintictic« LL(1) desarrollamos un algoritmo permite constmir la tabla de análisis sintáctico LL(1). Como ya lo indicamos en varios p tos, esto involiicra el cálculo de los conjuntos Primero y Siguiente, y más adelante en e sección volveremos a la definición y construcción de estos conjuntos, después de lo c proporcionaremos una descripción precisa de la construcción de una tabla de análisis sint tico LL(1). Al final de la sección consideraremos de manera breve cómo se puede exten la constmcción a más de un sínibolo de búsqueda hacia delante.

4.3.1 Conjuntos Primero Definición

Si X es un símbolo de la gramática (un terminal o no tenninal) o E, entonces el conjunto mero(X), compuesto de terminales' y posiblemente de E, se define de la manera siguie

l. Si X es un terminal o E, entonces Primero(X) = {X]. 2. Si X es no terminal, entonces para cada selección de producción X -+ X, X2 . . . X

Primero(X) contiene ~r imero(x~) - ( e ] . Si también para cualquier i < n, todos 1 conjuntos Primero(X,), . . . , Primero(Xi) contienen E, entonces Primero(X) contie Primero(Xi+,) - {E] . Si todos los conjuntos Primero(X,), . . . . Primero(X,,) contl nen E, entonces Primero(X) también contiene E.

Ahora definamos Primero(a), para cualquier cadena u = XIX2. . . X,, (una cadena terminales y no terminales), de la manera siguiente. Primero(n) contiene Primero(XI) - ( Pasa cada i = 2, . . . , n, si Primero(Xk) contiene E para toda k = 1, . . . , i - 1, entonces Pn ro(a) contiene Primero(X,) - {E). Finalmente, si para toda i = 1, . . . , n, Pr¡mero(X,) con e, entonces Pnmero(a) contiene E.

Esta definición se puede convertir fácilmente en un algoritmo. En realidad, el único c difícil se presenta cuando se calcula Primero(A) para todo no terminal A, ya que el co Pnmero de un terminal no es importante, y el conjunto primero de una cadena u se con a partir de los conjuntos primero de los símbolos individuales en n pasos como máxim donde n es el número de símbolos en u. Por consiguiente, establecemos el pseudocódigo p el algoritmo sólo en el caso de no terminales, el cual se presenta en la figura 4.6.

Figura 1.6 Algoritmo para calcular Pnmero(AJ para todos los no terminales A

Page 173: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Conjuntos Primero y Siguiente 169

Fiaura 4.7 Algoritmo simplificado de la figura 4.6 en ausencia de producciones c

'También es fácil ver c h o se puede interpretar esta definición cuando no hay produc- ci«nes E : sólo se nccesila tnantcncr suiriando Primero(Xi) a Pritncro(;t) para cada no termi- nal '\ y «pcii>n de prodiicci6n A 4 Xi . . . hasta que no tengan lugar sumas iidicioii;iles. Eii otras palabras, considere s d o el c a o k = I de la figura 4.6; y el ciclo while interno no es necesario. Establecetrios este algoritnio de manera separada eii la figtira 4.7. Si hay produc- ciones e:, la situaci6ii se vuelve liiás cornplic;ttla, ya que también debernos preguntirrnos si e csti en Primero(X,). y si cs así, continuar rcalizantlo e1 inisrno proceso para X2, y así srice- sivqiente. Aun así, el proceso se terminará después de un número finito de pasos. En rea- lidad, este proceso no sólo calcula los teririinales que pueden aparecer cottio los prirueros símbolos en una cadena derivada de un no terminal, sino que también determina si un no terminal puede derivar La cadena vacía (es decir, desaparecer). Tales no terminales también se (lenominan anulables:

for todos los txo teninntes A do Primero(A1 := ( ] ; whik existan rcrnzbios para cualquier Primert>(A) d~

for cada selecci6n de praduccitin A -+ X1X2 , . . .Yi, do agregue Primeru(Xl) a Pvtmero(d);

Definición

Un no terminal A es anulable si existe una derivüción A ** E

Ahora inostraremo\ el teorema \guiente

Teorema ír0

Un no terminal A es anulable si y sólo si Primero(A) contiene a e.

Demostración: Mostramos que si 4 es anulable, entonces Priniero(A) contiene a e. [..a iii- verya se puede demostrar de manera simihr. Emplearemos la inducción sobre la longitud de una derivación. Si A * e, entonces debe haber una producción A + S y, por definición, Prirnero(A) contiene Primerofg) = ( e ] . Suponga ahora la certeLa de la sentencia para derivaciones de longitud < n, y sea A =, X , , , , X, ** e una derivación de longitud ri (utilizando una selec- ción de producción '4 -+ Xl . . . X,). Si cualquiera de las X, es termin;d, no puede derivar a c. así que todas las X, deben ser no terminales. En realidad, la existencia de le cierivación an- terior, de la cual son una parte, implica que cada X, ** e, y en menos de t t pasos. De este rttodo, ri~cdiante la hipótesis de inducción, para cada i, PrimeroíXJ coiitiene c. Finzlniente, por definición. Prirriero(A) debe contener a F .

~ ~ ~ ~ ~ # w ~ ~ ~ ~ x ~ ~ m

Ol'reccmos varios ejcrnplos del cálculo (le conjuntos Prirriero para no tcr~iiiniilcs.

Page 174: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]
Page 175: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Conjuntos primero y siguiente

Pr~mero(~xp) = { (, número Priniero(1etm) = { (, número 1 Printero@ictor) - ( ( , número ] Priii?ero(o[?~uniri) = ( +, - ) Priri~eroío[~rnul~) = ( * )

(Atlvicrta que s i hubiérainos eiiutnerado Iris reglas grainaticales parqfirm~r en priiiier lugar en veL de al últitno. podríamos haber reducido el número de pasos de cuatro a dos.) Intlicarnos este cálculo en la t,~blü 4.6. En esa tabla s6io se registran los cambios en el cuadro apropiado tloiide ~~curren. Las entradas en blanco indican que ningún conjunto ha cambiado en ese paso. Tainbién cliininanios el último piso, puesto que no ocurre ningún cambio.

Tdhld 4 6 [altula de conjuntos Primero Regla gramatical

para la gramát~ca del ? rp - 6 ' X p

elemplo 4.9 O[JMimrl k'llti

/frc ror * número

Paso I Paso 2 Paso 3

Pr~mero(e L/?)

( 1, número )

Prirnzro(trrrn) - ( 1, número )

Prtniero( factor) =

(. número 1 §

Ejemplo 4.10 Considere la gramática (factorirada por la izquierda) de las sentencias if (ejemplo 4.5):

Esta grairiitica tiene una pmdiiccicín 6, pero lo único que se puede anidar es el rio terminal pcwto-rlsc, de ino<h que puetlen .;urgir muy poc;is coiiiplicacioncs tlrirantc el cilcr~lo. En iwali- dad, d o ~ieccsitarenlos :Igreg;u. E cn iin paso, lo quc tio a f c c t d a riiiiputio de lo\ okos prsos.

Page 176: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 4 / ANALISIS SINTACTICO DESCENDENTE

puesto que ninguno comienza con un no terminal cayo conjunto Primero contenga E. Es es típico en las gramáticas de lenguajes de programiicilín reales, donde las producciones casi siempre están muy limitadas y rara vez exhiben la complejidad del caso general.

Como antes, escribimos las opciones de regla gramaticd por separado y las nunieram

(1) sentenciu -+ senf-if (2) .seritencia -+ otro (3) sent-if if ( e.rp ) seritencia par$<-else (4) purte-else -+ else sentencia ( 5 ) parte-else -, E

(6) e.wp 4 O (7) exp -+ 1

De nueva cuenta, recorremos paso a paso las opciones de producción, haciendo una nueva pas siempre que un conjunto Primero haya cambiado en la pasada anterior. La regla granlatic comienza sin hacer cambios, puesto que Pr¡mero(sent-if) todavía está vacío. La regla dos a y e1 terminal otro a Primerofsentetzciu), de modoque Pnmer«(scrrtenci~) = ( otro]. La regla t agrega if a Prinicro(.sozt-3, de tal modo que Wmern(sent-3 = (if ). La regla 4 agrega els Pnmero(przrte-else), de rnanera que Primero@arte-else) = {else]. La regla 5 agrega E a Pnm m(purte-else), de tal suerte que Primero(puite-else) = (else, E ) . Las reglas 6 y 7 agregan O y a Prirnero(e.xp), de modo que Primero(e.xp) = (0 , 1 l . Ahora efectuamos una segunda pas comenrmdo con ia regla l. Esta regla &ora agrega if a Priinero(sentenciu), puesto que inerofsmt-3 contiene este terminal. De este modo, Primero(.senteni:iu) = (if, otro). Nin otro cambio se presenta durante la segunda pasada, y una tercera pasada no da como resulta ningún cambio. De este modo, calculamos los siguientes conjuntos Primero:

Primeroísentencirr) = ( i f, otro ) Primero(sent-if) = { if } Primeroíparte-else) = (el se. E ) Primero(exp) = ( 0, 11

lo niue5tra cambios y no exhibe la pasada final (donde no se presentan cambios)

r ,<a

La tabla 4.7 exhibe este cálculo de manera semejante a la tabla 4.6. Corno antes, la tabla só-

Tabla 4 7

Pr~mero para la gramática wnlenc~u -r cent-lf

del ejemplo 4.10

Paso 2 Cálculo de conjuntos Regla gramatical

Pnmerofrenrerrcra) = {if, otro)

~entencra + o t r o

<en!-rf-. if ( ~ c p )

sertrencm nartr-else

Paso I

Primero(senrenccu) =

{otro)

Pttniero(rent-IJ) =

i i f l

;)arte-e1.w -t else sentencia

Primero@~<rtre-rlse) =

(else)

p < i r t ~ p / . s e --+ t:

Page 177: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Conjuntos Prtrnero y Sigutente 173

Considere la gramitica siguiente para secuencias de sentencias (véase el ejemplo 4.3):

.secuencia-sent 4 sent sec-sent' .sec-sent' -+ ; secuencicr-sent / e .sent -+ s

De nueva cuenta, enumeramos de manera individual las opciones de prodticci6n:

(1 ) .secuencia .seal -+ sent sec-srnt' (2) sec-sent' -+ ; secitenci(i senr (3) sec-sent' -+ e

(4) sent 4 s

En la primera pasada la regla l no agrega nada. Las reglas 2 y 3 producen Primerof.~ec-.sent") = { ;, e ) . La regla 4 produce Primero(sentencia) = [ S ) . En la segun& pasada, la regla 1 ahora produce Primero(.seciienciu-,reni) = Primero(setzt) = (S}. No se hace ningún otro cambio, y una tercera pasada tarnpoko produce cambio alguno. Calculamos los siguientes conjuntos Primero:

Dejamos al lector ta constnicción de una tabta semejante a las tablas 4.6 y 4.7. 5

44.2 Conjuntos Siguiente

Dado un no terminal A, el conjunto Siguiente(A), compuesto de ternnnales, y posiblemen- te $, se define de la manera siguiente.

1. SI A es el .iímbolo inicial, entoncey $ está en Siguiente(A). 2. Si hay una producción B -+ a A y, entonces Primero(y) - ( e 1 ectá en SiguientefH). 3. Si hay una producción N -+ a A y tal que e está en Pnmero(y), entonces Siguien-

te(A) contiene SiguientefB).

Primero examinamos el contenido de esta definicicín y después escribimos el algoritmo para el crílculo de los conjuntos Siguiente que resultan de ello. La primera cosa que adver- timos es que el signo monetario a, utilizado para marcar el fin de la entmda, se comporta co- rno si fuera un token en el cúlculo del conjunto Siguiente. Sin él, no tendríamos un símbolo para seguir la cadena entera a ser comparada. Puesto que una cadena así se genera mediante el símbolo inicial de la grmática, sienipre se debe agregar e1 símbolo $ al conjunto Siguiente del símholo inicial. (Sed el únicc miembro del conjunto Siguiente del símholo inicial si este símbolo nunca aparece en el lado derecho de una produccih.)

La segunda cosa que rihservarnos es que el "pseudotoken" v:icío E nunca es un elemen- ro de un conjunto Siguiente. Esto tiene scntido porque c: se ulilizaha en conjuntos Primero 4 l o para marcar las cadenas que pudieran desapcirecer. En realidad no es posible recono- cerlo en la cntrxla. [.os .;íinholos sigrtiente. por otra parte, sictnlxe se coinpnr:i~.;in con los

Page 178: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 4.8 Algoritmo para el cálculo de conjuntos Siguiente

tokens de entrada existentes (incluyendo el símbolo $, que se concordad con un EOF ge fado por el analizitdor léxico).

También notamos que los conjuntos Siguiente están definidos sólo para no terminal mientras que los conjuntos Primero también están definidos para termindes y para caden de terminales y no terminales. Podríunm extender la definición de conjuntos Siguieute a cadenas de símbolos, pero no es necesario, puesto que al construir tablas de análisis sintá tic0 LL( l j sólo necesitaremos los conjuntos Siguieute de no terminales.

Finalmente, advertimos que la definicicín de conjuntos Siguiente funciona "por la dere cha" en producciones, mientras que la definición de los conjuntos Primero funciona "po izquierda". Con esto queremos decir que una producción A -+ u rto tiene infomacicín a . ca del conjunto Siguiente de A si u no contiene A. Sólo las ocurrencias de A en los la derechos de las producciones contribuirán a Siguiente(A). Por lo tarito, en general cada. lección de regla gramatical hará contribuciones a los conjuntos Siguiente de cada no ter nal que aparezca en el lado derecho, a diferencia del caso de los conjuntos Primero, don cada selección de regla gramatical se agrega al conjunto Primero de únicamente un no t minal (el que está a la izquierda).

Además, dada una regla gramatical A i u E, Siguiente(Bj incluirá a Siguiente(Aj el caso 3 de la definición. Esto se debe a que, en cualquier cadena que contenga A, A se dría reemplazar por u B (es decir, "libertad de contexto" en acción). Esta propiedad es, e cierto sentido, lo opuesto de la situación para los conjuntos Primero, donde si A i B entonces Primero(A) incluye Primero(B) (excepto posiblemente para e) .

En la figura 4.8 damos el algoritmo para el cálculo de los conjuntos Siguiente que resul tan de su correspondiente definición. Con este algoritmo calculamos conjuntos Siguiente para las rnismas tres gramáticas, para las cuales antes calculamos los conjuntos Primero. (Como en los conjuntos Primero, cuando no hay producciones E, el algoritmo se simplifica, siniplificación que dejamos al lector.)

%

Ejemplo 4.12 Considere otra vez la gramática de expresión simple, cuyos conjuntos Primero calculamos ,$ 3.

en el ejemplo 4.9 de la manera siguiente: ,$ '4 Primero(e.rp) = { (, número ] Primero(term) = ( ( , número } PrirneroCfactor) = ( (, número ] Primeroiop.vumcl) = { +, - 1 Primero(ol,r>l~tlt) = { * }

Page 179: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Conjuntos primero y siguiente

Volveinos a escribir las selecciones u opciones (le procii~ccicíii con ~iitnier;icitin:

Las reglas 3, 3, 7 y 9 no tienen rio terniinales eii sus I;idos derechos, de ~rrodo que 110 ngre- gan nada al cálculo de los conjuntos Siguiente. Considercinws las otras reglas en urden. An- tes de comenzar, estrihlczcamos Signieiite(c.r/~) = (S}. todos Los utros conjuntos Siguiente se inicializan coino vacíos.

¡.a regla 1 afecta a los conjuntos Siguiente de tres no terminiiles: c,.rp. o/t.vunu y ternr. Priniero(o~/w"'n) se agrega a Siguienie(rrp), de modo que ahora Sig~iieritc(~.ip) = (, +, - ) . A contiriitaci6n. Primero(~ern~) se agrega a Sigliiente(o[~xurt~), de iniir1ei.a que Siguien- te(ops~irrro) = ( (. number). Finalniente, Siguientc(r.rp) se agrep a Siguiente(ti~r~n). de tnodo que Siguiente(rerrn) = (S, +, - 1 .

La regla 2 vuelve a c;iusiir qne Sig~iiente(it\-(J') se agregue 3 Signientc(term), pero esto se acaba de realizar inediarite la regla 1 , de triartera que no ocurre r i r i cciiirbio a los conjuntos Si- wiente. %,

La regla 5 tiene tres efectos. Se agrega Pri~riero(o/~»rtiIr) a Sig~iiente(rerrr~). de amariera que Siguiente(rrrrri) = (s. +, -, * ] . Entonces Primero(lNctor) se agrega a Siguiente(op- rnult). de tnodo que Sigiiieiite(o~?rrrlIlt) = { (, numberl. Finnltnente. Siguiente(ferrn) se agrega a Siguienteíjkmr), de rnodo que SigiiienteíJi<,cor) = (S. +, -, * } .

La rcgla 6 tiene el inisrno efecto que el úliiino paso para la regla 5 y, por lo ianto, »o se hacen canrhios.

Finalmente, la regla 8 agrega Primero() ) = ( ) 1 a Siguiente(erp) de iriodo que Siguien- te(exp) = (. +. -, ) ) .

En la segunda pasarla la regla 1 agrega ) a Siguienic(lemz) (de miinera qne Siguieiiie- (tutn) = {S, +, -, *, ) }), y la regla 5 iigiega ) a Siguiente(firmr) (así que Sigt~ieritcuuctor) =

(S, +, -, *, ) }). Una tercera pasada no produce mis cambios, y el algoritmo termina. Hemos calcul;~d« los siguientes conjuntos Siguiente:

Siguienteírxp) = (, +, -, ) 1 Siguiente(o!tsurt~(i) = ( (, número) Siguiente(tcrnt) = {S, +, -. *. ) } Siguiente(o/)rn~rlt) = ( (, número ] Sigliiente(,/ilcror) = (a, +, -, *, ) )

Conio en el cálculo de los conj~iiitos Primero, euhibinios el progrew de este c~álcolo en I:i

t;iblri 4.8. Como mtes, omitimos el paso de conclusión cn esta rahla y 40 indicanws los camhios a los cor!juntos Siguiente cir:iiiJo ocurren. Ornititiios tairihiin las cuatro seleccioiies de regla gramatical que 110 pueden :tfeciar cl cilccilo. (lncluimo< las dos reglas cJ.sp -+ wrni y k,r?n -+

t¿rmr porque pnerlcn lericr n n efecto. ;iun cuiindo no lo tcngiirt cn recilitl;id.)

Page 180: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tabla 4.8 Cálculo de <onjuntor Regla gramatical

Iigu~ente para la gramática <'t[l .3 op\11n111

del ejemplo 4.12 ferm

Ejemplo 4.13

Siguiente(opmu1f) =

Corisidere de nuevo la gramática simplificada de las seriteiicias if, cuyos conjuntos Primero calculamos en el ejemplo 4.10, de la manera siguiente:

Primer«(setlter~cia) - ( i f . otro] Pritnero(.vent-@) = ( i f } Primero(parte-else) = [ e l se, e } Primero(exp) = ( O, 1)

Repetimos aquí las opciones de producción con numeración:

(1) .sentencia + srnt-if (2) sentrrzcia -+ otro (3) sent-if -+ i f ( exp )sentencia parte-else (4) parte-else -+ else sentencia (5) parte-&e -+ e (6) e.xp -+ O (7) exp -+ 1

Las reglas 2, S, 6 y 7 no tienen efecto sobre el c~ílculo de los conjuntos Siguiente, de modo que los ignoraremos.

Comencemos establecietido que Siguiente(sentencict) = ($1 e inicializando los Siguiente de los otros no terminales como vacíos. La regla I agrega ahora Siguiente(.ventencicr) a Si- guiertte(senf-VI. rle modo que Siguiente(serzt-if) = ( $ ) . La regla 3 afecta los conjuntos Siguiente de c,vp, sentenci<l y pwte-else. En primer lugar, Siguiente(e.r~~) implica Prinie- r o = ( ) ), de modo que Siguientdexp) = ( ) ). Etitonces Siguicntc(.~enteti<ia) iniplica Primero(pc1rre-elve) - (c:), (le manera que Siguiente(.serirerici«) - j S, else J. Finalnien- lc, Siguicntcísrnt-lf) se Ltgrega tanto :i Siguieiite(pcirle-eí.se1 corno ;I Siguiente(srrrrrr1~~i~l)

Page 181: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejemplo 4.14

Conjuntos primero y siguiente 177

(esto se debe a que .srr~t-1fpuc.de desaparecer). La primera adici6n nos proporciona Sigi~ieiite- (purte-e/sr) = {S) . pero la segiinda no produce carnbios. Finalniente. la regla 4 provoca que SiguienteQcrrtr-<,l.se) se iigregile a Siguieute(.s<~nter,cicl), lo que tampoco pl-«duce ningún cambio.

En la segunda piisada, la regla 1 vuelve a agregar Siguiente(.sertteniiu) 11 Siguicnte(.srtit-iff, lo que resulta en Siguienle(trnt-18 = /S, e l s e ) . La regla 3 ahora provoca qrie el tenninal e l s e se agregue a Sigr~iciite(lt<rrte-e/,~e), de modo que Siguieritc(l>ctrt<,-e&) - (S e l s e / . Finalmente. la regla 4 no ocasiona más cambios. La tercera pasada tampoco' prodnce cambios adicionales, y hemos calciilado los siguientes conjuntos Siguiente:

Dejamos al lector el constniir una tabla para este cálculo como en el ejemplo anterior. fi

Calciilenios los conjtintos Siguiente para la gramática simplificada de secuencia de sentencias del ejemplo 4.1 1 , con las opciones de regla gramatical:

En el ejemplo 4.1 1 calculamos los siguientes conjuntos Primer«:

Las reglas gramaticales 3 y 4 no contribuyen al cálculo del conjunto Siguiente. Conienmtios con Siguiente(.secuericiu-,s<,tit) = ( S ) y los otros conjuntos Siguiente como vacíos. La regla I produce Siguiente(ser1t) = (; ) y Siguiente(sec.-sertt') = ( $ 1 . La regla 2 no tiene efectos, Una segunda pasada no prodiice cambios adicionales. De este modo, calculamos los conjiin- tos Siguiente

4.33 Construcción de tablas de análisis sintáctico L L ( 1 ) C'wxidcre ahora la consirrccci6n origin;il de las entrail:is de In tabla de :niálisis 5intiictico l . 1 ~ 1 ) . co~no \e clicron en la iección 4.2.2:

Page 182: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 4 1 ANALISIS SINTACTICO DESCENDENTE.

l . Si A -+ u es una opción de prtiduccióri, y existe una derivación u *'"1 $, donde a es un token, entonces agregue A -t a a la entrada de tabla Mv, ul.

2. Si A -, E es una producción E y existe una derivación S $ ** a A a p, donde S es sín~bolo inicial y (1 es un token (o signo de $). entonces agregue '4 - E a la entrad de tabla M[A, c i j .

Evidentemente, el token (1 en la regla I está en Primeroíu), y el token u de la regla 2 está SiguientefA). De este modo llegamos a la siguiente construcción algorítniica de la tabla de análisis sintáctico LL(1):

Construcción de Repita los siguientes dos pasos para cada no terminal A y opción de protiucción A 4 u. la tabla de análisis 1. Para cada token u en Primero(u), agregue A -. u a la eiitriida M[A, (11. sintáctico I,L(l) M [ N , 1.1

2. Si E está en Primnero(cu), para cada elemento u de Siguiente@) (un token o signo de $), agregue A + cu a M[A, u l.

El teorenia siguiente es Iundamentalrnente una consecuencia directa de la definición de gramática LL(1) y la construcción de La kabla de análisis sintáctico recién dada, y deja su tlernostración para los ejercicios:

Teorema

Una gramática en BNF es LL(1) si se satisfacen las condiciones siguientes.

l . Para toda produccicín A 4 u , / cu2 / . . . / a,,, Primero(ui) n Primero(a,) es vacío para t o d a i y j , I ( i , j c n , i f j.

2. Para todo no terminal A tal que Priniero(A) contenga a E, Primero(A) n Sig~iente(~4) es vacío.

Ahora examinaremos algunos ejemplos de tablas de análisis sintáctico para gramática que ya vimos con anterioridad.

Ejemplo 4.15 Considere la gramática de expresión siniple que hemos estado utilizando como ejemplo es- tándar a lo largo de este capítulo.'~sta gramática, couio fue dada originalmente (véase el ejemplo 4.9 anterior) es recursiva por la izquierda. En la última sección escribimos una gramática equivalente utilizando la eliminación de recursión por la izquierda de la manera siguiente:

exp -+ term exp' exp' -+ opsurna twm exp' 1 E

opsuma -+ + / - term -+ &tor ternz' terml -+ opmultfactor terrn' 1 E

opnwlt -+ * j¿lctor -+ ( e.rp ) / número

Debemis calcular los conjuntos Primero y Siguiente para los no terminales de esta gramá- tica. Dejamos este cálculo para los ejercicios y simplemente establecenios el resultado: 1

Page 183: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

tonjuntos primero y riguiente

Ejemplo 4.16

Ejemplo 4.17

Priinero(crp) - ( (. n ú m e r o )

Primer»(c.tlt') = 1 +. -, x ]

~r i~nero(o,~.s ' r t r~~t~i - { +. - ]

I%iiner«(tcrrrr) -= ( ( . número]

Priinero(trrr~r') =: *, t:) Primero(o/mrrlti = j * 1 Prirnero(/iicro,.) - { (, n u m é r o )

Al aplicar la constrncci6n de la tahla de i~n i l i s i s siirtictico L L ( I i ync ;tcahainos de r1esci.i- hir, ohtcneinos ni i ;~ tabla como la 4.4 de In plígiixi 163. §

Considere In g ra~n i t i c r~ siniplificr~da de las sentcticias i f

Los conjuntos Pi.inicro de esta grn~i i i t ica se calcul:nvti en el ejemplo d. 10, mientras t p e los conjnntos Siguiente en el qjernplo 4. 13. Mostraiiios estos conjnntos tlc nueva cuenta rtcluí:

1.a coiistruccií,rt de la inhla de :rnilisis sirit:íctico 1,L.O) iios proporcioriti la tahla 4.3 de la p ig ina 156. 5

Considere la grai i i i t ic;~ del cjctnplo 3.4 (en la que se aplicí, fac tor ix tc ih por la i/quicrd;i):

Esta gramática tiene los .;igi~ierites conjuritos Primero y Siguiente

y lii inhla tle ;iii:ilisis ~ i n t i c t i c o [.J.( I I en la parte wperior de In pígin;r \ig~iicriie.

Page 184: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

43.4 Extensión de la búsqueda hacia delante. Analizadores sintácticos LL(k)

El trabajo previo se puede extender a k símbolos de búsqueda hacia delante. Por eje plo. podenros definir Primnerok(a) = ( wk 1 cx -J" 1, donde u. es tina cadena de tokens wk = los primeros k tokens de w (o W , si w tiene menos de k tokens). De manera simil podemos definir Siguiente,íA) = ( w k / S$ ** dw}. Aunque estas definiciones s menos "algorítmicas" que nuestras definiciones psra el caso k = 1, pueden desmollarse a goritmos que permitan calcular estos conjuntos, y construir como antes la tabla de análisi sintáctico LL(k).

Sin embargo, en el análisis sintáctico LL(k) se presentan diversas complicaciones. En pri- mer lugar, la tabla de análisis sintáctico se vuelve mucho más extensa, puesto que el núm de columnas se incrementa de manera exponencial con k. (Esto se puede contrarrestar has cierto punto utilizando métodos de compresión de tablas.) En segundo lugar, la tabla mism de anáiisis sintáctico no expresa la potencia completa del análisis sintáctico LL(k), sobre todo porque no todas las cadenas Siguiente se presentan en todos los contextos. De este modo, análisis sintáctico por medio de la tabla tal como la construimos se distingue del análisis si táctico LL(k) al nombrarlo análisis sintáctico LL(k) fuerte o análisis sintáctico SLL( [por las siglas de Strong LL(k)l. Remitimos al lector a la sección de notas y referencias para más infomlación.

Los análisis sintácticos LL(k) y SLl,(k) no son comunes, en parte debido a la complejidad agregada, pero sobre todo porque si una gramática falla en ser LL(1) probablemente en la prác- tica no sea LLfk) para ninguna k. Por ejemplo, una gramática con recursión por la izquierd nunca es LL(k), sin iniportar cuán grande sea k. Sin embargo, los analizadores sintácticos des- cendente~ recursivos pueden utilizar de manera selectiva búsquedas hacia delante más grand cuando es necesario, e incluso, como hemos visto, emplear métodos ex profeso pana analiz sintácticamente gramáticas que no sean LL(k) para ninguna k.

4.4 UN ANALIZADOR SINTÁCTICO DESCENDENTE RECURSIVO PARA EL LENGUAJE TINY

En esta sección estudiaremos el analizador sintáctico descendente recursivo completo para el lenguaje TINY que se muestra en el apéndice B. El analizador sintáctico construye iin árbol sintictico como se describió cn la seccicín 3.7 del capítulo anterior y , adicionalniente, irnpriiiie una representación del &bol sintictico para e1 archivo de listado. El analizador sintáctico utiliza !a EBNF tal como se proporciona en la figura 4.9, correspondiente al HNF de la figura 3.6 del copíttilo 3.

Page 185: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

que define la rutina de aniilisis j inticrico principal parse, la i.uul de\ i i c l \e u11 q m t a d o r al .irhol sintiiciico conmi ido por el ;innliz:idor sintüctico. E l archivo parse. c \e proporciona cn el apéndice €3, l í ne~s 9íN- l 111. Se coiiipme de 11 ~~mccdiinieritos mutuanienre rccun iv i~s que corresponden directamente a la graniiitica EBNF de la tigura 1.9: uno p s a . t r < . ~ i r r i ~ ~ i ~ i - \ r ~ i r I rirnt-.sryyiirnc~r), uno para snirrrrc~i<i (~turrrnrnr), uno pwü cada una de la\ cinco clases dife- rentes de sentencias y cuatru para los diferentes nivele^ de prcceden~ia de las cxpre&nes. 1 . i ~ ~ no tcrniitiitles del operador ni) se incluyen como procedimientos. pero ,e reconocen coirio ~ ~ ü r t e de sus expresiones a w i a d a ~ . T a r i i p o hay correspondencia de procedimiento p;ua p r t J -

yrlimu (progrum), puesto que un programa \51v es una secuencia de sentencias. de manera que la nitina parse ("análisis sintáctico") simpleniente Ilitniü ;t ("secuen- cia-sent") stmt-sequence.

E l cúdigo del anal imlor sintáctico también incluye una variable c\tática token que mantiene el roken de búsqueda hacia delante, un prwedimiento match que busca un roken específico, al que llama getToken si lo encuentra y Jeclwd un error en casi> contrario, y un procedimiento SyntaxError que imprime un mensaje de error en el xch ivo de1 li\t;~do. El procedimiento principal parse inicializa token para el primer token en la entr;ida. 1l;iina a stmt-sequence y postcrionmnte verific;~ s i esti el Iinal del :irchivo iuente anres de r e g w ,;U. a1 árbol constniido por stmt-sequence. (Si dcspués de que rqresi i atmt .sequence hay más tokenh, significa que exi\te un error.)

El contenido de c;ida procedimiento recursivo tlchería \er rc1aiii;iincnte :iutoeuplic;iii\i,. con la posible excepción de stmt-seguence, el cual se escribió en una forma algo más com- pleja para mejorar e l manejo de errores; esto se explicará en e l análisis del manejo de erro- res que en breve se realizará. Los procedimientos de análisis sintáctico recursivo utilizan tres procedimientos utilitarios, los cuales se reúnen por simplicidad en el archivo util . c

Page 186: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Fioura 4.10 Exhibición de un irbol rintictico de TINY mediante el procedimiento printTree

CAP. 4 i ANÁLISIS S I N T ~ T I C O DESCENDENTE

íaphiice B. Iíneas 350-526), con la int&ar util .h (apéndice B. líneas 300-335). Esto procediinientos son

1. newStmtNode (líneas 405-421), el cual toma un parámetro indicando la clase d sentencia, y asigna un nuevo nodo de sentencia de esa clase, devolviendo un apunt dor al nodo recien asignado;

2. newExgNode (líneas 423-&O), el cual toma un parámetro indicando la cl;ise de e presión, y asigna un nuevo nodo de expresión de esa clase, devolviendo un apunta dor al nodo recién asignado; y

3. copyString (Iíneas 442-455), el cual toma un parámetro de cadena, asigna es cio suficiente para una copia, y copia la cadena, devolviendo un apuntador a la co recién asignada.

El procedimiento copystring es necesario porque el lenguaje C no asigna de manera aut ~iiática espacio para cadenas y porque el anaiizador léxico vuelve a utilizar el mismo espaci para los valores de cadena (o lexemas) de los tokens que reconoce.

También se incluye en util . c un procedimiento printTree (Iíneas 373-506) que e cribe una virsión lineal del árbol sintáctico para el lihtado, de manera que podamos observ el resultado de un análisis sintáctico. Este procedimiento se llama desde el programa p cipal, bajo el control de la variable global traceparse.

El procedimiento printTree funciona al imprimir información del nodo y luego efe tuar una sangría para imprimir la información del hijo. El irbol real se puede reconstruir partir de estas sangrías. traceparse imprime el árbol sintáctico del progrania TINY de muestra (véase el capítulo 3, figuras 3.8 y 3.9, piginas 137-138) en el archivo del listado co- mo se muestra en la figura 4.10

Write

Id: fact

Page 187: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Retuperation de errores en analizadorer sinrátticos deiceodenter

DE ERRORES EN ANALIZADORES SINTACTICOS DESCENDENTES 1.a respuesta de un ~inalimdor sinláctico a los errores de sintaxis a iiien~cdo es uri f:ictor crítico i n la util icixi de un cornpilaclor. Un nrtalizador sintáctico debe tleterminar por l o nienos si un programa es sintáctica~riente correcto o ti«. Un analizador sintáctico que sdlo rcalizn esta tarea s2 denomina reconocedor, puesto que se limita a reconocer cadenas en el lengti;!je libre tle contexto generado por la grariiitica del lenguaje de prograrnacidn c11 cuestidn. Vale la pe~ ia c\iablecer que cttalqtiier üiializatlor sintictico debe comportarse por l o menos corno un reco- nocedor; es decir, si un programa contiene un error de sintaxis, el ann1iz:rdor sintáctico debe iridicar que existe ul'qúti crror y. ;i la inversa, si tiii programa no contiene errores de sintaxis. intonces el :inaliz;irIor sintáctico iio debe afirmar que existe alguno.

Más allá de este coinpctrtamie~ito mínimo, un analizador sintáctico puede mostrar rri~ichos ~iiveles diferentes de respuestas a Iiis ernms. W.ibituülmcnte, un ana l imhr sintáctico intentará proporcionar uii n i e ~ m j e de error importante, cuando nienos para el primer error que encuentre, y tambiéri intentar determinar de manera tan exacta como sea posible la ubic:tcii,n en la que haya ocu i~ ido el error. Algtinos analizadores sintácticos pueden ir tan lejos coi110 para intentar algum fojrnia de corrección de errores (o, para decirlo de manera quixís más ;tpropi;ttla, repa- ración de errores), donde el aii;ilizador sintáctico intenta deducir un pntgrama correcto :t partir del incorrecto que se le proporciona. Si intenta esto, In mayor px te de 1:)s veces se limitará sirlo a casos fáciles, como la falta de LII~ signo de pontuacicín. Existe un grupo de algoritmos que \e ptieden ;tplicar para encontrar LIII programa correcto que en cierto sentido se parezca ni~ic l iu al proporcionado (habiti~almente en ténnirios del número de tokeris que dehen insertíirsc, eliminar- se o modilicarse). Esta corrección de errores de distancia mínima es por lo regular muy ine- ficiente corno para aplic;trsc a cualquier error y. cn cu~lquier caso, la reparacidtl ilc errores qne 3a corno resultado a menudo está muy lejos de lo qiie el pmgrcriiiador pretendía. Por consigoien- te, es nui, que se vea eii a ~ i r i l i m l w x ~ i n t k t i e o s reales. A los escritores de compiladores les es muy difícil genera mensajes de error sigiiificativos si11 intenta hacer correccidn de enxres.

La mayoría de las técnicas para la recuperacidn de errores son de propdsito específico. ya que se aplican a letigriajes específie«s y a algoritmus de anilisis s i~~táct ico específico, con inii- chos casos especiales para siturrcioiies ptrticiilares. Los priiicipios generales son difíciles de ohtcner. Algun;ts coi~sideraciones irnportnntes que se aplican son las siguientes.

1. U n analizador sintáctico debería intentar determinar que ha ocurrido un crror m1

1~wnfo r:ottio jwrci posihle. Esperar demasi;ido tienlpo arites de la declnracicíri del error significa que la iibicación clel error real puede hahersc perdido.

2. Después de que se Iia presentado un error, e l analizador sintáctico debe selecciooitr un lugar probable para reaiiudar e l análisis. Un arializador sintáctico sieriipre debería iritttntar ;trtaliz;ir taiito c6digo coi-rio f t ~ e n i posible, a f in de encontrar tciritos errores reales como sea posible durante una traducción simple.

3. U n analizador sintáctico debería intentar evitar el prublema de cascada de errores, en la cual un error genera tina larga secuencia de mensajes de error falsos.

4. UII a~ializador sintáctico debe evitar bucles infinitos en los errores, en los que se ge- nera una cascada sin fiti cte mensajes cte error sin consuriiir ninguna enirada.

Algunos de estos objetivos entran en cunflicto entre sí, de tal manera que un escritor de con+ piladores tiene que cfecttuir "convenios" durante la constnicción de un ~n:in-ja<lor tic errorcs. Por ejeinplu. el evitar los pritblenins de casc:iila de errores y bucle infinito puede cic~tsion;ir q u w l :~naliz:idur sintictico oirii(:i algo de In entrlidii. con lo que coniprcimcte CI &jcrivo de procosdr tanta iiiformnciirii de la ecitradu coirio w 3 posiblc.

4.5.1 Recuperacion de errores en analizadores sintácticos descendentes recursivos ~:II;I I'~~III~I ~ ~ I I L I X CIC r c c i ~ p a c i h de mores e11 los m ~ l i ~ d o r ~ s ~hi t~íct icos ~ICSCCII(~CI~IL~\ WLIII-\IWS bc ~ I c n c ) ~ l l i ~ l ~ i 111odo de ;tImnia. F l cionitm k r i w cid hecho ~I IC . CII ii111xiont:s

Page 188: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

GiP 4 1 ANALISIS I INTA~ IEO DESCENDENTE

conlplejas, el manejador de errores consumiri un número posiblemente grande de token un intento de hallar un lugar para reanudar el análisis sintáctico (en el peor de los caso cluso puede consumir todo el resto del programa, lo que no es mejor qne simplemente después del error). Sin embargo, cuando se implementa con cuidado, éste puede ser un m para la recuperación de errores mucho mejor que lo que implica su nombre.' Este mo a l m a tiene, adem6. la ventaja de que virtualmente asegura que el analizador sintáctico caiga en un bucle infinito durante la recuperación de errores.

El mecanismo básico del modo de alarma es proporcioriar a cada procedimiento recursiv . - un paránietro extra compuesto de un conjunto de tokens de sincronizkión. A medida que se efectúa el análisis sintáctico, los tokens que pueden funcionar como tokens de $incronizaeibn . - se agregan a este conjunto conforme se presenta cada llamada. Si se encuentra un error, el a lirador sintiíctico explora hacia delante, desechando los tokens hasta que ve en la entr uno del conjunto de tokens de sincronización, en donde se reanuda el análisis sintáctico. Las c cadas de errores se evitan (hasta cierto punto) al no generar nuevos mensajes de error mie tiene lugar esta exploración adelantada.

Las decisiones importantes que se tienen que tomar en este método de recuperación errores consisten en determinar qué tokens agregar al conjunto de sincronización en cada del análisis sintáctico. Por lo general los conjuntos Siguiente son candidatos importantes p les tokens de sincronización. Los conjuntos Primero también se pueden utilizar para evitar el manejador de errores omita tokens importantes que inicien nuevas construcciones princip (como sentencias o expresiones). Los conjuntos Primero también son importantes, puesto permiten que un analizador sintáctico descendente recursivo detecte pronto los errores en análisis sintáctico, lo que siempre es útil en cualquier recuperación de errores. Es importan darse cuenta que el modo de alarma funciona mejor cuando el compilador "sabe" cuándo alarmarse. Por ejemplo, los símbolos de puntuación perdidos, tales como los de punto y col y las comas, e incluso los paréntesis derechos olvidedos, no siempre deberían provocar un manejador de errores consunia tokens. Naturalmente, debe tenerse cuidado para aseg que no se presente un bucle infinito.

Ilustraremos la recuperación de errores en modo de alarma esquematizarido en pseudoc digo su implementación en la calculadora descendente recursiva de la sección 4.1.2 (véase t bién la figura 4.1). Además de los procedimientos matcli y error, que en esencia permane iguales (excepto que error ya no sale de inmediato), tenemos dos procedimientos más, che put, que realiza la verificación temprana de búsqueda hacia delante, y scanto, que es el to consumidor en modo de a l m a propiamente dicho:

procedure scunto ( synchset ) ; be&

while not ( token En synchset u ( $ }) do getToken ;

end scanto ;

procednre chrckinput ( f irmet , followsct ) ; begin

if not ( token infirstset ) then error ; &canto (Jrrtset ufi~llowset ) ;

end if ; end;

Aquí el signo $ se refiere al fin de la entrada (EOF).

-- 4. Wirih 11'1761, de hecho. Ilrirna :il tncido de :il:irma la rcgla de "no :il;ri-~m", s!lprieit:iincnte cn iin

intente de mejorar si1 iiii:igcn.

Page 189: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Recuperacion de errores en analizadorer iintictitos dest~ndentes 185

Advierta ciímo cl~eckirrpiit es Il:inta~lo dos veces ci1 cada proccdintiento: una v c ~ pasa veril'- car que irri tokcn en el conjtinio Pr i i ixro sea el token siguioite en la e~itr;rtl;i y wi:r segurida ved para vei-ificar que un tokcii en c l cocij~into Siguiente ío .s,vnc.h.ser) sea el iokcn siguicrtte en lil salida.

Esta forma de niodo de alarrito generar6 errores samnahles (niencajes de error útiles que tarnhién se pueden agregar coino parlinretsos a ~h rc .k i~ rp r t y error). Por e,jeinplo, la cadeiia de entrada (2+ - 3 ) *4-+5 geiierarti exactiirrreirtc dos nicnsajes de error (uno p i ra el priiricr signo de nienos y otro para el segundo signo de inrís).

Observitirios que, en getieral, .s~nt~li.set se pasa en las Il;iinadiis rcctil-sivas. con riuevcis to-

kciis de siiicrorii~;icióit ;igrt.gados (le ni;iiiem apropiada. En el cas« de jifirctor, se hace iiiia eu- cepción después de qiie se ve uii p;ir6ntesis i~quierdo: se llanta a ry3 con pitr6ritcsis derecho sólo conro su conjunto siguiente (.syidi.iet se descasia). Esto es iípico de la clase de ii i i i l isis de prcipiísito específico que ircompaña a la reciipcracióri de errores en modo tle nlar~tta. (Hicirlios csto de modo clue. por cjcniplo, I:i expresicíri ( 2+ * ) no gciierase ni1 I'ilso trieiisajc de crnir p;i- r;i cI partntesis tlcreclio.) Iki:irtios iin ;iniilisis {le1 coinport;itriiei~to de wtc código. así ciiirio m in ip le~ iw~tac ión en ( ~ . p;~r;t l t i s +xicios. Por desgruci;~. para ~ibiencr l o s iri-jorcs i i i c i i ~ j c i de m o r y iectiperacirín de errore\. cirrti;iltiiciite torl;~ prtich;t (le tokeii se clchtl cwi-ii l i i i i- 1~1ir 1;' p s i b i I i ~ l ; d (12 1:pe uria priicha r i i is g~xci-;iI, o triia prtichii t r i i s t c ~ i i p ~ i i i ; ~ ~ ii ie~oi-e cl LY)~II~OI--

t~ii i i ienitl tlel ci-ror.

Page 190: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

4.5.2 Recuperación de errores en analizadores sintácticos LL(1) La recuperación de errores en modo de a l m a se puede implementar en analizadores sintác cos LL(1) de manera semejante a como se implementaron en el aniilisis sintáctico descenden recursivo. Como el algoritmo es no recursivo, se requiere de una nueva pila para manten los parámetros syrtchset, y debe planearse una llamada a checkinput en el algoritmo antes cada acción generada por el algoritmo (cuando un no terniinal está en la parte superioi de &)." Advierta que la situación de error primario ocurre con un no terminal A en la parte s perior de la pila y que el token de entrada actual no esté en Prirnero(A) (o Siguiente(A), si está en Primero(A)). El caso en que un token está en ta parte superior de la pila, y no es mismo que el token de entrada actual, normalmente no ocurre, porque los tokens, en gener scílo se insertan en la pila cuando se ven, de hecho, en la entrada (los métodos de compresi de tabla pueden comprometer esto ligeramente). Dejamos,las modificaciones para el algor' nio de análisis sintáctico de la figura 4.2, página 155, para los ejercicios.

Una alternativa para el uso de una pila extra es construir de manera estática los conjunt de tokens de sincronización directamente en la tabla de análisis sintáctico LL(I j, junto las acciones correspondientes que tornaría checkinput. Dado un no terminal A en la p superior de la pila y un token de entrada que no esté en PrirnerorA) (o Siguiente(A), si E

en Primero(Aj), existen tres alternativas posibles:

l. Extraer A de la pila. 2. Extraer de niariera sucesiva tokens de la entrada hasta que se vea un token para e

cual se pueda reiniciar el análisis sintáctico. 3. insertar un nuevo no terminal en la pila.

Seleccionamos la alternativa l si el token de entrada actual es $ o está en Siguiente(A) y la alternativa 2 si el token de entrada actual no es $ y no está en Primero(A) u Siguiente@) La opción 3 ocasionalmente es útil en situaciones especiales, pero rara vez es apropiad (analizaremos un caso más adelante). Indicamos la primera acción en la tabla de análisis sin- táctico mediante la notación extraer y la segunda mediante la notación e,rplorar. (Advierta que una acción extraer es equivalente a una reducción mediante una producción E . )

Con estas convenciones la tabla de análisis sintáctico LL(I) (tabla 4.4) se verá como en la tabla 4.9. El comportamiento de un analizador sintáctico LL(1) que utilice esta tabla, da- da la cadena ( 2 + * ) , se muestra en la tabla 4.10. En esa tabla el análisis sintáctico se muestra sólo a partir del primer error (de manera que el prefijo (2+ ya se ha reconocido con éxito). También utilizamos las abreviaturas E para exp, E' para exp', y así sucesivamente. Observe que hay dos movimientos de error adyacentes antes que se reanude con éxito el análisis sin- táctico. Podemos arreglar suprimir un mensaje de error en el segundo movimiento de error exigiendo, después del primer error, que el analizador sintáctico haga uno o más movimientos con éxito antes de generar cualquier mensaje de error nuevo. De este modo, se evitarían la cascadas de mensajes de error.

Existe (por lo menos) un problema en este método de recuperación de errores que pide una acción especial. Como muchas acciones de error extraen un no terminal desde la pila. es po- sible que ésta se vuelva vacía, todavía con alguna entrada para el análisis sintáctico. Un caso simple de esto en el ejemplo que acabamos de dar es cualquier inicio de cadena con un paf réntesis derecho: esto causará que E se extraiga de inmediato y la pila se quede vacía con toda la entrada esperando ser consumi&.

Page 191: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Retuperacián de errores en analizadores sinticticos descendentes

Una acción que puede tomar el analizador sintáctico en esta situación es insertar el síni- bolo inicial en la pila y explorar hacia delante en la entrada Iiasta que se vea un símbolo en el conjunto Primero del sítnhulo inicial.

Tabla 4.9 Tabla de analislr rintactao L L ( 1 ) (tabla 4.4) con entradas de recuperación de errores

número

etp - rrrm erp' rerm eip'

explorar

- op"l<m<'

term

term'

extraer extraer

term -t tcrm -+

firctor jirctor

rerm' lerm'

explorar explorar

( erp ) número

extraer

r q ' -+ E

explorar

extraer

explorar

cxtraer

explorar explorar

etp' -, ccp' - ~,l>~urna o~sutriu

term exp' I e rn~ erp' r opsumcr + opsuma i

ex traer extraer

, ferm' - E term' -t c:

explora explorar

cxtraer extraer i Tabla 4.10 Movimientos de un analizador Pila de análisis sintactito LL(1) utilizando sintáctico Entrada Acción

la tabla 4.9 % E ' T 1 ) E ' T * ) $ cnplorur (error) ~ E ' T ' ) E ' T ) a extraer (error) a E' T ) E' ) a E ' - E

$ E' T' ) ) a concordar a E' T $~ ' l- -+ E

$E ' a E ' * & $ a acepta

explorar erp' -+ E t extraer

explorar extraer

rerm' i tern~' -+ E

opmirlr

fitctor lerm'

r~prnult i * / extraer

extraer extraer

4.52 Recuperación de errores en el analizador sintáctico de TINY El manejo de errores del analizador sintáctico de TINY, corno se da cri el apiridice 8, es muy rudimentario: sólo esti iinplementada una forma muy primitiva de recuperación cn m«- do de alarma, sin los conjnritos de sincroniracióii. El procediiniento match siriiplcmente ~ l e c l m e m r , estableciendo mi l tokeri que no esperahü encontró. Adernás, los procedimientos statement y factor t1eclrir:tn error cuando no se encuentra una seleccih correcta. El procedimiento garse tanihi6n ileclara error si se cncuenrrü iiri ~okeri disiiiito id f i ~ i de iirclii- v o dcspit6s de que tcrminii cl :inálisis \iritáctico. El principal metis;!je (le error +1e SL. gcrict:~

Page 192: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 4 1 A N A L ~ I S IINTACTICO DESCENDENTE

es "unexpected token" ("token inesperado"), el cual puede no ser inuy útil para el usuar' Adcrnás, el analizador sintáctico no hace u n intento por evitar cascadas de error. P ejemplo, el programa de muestra con un signo de punto y coma agregado después de 1 sentencia write

. . . 5: read x ;

6: i f O < x then

7 : f a c t : = l ;

8: repeat

9: f a c t := f ac t * x;

10 : x : = x - 1

11: u n t i l x = 0;

12: wri te f ac t ; (<- - :SIGNO DE PUNTO Y COMA ERRÓNEO! )

13: end

14 :

provoca que se generen los siguientes dos mensajes de error (cuando únicamente ha ocuin un error):

Syntaxerroratline13:unexpectedtoken ->reservedword: end

S y n t a x e r r o r a t l i n e 14:unexgectedtoken->EOF

Y el mismo programa con la comparación eliminada en la segunda 1íne.a de código

. . . 5: read x ;

6: i f O x then { <- - SIGNO DE COMPARACI~N PERDIDO AQUÍ! ).

7: f a c t := 1;

8: regeat

9: f a c t := f a c t * x;

10: x : = x - 1

11: u n t i l x = 0;

1 2 : write f a c t

13: end

1 4 :

ocasiona que se impriman cuatro mensajes de error en el listado:

Syntaxerroratline6:unexpectedtoken-> I D , nameix

S y n t a x e r r o r a t l i n e 6 : unexpectedtoken-> re8ervedword:then

Syntaxer rora t l i n e 6: unexpectedtoken-zreeervedword: then

Syntaxerroratline7:unexpectedtoken-> I D , name= fac t

Por otra parte, algo del comportamiento del analizador sintáctico de TINY es raionahle. Por ejemplo, un signo de punto y coma perdido (rnás que uno sobrante) generará scílo un men- saje de error, y el analizador sinráctico seguirá construyendo el árbol sintáctico correcto como si el signo de punto y coma hubiera estado allí desde el principio, con lo que está reali- zando una forma rudimentaria de correccicín de error en este único caso. Este comportamiento resulta de dos hechos de la codificaciíin. El primero es que el procedinúento match no con- sume un Lokeii, lo (pie da corno rcsult:ido un comportarnicnto que es idéntico al de insertar

Page 193: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

un token perdido. El segundo es que el procedimiento stmt-seguence se escribicí de ma- ncra que conecte tanto del kbol sintktico como sea posible en el caso de un error. En par- ticular, se debe tener cuidado para asegurar que los apuntadores hernianos estén conectados dondequiera que se encuentre u n apuntador no nulo (los procedimientos del analizador sin- táctico están tiiseñados para devolver un apuntador de árbol sintáctico nulo si se enciieutra un error). 'También, la manera obvia de escribir el cuerpo de stmt-seguence basado en el EBNF

statement ( ) ;

while (token==SEMI)

match(SEM1);

statement ( ) ;

1

se puede escribir con una prueba de bucle inás complicada:

statement ( ) ;

while ((token!=ENDPILE) && (token!=END) &&

(token!=ELSE) && (tokenlsUNTIL))

i: match(SEM1);

statement ( ) ;

1

El lector puede advertir que los cuatro tokens en esta prueba negativa comprenden el con- ,junto Siguiente para stnit-sequence (secuencia-serlt). Esto no es un accidente, ya que una prueba puede buscar un token en el conjunto Primero [como lo hacen los procedimientos para statement (.retitencia) y ,factor], o bien, buscar un token que no esté en el conjunto Siguiente. Esto último es particularmente efectivo en la recuperación de errores, puesto que si se pierde un símbolo Primero, el análisis sintáctico se detendría. Dejarnos para los ejercicios un esbozo del comportamiento del programa para mostrar que un signo de punto y coma perdido en realidad provocaría que el resto del programa se omitiera si stmt-sequence se escribiera en la primera forma dada.

Finalmente, advertimos que el analizador sintáctico también se escribió de una rnanera tal que no puede caer en un bucle infinito cuando encuentra errores (el lector dehería haberse preocupado por esto cuando advirtió que match no consume un token no esperado). Esto se debe a que, en una ruta arbitraria a través de los proeediinientos de análisis sintáctico, con el tiempo se debe encontrar el caso predeterminado de statement o factor, y ambos consumen un token mientras generan un mensaje de error.

4.1 Escriba el pseudocódigo para t e m y Jirctor correspondiente al psetidocódigo para esp de la sec: EJERCICIOS ción 4.1.2 (página 150) que construye un árbol sintáctico para expresiones aritméticas simples.

4.2 Dada la gramática A -. ( A ) 14 I s escriba el pseudocódigo para el análisis sintáctico de esta gra- mática mediante el método descendente recursivo.

4.3 Dada la graniática rcrztenciri -+ srnt-<is.sign / sent-cn11 / otro srrrt-~~,s.siyrz -, identi f icador : = e.rp smt-crr l l -e identificador ( crp-li,st )

Page 194: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 4 1 ANALISIS IINTACTICO DESCENDENTE

4.4 Dada la gramática

lexp i número / ( o p k r p - . w q ) op+t / - / * fexp-.wq -+ lexp-seq Iexp / 1e.y

escriba el pseudocódigo para calcular el valor numérico de una lerp mediante el método des- cendente recursivo (véase el ejercicio 3.13 del capítulo 3).

4.5 Muestre las acciones de un'analizador sintáctico L.t.11) que utiliza la tabla 4.4 (página 163) pa ra reconocer las siguientes expresiones aritméticas: a. 3+4*5-6 b. 3 * ( 4 - 5 t 6 ) . c. 3-(4+5*6)

4.6 Muestre las acciones de un analizador sintictico LL(I) que utiliza la tabla de la sección 4.2.2 (página 155) para reconocer las siguientes cadenas de paréntesis balanceados: a. ( ( ) ) O b. ( 0 0 ) c. ( ) ( O )

4.7 Dada la gramitica A -i ( A ) A 1 E ,

a. Construya los conjuntos Primero y Siguiente para el no terminal A. b. Muestre que esta gramática es LL(1).

4.8 Considere la gramática

lexp -+ utom / list utonz -+ número 1 identificador kst 4 ( 1e.r~-seq ) le.kp-seq -+ lexp-seq lexp / Ie.rp

a. Elimine la recursión por la izquierda. b. Construya los conjuntos Primero y Siguiente para los no terminales de la gramática re-

sultante. c. Muestre que la gramática resultante es LL(1). d. Construya la tabla de análisis sintáctico LL(1) para la gramática resultante. e. Muestre las acciones del analizador sintáctico LL( 1 ) correspondiente, dada

lacadenadeentrada(a ( b ( 2 ) ) (c)). 4.9 Considere la gramática siguiente (similar, pero no idéntica a la graniática dcl ejercicio 4.8):

lexp -+ utom 1 lirt atom -+ número / identificador list -+ ( lerp-seq )

lexp-seq -+ lexp , lexp-seq / l e ~ p

a. Factorice por la izquierda esta gramática. b. Construya los conjuntos Primero y Siguiente para los no teminales de la gramática resultante. c. Muestre que la gramática resultante es LL(1). d. Construya la tabla de análisis sintáctico LL(I) para la gramlitica resultante. e. Muestre las acciones del analizador sintáctico LL(1) correspondicnle, dada la cadena de

entrada (a, (b, ( 2 ) ) . (e)). 4.10 Considere la gramática siguiente de declaraciones simplificadas en C:

Page 195: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejercicios

rlecl~rrcrción -. tipo vur-list tipo -t i n t / f l oa t wr- l i .~ t -+ iden t i f icador, wr-list / iden t i f icador

a. Factorice por la izquierda esta graniática. b. Construya los conjuntos Primero y Siguiente para 10s 110 terminales de la grun~dtica resultante. c. Muestre que la gramática resultante es LL.11). d. Construya la tabla de análisis sintáctico LL(l) para la gramática resultante. e. Muestre las acciones del analizador sintáctico LI.(I j correspondiente. dada la cadena de

entrada i n t x, y , z. 4.1 1 Una tahla de análisis sintáctico LL(1). tal como la de la tabla 4.4 (página 163). generalmente

tiene muchas entradas en blanco que rcpresentan errores. En inuchos casos todas las entradas en blanco en un renglón se pueden reemplazar por una sola entrada predeterminada, con lo que se disniinuye el tamaño de la tahla de manera considerable. Se presentan posibles entradas predeterminad&s en renglones de no termiiial cuando un no terminal tiene una «pci<in de pr«ducción simple o cuando tiene una produccidn e . Aplique estas ideas a la tahla 4.4. ¿,Cuáles son las desventajas de un esquema de esta naturaleza, si es que existen?

4.12 a. ¿Puede ser ambigua una gramática LL(1 )7 Justifique su respuesta. b. ¿Puede una gramática ambigua ser LL(I)? Justifique su respuesta. c. ¿Una gramitica ambigua debe ser LL(I)'? Justifique su respuesta.

4.13 Muestre que una gramática recursiva por la izquierda no puede ser LL(1). 4.14 Demuestre el teorema de la página 178 que concluye que una gramática es L.I.(I) de dos con-

diciones en sus conjuntos Pnmero y Siguiente. 4.1 5 Defina el operador @ sobre dos conjuntos de cadenas de token SI y S? como se describe a

continuación: S, @ S2 = {Pnmero(.y) / .r C Si , y E Si ). a. Muestre que, para cualesquiera dos no terminales A y B, PrimeroMB) = Primer»(A) @ Pri-

mero(B). b. Muestre que las dos condiciones del teorema en Ia página 178 se pueden reemplazar por la

condici6n simple: si A + a y A -. B, entonces (Primero(a) @ Siguiente(A)) n (Printe- m@) @ Siguiente(A)),es vacío.

4.16 Un no terminal A es inútil si no hayderivaciiin del símhulo inicial para una cadena de tokens en la que aparezca A. a. Proporcione una formulación matemática de esta propiedad. b. ¿Es probable que una gramática de lenguaje de programación tenga un símbolo inútil'? Ex-

plique por qué. c. Muestre que. si ima gramática tiene un simbolo inútil, el cálculo de los conjuntos Primero

y Siguiente como se dio en cste capítulo puede producir cmjuntos que sean demasiado grandes para construir con precisión una tabla de análisis sintáctico LL( 1).

4.17 Proporcione un algoritmo que elimine no terminales inútiles (y prodiscciones asociadas) de una gramática sin cambiar el lenguaje reconocido. (Véase el ejercicio anterior.)

4.18 Muestre que si una gramática carece de no terminales inútiles, lo opuesto al teorema de lil pá- gina 178 es verdadero (véase el ejercicio 4.16).

4.19 Dé los detalles del cálculo de los conjuntos Primero y Siguiente que se exhiben en el ejemplo 4. 15 (página 178).

4.20 a. Construya los conjuntos Primero y Siguiente para los no terminales de la gramática factori- rada por la izquierda del ejemplo 4.7 (página 165).

b. Constntya la tabla de análisis sintktico LL(I) utilizando los resultados que obtuvo en el iticiso a.

Page 196: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 1 1 ANALISIS SINTAUICO DESCENDENTE

4.21 Dada la gramhtica A i u A u 1 E,

a. Muestre que esta gramática no es LL( 1). b. Un intento de cscribir uri analizador sintáctico descendente recursivo para esta gcmiátic'a

estd representado por el pseiidocódigo siguiente.

procedure A ; begin

if token = u then gerToken ; A ; if token = a then getToken ; else error ;

else i f token o $ then error ; end A ;

Muestre que este procedimiento no funcionará comctamente. c. Se puede escribir un analizador sintáciico descendente recursivo en reversa para este len-

guaje, pero se necesita usar un proc,edimiento urrCetToken que tome un token como pará- metro y devuelva ese token al frente del flujo de tokens de entrada. También es necesario que el procedimiento A se escriba como una función booleana que devuelva un éxito o un fracaso, de manera que cuando A se llame a sí misrna, pueda probar si hubo un éxito antes de consumir otro token, y de modo que. si la selección A -+ u A o fracasa, el código pueda continuar para intentar la alternativa A + E. Vuelva a escribir el pseudocódigo del ir~ciso b, según esta fórmula, y explore su operación en la cadena auau$.

4.22 En la gramática TINY de la figura 4.9 (página 181) no se hace tina distinción clara entre ex- presiones booleanas y aritméticas. Por ejemplo, a continuación tenemos un programa TINY sintácticamente legal:

if O then write 1>0 else x := (x<l)+l end

Vuelva a escribir la gramática TINY de manera que s61o se permitan expresiones booleanas (expresiones que contengan operadores de comparación) como la pmeba de una sentencia if o repeat, y sólo se permitan expresiones aritméticas en sentencias write o assign o corno operandos de cualquiera de los operadores.

4.23 Agregue los operadores booleanos md, or y not a la gramática TINY de la figura 4.9 (pB- gina 181). Asígneles las propiedades descritas en el ejercicio 3.5 (página 139), así como también una precedencia más baja que la de tudm los operadores aritméticos. Asegúrese de que cualquier expresión pueda ser booleana o entera.

4.24 Los cimbios a la sintaxis de 'MNY en el ejercicio 4.23 empeoraron el problema que se des- cribió en el e,jereicio 4.22. Vuelva a escribir la respuesta al ejercicio 4.23 para distinguir ri- gurosamente entre expresiones booleanas y aritméticas, e incorpore esto a la solución del ejercicio 4.22.

4.25 La recuperación de errores en modo de alarma para la gramática de expresión aritmética sim- ple, como se describió en la sección 4.5.1, todavía tiene algunas desventajas. Una es que los ciclos o bucles while que prueban operadores deberían continuar funcionando en ciertas cir- cunstancias. Por ejemplo, la expresión ( 2 f ( 3 ) perdió su operador entre ios tjctores, pero e1 rnanejador de errores consume el segundo kictor sin rcioiciar el análisis sintictico. Vuelva ;i escribir el pscudoc6digo para niejorar su comportamiento cn este cabo.

Page 197: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejercicios de programación 193

4.26 Explore les :icci»~ies Je ~ i n o~rali~:tdor \i~it,ictic» 1.t.i l) en I:i eiitriida ( 2 + - 3 ) *4-+5. ritilizan<lo

la recuperación <le errores dada en la tabh 4.9 (págin;i 1x7). 4.27 Vuelva ii escribir el ;ilgi~rirnio de an5lisis sintk'tictl L1.í 1 ) ile ia figura J.? ipigirra 1 % ) para

i~riple~iientar 1;1 reciipcracitin de errores CII iiititlo de ;iIarnr;i con~pleto. y cxamirie su cwriprirt;i-

~i i iento en La entr:ida ( 2 + - 3 ) *4 -+5 .

4.28 a. Explore la oper;rci(íri tlcl ptoce~liinientti stmt-sequence cn c l ;inalimdor sintictico T l N Y

parü verificar cjue se construya tl i rbo l si11t5cticii correcto para e1 siguiente programa en

TlNY. :I pesar del 4gno Je punto y cwnn per<Jido:

b. ;,Qué i rbo l sintrictictl se construye para LI siguiente prograni;~ iincorrecto)?:

c. Suponga que en su lugar se escrihiti c l procedimiento stmt_sequence utilizando el código m i s simple esbozado en la pigina 189:

statement ( ) ;

while (token==SEMI)

f match(SEM1);

statement ( ) ;

1

;,Qué árboles sint&cticos se construyen para 10s programas en los incisos a y b iitil i i i indo esta

versión del procediniiento stmt-sequence'?

EJERCICIOS DE 4.29 Agregue IO siguiente a 121 eaicuiat~ora simple descendente recursiva de nr i t i i i~t ica entera cie 13

PROGRAMACIÓW figura 4. l . páginas 148-1.49 (asegúrese de que tengan precedencia y asociati\ idad correctas):

a. División entera con e l símbolo /.

b. Módulo entero con el síinb«lo %.

c. Exponenciación entera con el símbolo ". (Advertencia: este operador tienc inayor prece-

dencia que la ~uultiplicacidn y es asociativo por la derecha.)

d. Menos unitario con el sítnbolo -. (Véase el ejercicio 3.17. página 140.) 4.30 Vuelva a escribir la calculadora descendente recursiva de la figura J. I (phginas 148- 139) de

manera que calcule coi1 números Je punto tlotante en lugar de mteros.

4.3 1 Vuelva ii escribir la calcul&ra descendente recursiva de ia figura J. 1 de nianera que <ii,siingu entre valores de punto flotante y valores enteros. en vez de sinrpleme~ite ciilcular todii como

números enteros o de pnntti tlotante. (St~gerencia: un "valor" es ahora utr rcgistro con un;i mar-

ca que indica si es erttero o de punto tliX;~nte.)

4.32 a. Vuelva a escribir la calculadora descendente recursiva de la figura 4. I de t1i:itiei:i que de\'i~el- va iin :irbtil sintktico de ;icuerdo con las declarrtciones de la iecci~ín 1 3 . 2 (p:ipinii 1 I 1 ).

b. Escriha una St~ncitin que tome corno pariirietro c l :irbol sint;icticu pro(lrici<lo por su ci>digti Jel inciso a y ileviielv;~ c l v;ilr,r crricrtlado al recorrer el i rbol .

Page 198: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

4.33 Escriba una calculadora descendente recursiva para aritrntitica entera simple semejante al de la ~'

figura 4. 1, pero utilice la gramática de la figura 4.4 (página 160). 4.34 Considere la gramática siguiente:

lexp -P número / ( op l q - s e q ) o p + + / - / * lexp-srq + krp-seq le.rp / lexp

Esta gramática puede imaginarse como representación de expresiones aritméticas enteras sim ples en forma prefija tipo LISP. Por ejemplo, la expresión 34-3*42 se escribiría en esta gra máticacomo ( - 34 ( * 3 4 2 ) ) .

Escriba una calculadora descendente recursiva p&ua las expresiones dadas mediante esta gramática.

4.35 a. Invente una estructura de árbol sintáctico para la gramática del ejercicio anterior y escriba u analizador sintáctico descendente recursivo para la misma que devuelva un árbol sintiictic

b. Escriba una función que torne como parámetro el árbol sintáctico producido por su códig tiel inciso a y devuelva el valor calculado al recorrer el árbol.

4.36 a. Utilice la gramática para expresiones regulares del ejercicio 3.4, página 138 (con una eli- minación apropiada de ambigüedades) para construir un analizador sintáctico descrndent recursivo que lea una expresibn regular y realice la construcción de Thompson de un NF (\'&ase el capítulo 2).

b. Escriba un procedimiento que tome una estructura de datos NFA producida por el analiza- dor sintictico del inciso a y construya un DFA equivalente según la construcción de sub- con,junto.

c. Escriba un procedimiento que tome la estructura de datos producida mediante tu procedi- miento del inciso b y encuentre concordancias de subcadenas más largas en un archivo de texto según el DFA que representa. (¡Su programa ahora se ha convertido en una versión "compilada" de grep!)

4.37 Agregue los operadores de comparaciún <= (menor o igual que), > (mayor que), >= (mayor o igual que) y < > (distinto de) al analizador sintáctico de TINY. (Esto requerirá agregar estos tokens y modificar también el analizador léxico, pero no debería requerir un cambio en el &r- bol sintáctico.)

4.38 Incorpore sus cambios gramaticales del ejercicio 4.22 al analizador sintáctico de TINY. 4.39 Incorpore sus cambios gramaticales del ejercicio 4.23 al analizador sintáctico de TINY. 4.40 a. Vuelva a escribir el programa de la calculadora descendente recursiva de la figura 4.1

(phginas 148-149) para implementar la recuperación de errores en modo de alarma como se esquematizó en la sección 4.5.1.

b. Agregue mensajes de error útiles a su manejador de errores del inciso a. 4.4 1 Una de las razones por las que el analizador sintáctico de TINY produce mensajes de error de-

ficientes es que el procedimiento match está limitado a imprimir s6lo el token actual cuando se presenta un error, en vez de imprimir tanto el token actual como el token esperado, y que ningún mensaje de error especial se pasa al procedimiento match desde sus ubicaciones de llamada. Vuelva a escribir el procedimicnto match de manera que imprima el token esperado además del token actual y que también pase un mensaje de error al procedirniento SyntaxError. Esto requerirá volver a escribir el procedimiento syntaxError, así como los cambios a las Ilaniadas de match para incluir un mensaje de error apropiiido.

4.42 Agregue conjuntos de sincronizwión de tokens y recuperacik de errores en modo de alarma al analizador sintktico de TINY, de la manera que se describió en la página 184.

4.43 (De Wirth [107h]) Las csuucturas de datos basadas en diagranias sintáctiios puede11 ser utilizadas por un analizador siritáctico descendente recursivo "generico" que analizará sint6ctic;tmente

Page 199: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejercicior de programación 195

cualquier conjunto de rcglas gratii;iticales F.f.( I ). ün;i estructura de íl;itos adecuiida esti ~l:i&i por las siguicntes cIccl:ir;iciones en C:

tygedef struct rulerec

( struct rulerec *next, *other;

int isToken;

union

( Token name;

struct rulerec *rule;

) attr;

1 Rulerec;

El citmpo next ("siguiente") se utiliza para apuntar al sigiiiente clementci en la regla pniit t ical , mierttrüs que cl campo other ("otror'i se eniplea para apuntar a las alternativ:is dadas pos el t~ictaiiir~bolo l. L>e este modo, la estructura de datos para la regla gramatical

donde los campos de la estrtrcttira de registn~ se muestran de la m;inera siguiente

1 FALCE

NULL

a. Dibuje Iiis zstriicturiis de datos para el resto dc las reglas graiiiaticalcs de la figura -1.4, pi- gioa 160. (Sirgerrrrcirz: neccsitxd i in tokcn especial para represcmir iriicriiainente s cn la cstriicairü <le datos.)

b. Esrihii iiii procedimiwco de arr5lisis iintktico gcn6ric.o que utilice cstas ehiriicturas de da- tos para rcwiiocer i m i c:irlena de cntrad:i.

c. Escrih:~ un gcriernilor Jc ~i~i;~li /ador i int~ctico qiie Ie:i rcgliis WNF (yü sea de t i c iiii :irchivo i~ desde I;i ciitrada i.\r;i111l;iri y geiierc las ewiictiwas dc datos precetieiiits,

I

isToken

TRUE

NULL

otro

1

NULL

I

siguiente 1

Page 200: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 4 i ANÁLISIS I INT~CTICO DESCENDENTE

NOTAS ~i análisis siniáctico descendente recursivo ha sido un método estindar para la construcc

Y REFERENCIAS de anafizdore"h"'cticoda& principios de la tlécüdü de los sesenta y la inrroducci6n reglas BNF en el informe de Algo160 [Naur, 19631. Para una descripción inicial del n todo, vi'ase Hoare j19621. Los an:ilizadores si~itácticos descendentes recursivos en revers se han vuelto populares en lenguajes funcionales lentos tiierternente tipificados. tales co Haskell y Miranda, donde esta fornia de ;rnálisis sintáctico descendente recursivo se de iniria ariálisis sintáctico de combinación. Para una ~íescripción de este método véase a Pey Joties y Lester [lY92] o Hutton f19921. El uso de EBNF junto con el análisis sintáctico ce~idente recursivo hit sido popularkado por Wirth 119761.

El análisis sintáctico LL(I) fue ampliamente estudiado en la década de los sesenta principios de los setenta. Para una descripción inicial véase Lewis y Steams ( 19681. Una in tigacióti sobre el análisis sintáctico LL(k) se puede encontrar en Fischer y LcBlanc 119 donde puede hallarse un ejemplo de una gramática LL(2) que no es SLL(2). Para usos prácti del análisis sintáctico LL(k), véase P m , Dietz y Cohen [1992].

Existen, por supuesto, muchos métodos inás de a~iálisis sintáctico descendente apart los dos que acab~mos de estudiar en este capítulo. Para un c+mplo de otro método m 6 neral véase GrUham, Harrison y Ruzzo 119801.

La recuperación de erntres en modo de alarma se estudia en Wirtti (19761 y Sti [1985]. La recuperación de errores en analiradores sintácticos LL(k) s<t estudia eri Bu Fischer [1987]. Métodos de reparación de errores más sofisticados se estudian en Fisc y LeBlanc f19911 y Lyon [1974].

En este capítulo no se analizaron herramientas automáticas para la generación de an zadores sintácticos descendentes, principalmente porque la herramienta más atnpliani utilizada. Yacc, se estudiará en el siguiente &pítulo. Sin embargo, existen buenos genera res (te analizadores sintácticos descendentes. Uno de ellos se llama Arttlr y es parte del co junto de henmientas de construcción de compiladores de Purttue (PCCTS, Purdue Compil Constructiim Tvol Set). Véase una descripción en Par , Dietz y Cohen (19921. Airtlr gene un aiialirador sintáctico descendente recursivo desde una descripción EBNF. Tiene diver características M e s , incluyendo un mecanismo integrado para construir árboles siiitáctic Para una revisión de un generador de anzilizador sintictico LL(1) deriominado LLGen, vé Fischer y LeBlanc [lY91].

Page 201: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Capítulo 5

Análisis sin tác tico ascendente

5.1 Perspectiva general del análisis 5.5 Yacc: un generador de sintáctico ascendente analizadores sintácticos UJILR(1)

5.2 Autómatas finitos de elementos 5.6 Generación de un analizador LR(0) y análisis sintáctico LR(0) sintáctico TINY utilizando Yacc

5.3 Análisis sintáctico SLR(I) 5.7 Recuperación de errores en 5.4 Análisis sintáctico MLR(1) y analizadores sintácticos

LR( 1 ) general ascendentes

En el capitulo anterior contemplamos los algoritmos de análisis sintáctico descendente bási- cos del análisis sintáctico predictivo y descendente recursivo. En este capitulo describiremos las principales técnicas de análisis sintáctico ascendente y las construcciones que se asocian con ellas. Como con el análisis sintáctico descendente, nos limitaremos a estudiar aquellos algoritmos de análisis sintáctico que utilizan como máximo un simbolo de búsqueda hacia adelante, pero incluiremos algunos comentarios sobre cómo extender los algoritmos.

Con una terminologia similar a la de los analizadores sintácticos LL(1), el algoritmo ascendente más general se denomina análisis sintáctico LR(1) (la L indica que la entrada se procesa de izquierda a derecha, "Lek-to-right", la R indica que se produce una derivación por la derecha, "Rightmost derivation", mientras que el número I indica que se utiliza un símbolo de búsqueda hacia adelante). Una consecuencia de la potencia del análisis sintáctico ascendente es el hecho de que también es significativo hablar de análisis sintáctico LR(O), donde no se consulta ninguna búsqueda hacia adelante al tomar decisiones de análisis sintáctico. (Esto es posible porque un token de búsqueda hacia adelante se puede examinar después de que aparece en la pila del análisis sintáctico, y s i esto ocurre no cuenta como búsqueda hacia adelante.) Una mejora en el análisis sintáctico LR(0) que hace algún uso de la búsqueda hacia adelante se conoce como análisis sintáctico SLR(1) (por las siglas en inglés de análisis sintáctico LR(1) simple). Un método que es un poco más potente que el análisis sintáctico SLR(I) pero menos complejo que el análisis sintáctico LR(L) general se denomina análisis sintáctico LALR(1) (por las siglas de "lookahead LR(1) parsing", o sea. "análisis sintáctico LR(1) de búsqueda hacia adelante').

Abordaremos las construcciones necesarías para cada uno de estos métodos de aná- lisis sintáctico en las secciones siguientes. Esto incluirá la construcción de los DFA de elementos LR(0) y LR(I), descripciones de los algoritmos de análisis sintáctico SLR(I), LR(l) y LALR(I), así como la construcción de las tablas de análisis sintáctico que se asocian con ellos. También describiremos el uso de Yacc, un generador de analizador sintáctico CALR(1) y con él generaremos un analizador sintictico para el lenguaje TlNY que cons-

Page 202: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

truye los mismos árboles sintácticos que el analizador sintáctico descendente recursivo que desarrollamos en el capítulo anterior.

Los algoritmos de análisis sintácticos ascendentes son en general más poderosos qu los métodos descendentes. (Por ejemplo, la recursión por la izquierda no es un proble- ma en el análisis sintáctico ascendente.) Como es de esperarse, las construcciones invo lucradas en estos algoritmos también son más complejas. Por consiguiente, tendremos que describirlas con cuidado y presentarlas por medio de ejemplos de gramáticas muy simples. Al principio del capítulo daremos dos ejemplos de esta clase, los cuales utilizar mos a través del resto del capítulo. También continuaremos con el uso de algunos de I ejemplos recurrentes del capítulo anterior (expresiones aritméticas enteras, sentencias I etc.), Sin embargo, no realizaremos a mano ninguno de los algoritmos de análisis sintácti ascendente para el lenguaje TlNY completo, ya que esto seria demasiado complejo.'D hecho, todos los métodos ascendentes importantes son realmente muy complejos p hacer a mano la codificación, pero son muy adecuados para generadores de analizadore sintácticos como Yacc. Sin embargo, es importante comprender el funcionamiento de I métodos, de manera que el escritor del compilador pueda analizar apropiadamente el comportamiento de un generador de analizadores sintácticos. El diseñador de un lenguaj de programación tambien puede beneficiarse de esta información, puesto que un g rador de analizadores sintácticos puede identificar problemas potenciales con una sin de lenguaje propuesta en BNF.

Los temas vistos con anterioridad que serán necesarios para comprender el funcio miento de los algoritmos de análisis sintáctico ascendente incluyen las propiedades de autómatas finitos y la construcción del subconjunto de un DFA a partir de un NFA (ca tulo 2, secciones 2.3 y 2.4), así como las propiedades generales de las gramáticas libres d contexto, derivaciones y árboles de análisis gramatical (capítulo 3, secciones 3.2 y 3.3). Ocasionalmente también serán necesarios los conjuntos Siguiente (capítulo 4, sección 4.3 Comenzamos este capítulo con una perspectiva general del análisis sintáctico ascendente.

i.1 PERSPECTIVA GENERAL DEL ANÁLISIS SINTACTICO ASCENDENTE Un analizador sintáctico ascendente utiliza una pila explícita para realizar un análisis sintác tico, de manera semejante a como lo hace un analizador sintáctico descendente no recursivo. La pila de análisis sintáctico contendrá tanto tokens como no terminales, y tambikn algu informacióri de estado adicional que analizaremos posteriormente. La pila está vacía al pr' cipio de un análisis sintáctico ascendente y al final de un análisis sintáctico exitoso contendr el sírnbolo inicial. Un esquema para el análisis sintáctico ascendente es, por lo tanto,

$ aceptar

donde la pila de análisis sintáctico está a la izquierda, la entrada está en el centro y las acciones del anali~ador sintáctico est'úi a la derecha (en este caso, "aceptar" es la única accidn indicada).

Un analirador sinláctico ascendente tiene dos posibles acciones (aparte de "aceprar"):

l. Desplazar o transferir un terminal de la parte kontal de la ciitrad;~ hasta la parte su- p r i o r de la pila.

2. Reducir una cadena »i en la parte superior de la pila a un no terminal A. dada In se- lccciiin BNF ;1 -+ N.

Page 203: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Perspetrwa general del analis~r ilntactico artendente 199

Ejemplo 5.1

t'or esta F:r~ón en ocasiones uit ai iül imit ir siiir,ictic« :rscend.rnie se ctiiioce coino ai~alizadix siniictico de reducci6n por desplazamiento ("shift-reduce").' [.as ncciones de tlespl;iza- ririetito se i t idicai~ al escribir Ict !xil;ihra s h ~ $ o ~!uslilu:rirnicrirr>. (.as acciones de reducci<iii si. . , t i i d m n al escrihir la pnlahrü ~ . e d r ~ w o rcvllrwkjti y pi-op«l.cioii;ir la seleccihi BNF titi l imda cn la. re t lucc ió~ i .~ Otra c;tractcrística <le los ürtalimdores sintácticiis iiscentleiitcs es que, por I-;izoties técnicas que se coiiicritardn rnás ;alelnnte, Iss graniiticas siernpre se aumentan coi1 un nuevo síinholo inicial. Esto h i y i f i c ~ i que si S es el siinbolo inicial, se ;igregii a la grairiá- Lica i i r i IILI~.V» sí~i ibt i lo irricial S'. ctin una ~irorli icción simple coi-respoiidiciiie al sítriholo ini- cial anterior:

Contiiiiiitrenirts de iiiitiediato con dos cjeiiiplos, y tlespués conientcircinos ~ Ig t i i ius de las pt-opicdades del ariálisis sintáctico asceiidcnte que se niuestr;~ii mediante estos cjernplos.

Cotisidcre la siguiente gramática stiinentada Imra paréntesis hül;inccodos:

S' 4 S .S-+ ( S ) S j e

Un análisis sintictico :tsccirdente de la cadena ( ) en el que se ut i l izn esra g ~ i i n i t i c a se pro- porciona en la tabla 5.1.

Tabla 5.1 Rtriones de análisis sintactito / Pila de análisis sintáctico Entrada Acción

-- de un analizador rintactito

O $ desplazatnicnto arcendente para la gramatita ) 5 reducciót~ S - 8:

del ejemplo 5.1 )S dcsplazatnietito

Ejemplo 5.2 Considere la siguiente grainitica aumentada para expresiones aritméticas rudinieritariüs (sin par-éntesis y con una operacicín):

lJn anilisis sintictico itscentlenie tle I;i cadena n + n en el que se t i t i l i ra esta grnmáticn se proporciona en 13 tabla 5.2.

l . Por ILI 111is1iii1 r ; i h , los :~ ix~ l imd<~~cs 4nkícLictis dcsceiidetnks w p(itlrkin ~I~III;I~ ~III~IIII~C% Je

gcneriiciiiti prir ~~onciird;iricia. peri! es i o ir« se m)\tiiinhra. 2. 1 3 e l c ~ i de l t t i a r d ~ ~ c c i ~ í i ? , p ~ ~ d r í a t i i t ~ eswihir ~ i ~ n p l e i ~ ~ c ~ ~ t e I~I wlwci~iin IIhF por hí III~III:I.

w n i i ~ Iiicin~os p:li:i rin;i icciiiii <le , ~ c i i < ~ i ? i < k ; t ~ cri e l ; i ~ ~ ; i l i \ i s ~in1;iclii.o ~ l e s w ? d ~ ~ i t e . pr.r<l :~c<~\klt~ihr;i ;i,grib;ar I;i nviin.<.i, i i i .

Page 204: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tnbla 5.2 A(&ner de analirir rinrat&c@ de u n analizador rintictico

I ascendente para la gramática del ejemplo 5.2 3

4 5 h 7

Pila de análisis sintactico Entrada Acción

desplazamienlo reducción E -. n desplazamiento desplazamiento reducción E + E + n reducción E' E aceptar

Los malizadores sintácticos ascendentes tienen menos diticultades que los descendentes la búsqueda hacia delante, De hecho, un anaíizador ascendente puede transferir símbolos de trada en la pila hasta que determine qué acción realizar (suponiendo que se puede deteni una acción que no requiera los símbolos psra ser transferida de regreso a la entrada). Sin em go, un analizador sintáctico ascendente puede necesitar examinar más allá de la parte supe de la pila para determinar qué acción realizar. Por ejemplo, en la tabla 5.1 la línea 5 tiene parte superior de la pita, y el analizador sintáctico realiza uiia reducción mediante la pmd S --t ( S ) S, mientras que la Iínea 6 también tiene S en la parte superior de la pila, pero el lizador sintáctico efectúa una reducción mediante S' -+ S. Para poder saber que S -+ ( S ) una reducción vjlida para el paso 5, debemos saber que la pila en realidad contiene la c ( S ) S en ese punto. De este modo, el análisis sintáctico ascendente requiere cie una "p búsqueda hacia adelante" arbitraria. Esto no es ni con mucho tan serio como la entrada de queda hacia adelante, puesto que el analizador sintáctico mismo construye la pila y puede c e x la información apropiada para que esté disponible. El mecanismo que efectuará esto e autómata finito determinfstico de "elementos" que se describirá en la sig~tiente sección.

Como es natural, no basta con hacer el seguimiento del contenido de la pila para p determinar de manera única el siguiente paso en un análisis sintictico de reducción por plazamiento, también puede ser necesario consultar el token siguiente en la entr búsqueda hacia adelante. Por ejemplo, en la línea 3 de la tabla 5.2. E está en la pila y ocu un desplazamiento, mientras que en la Iínea 6, E está nuevamente en la pila pero ocurre u reducciSn mediante E' -+ E. La diferencia es que en la línea 3 el token siguiente en la entra es +, mientras que en la Iínea 6 el siguiente token de entrada es $. De este modo, cualquier al ritmo que realiza ese anáiisis sintáctico debe utilizar el siguiente token de entrada (la búsq hacia adelante) para determinar la acción apropiada. Diferentes métodos de miilisis sintáctic reducción por desplazamiento~utilizan búsqueda hacia adelante de distintas manera& y da como resultado malizadores sintácticos con una complejidad y potencia variadas. Ante examinar los algoritmos individuales haremos algunas observaciones generales acerca de c pueden caracterizarse las etapas intermedias en un anhlisis sintáctico ascendente.

En primer lugar, obsewemos de nuevo que un analizador sintáctico de reducción por plazamiento sigue una derivación pur la derecha de la cadena de entrada, pero los pasos derivación se presentan en orden inverso. En la tabla 5.1 tenemos cuatro reducciones, corre. dientes en modo inverso a los cuatro pasos de la derivación por la derecha:

S'=+S* ( S ) S * ( S ) * ( 0

En la tabla 5.2 la derivación corre5pondiente es

Cada una de las cadenas interrnecfias de terminales y no terminales en una derivacihn así se conoce como furnia sentencia1 derecha. Cada una de esas formas sentenciales se divi- de entre la entrada y l;i pila de análisis sintáctico cLnr:inte un análisis sintUctico de rcjuc- cihij por iiespla~amiento. Por i,jcntplo, la forma sciitenciol dcrwha E t n, que se prcwita

Page 205: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Automatar íioitas de elementos LR(0) y analirir s~ntacttco L@O) 20 1

i! AUTOMATAS FINITOS DE ELEMENTOS LR(0) Y ANALISIS si~dtrico LR(O)

5.2.1 Elementos lR(0)

Page 206: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. S I ANALISIS IINTAC~ICO ASCENDENTE

metasfmbolo para no confundirlo con un token real). De este modo, si A - a es tina regla producción, y si P y y son dos cadenas cualesquiera de símbolos (incluye~ido la cadena va e), tales como py = u, entonces A --. p .y es un elemento LR(0). Éstos se conocen como el mentos LR(0). porque no contienen referencia explícita a In búsqueda hacia delante.

Ejemplo 5.3 Considere la gramática del ejemplo 5.1 :

S ' -+ S S-+ ( S ) S / e

Esta gramática tiene tres reglas de produccicín y ocho elementos:

S' -+ .S S' -t S. S-+ . ( S ) S S-+ ( . S ) $ S-+ (S . )$ S-+ ( S ) . S S-+ ( S ) S . S - + .

Ejemplo 5.4 La gramática del ejemplo 5.2 tiene los siguientes ocho elementos:

E' -t .E E'-+ E. E - + . E + n E-+ E.+ n E-+ E + . n E - + E + n . E - + .n E - t n .

La idea detrás del concepto de un elemento es que un elemento registra un paso interm en el reconocimiento riel iado derecho de una opción de regla gramatical específica. En part lar, el elemento A -+ f3 . y construido de la regla gramatical A-+ ct (con u = @y) significa ya se ha visto p y que se pueden derivar tos siguientes tokens de entrada de y. En término pila de anjlisis sintáctico, esto significa que P deberá aparecer en la parte superior de la pi elemento A -+ .u significa que podemos estar cerca de reconocer una A mediante el uso d selección de regla gramatical A -+ u (a los elenlentos de esta clase los denominamos elernen iniciales). Un elemento A + u. significa que u reside ahora en la parte superior de la pila d análisis sintáctico y puede ser el controlador, si se va a utilizar A -+ u para la siguiente reducción (tales elementos se conocen como elementos ~ompletos).

5 2 2 Autómatas finitos de elementos

L.os eleirteiitos LR(0) se pueden emplear como los estados de rro nutcímata finito que man- tienen inf»rrnacicín ;iccrca cle la pila de ;inálisis sintáctico y dcl progreso de un aiiilisis de retluccicín por ilesp1a~;rmiento. Esto comcnr~ri como un autcíniata I'inito no detcrriiitiístico.

Page 207: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Autómatas finitos de elemestot IR(@! y analirir rintáctico LR(0) 203

i\ partir de este NFA cle clcnienlos LIZ(O1 podernos construir el D F t i i lc con~untos cle cle- rnetitos LR(0) utilizando la coiistrucción tiel suhcorljurito del capítulo 7. c o n ~ o vereinos. ianihi6n es fácil coristruir el UFA de con,juntos de elementos LR(0) tic tiranera directa.

i,Cuálch son las tratisiciorics del NFI\ de elerrientos LNO)? <'oiisitlere el eletnento ,t +

cu . y y suponga que y ctirtiienzii con el siniholo X, el cual puede ser nt1 fokcri « t i t i no ter- minal, cie iii:iiiera que el elcrrrerittt se pi ied~i escrihir conlo :l -+ «i . Xq. E~itor~ces existe una t rans ic ih en el sí~nholt i Y ilcsdc el c\taclo rcpseseritado por este elcirieirto hasta el estado re- pi.escntnd« por el eleinento A - cx X . 2. En fortnn gráfica esto se escribe coino

Si X es un iokeii. entonces esta trnrisicirín corresponde n ~ ~ i r desp1a~:irniento de X desde la eritratln hacia la parte superior de 1;) pila durmtc un análisis sintáctico. Por otra parte. si X es u11 rio ter~ninid, entonces la iittcrpretacicíii de esta t ransic ih es i~ iás coiiipleja, puesto que .Y nunca aparecerá ccirno un sítnholo de entrada. De hecho, una Iransicicín de esta clase tc:ckivía correspontlcrá a la inserciiin de X en la pila clusanie un ariálisis siirtiíctic», pero esto srílu piie- tie ocurrir tlurante una rcd~iccitin CII la que intervenga ~ i i l a prot luceih X -. p. t\hora, corno una reducci6n de esta clase debe estar precedida por e l reconociinierito de una b, y el esta- do pr<prcionar lo por e l elemento inicial X -+ .@ representa el coniicnzo de este proceso (el prinlo indica que estamos cerca de recitiiocer ;t 6). entonces pura caciiielenienttr A -. n . Xq ilebeirios agregar uiia transicicín I-:

en cntln regla de protl~iccirín X -+ f3 de X. inJicmtlo que se puetle pf»ducir X iiicdirir~ce el se- co~ii~citnieri to (le ctlalquiera de los lacios tierechos de sus producciories,

Estos dos casos representan las únicas tratisiciorics en el N F A de clerirentos LR(0). Que- da por analizar la seleccih del est:ido inicial y los estados tle aceptacirin del NFA. E l estado inicial del N F A tlehería corresponder al estado inicial del arializador sintáctici): la pila es15 vacía, y estanios por reciinocer un S. donde S es el síinholo inicial de la gronráticii. De este rnodo, cualquier elctxicnto ittickil S+ .o: construido ii partir de uitü regla (le prtwluccicíri p:i- ra S podría servir como estado inicial. Desgraciadamente, puede haber muchas reglas tic produccirín tle esta clase para S. ;Cí,ino ptrdetnos decir cuál utilizar'! De hecho, no podernos. La soltici6n es aumentar la gnitnática tnecliante una produccicín simple S' -t S, donde S' es un riuevo no terminal. Entonces S' se convierte eii el estado inicial de la gramiitica aumentada, y el eleniento inicial S' ..S se corivierte en el estado inicial del NFA, Ésta es la raztjti por I:i que itunient;itnos las gycitnáticas de los ejemplos artteriores.

,Cuáles eskalos deherían coiivertirse cn estados ilc ;icept~icitíri cri este NFit'? :\qtií ticbe- nios recordar que c l pr«pcí~irct del NFt\ es tnantetier ~ i o xgi i i iniento del estado ile w i análisis siiitáctico, rio secoriocer c;ttle~ras dil-cctomente. m i n o están disefiados para h;icerlo los citittirna- t;is iIcI c:ipítcilo 2. De este iiiotlo. el niialirnclor sirirrictico r i ~ i m o tleciclirá cuántlo iiccptnr, y el %FA no rieccsita coiitciicr esa inforrniiciíin, i lc r~i;iticra qiic e1 NFA <te 1iech0 no tcndr6 estados de ;iccptacitin en :~hsoluto. (E l NFX r~ rd r ( i ; i lg~i i i ;~ inforrn;icinn ;iccrc;i de !a ;iccpr:~ciíin. penr no CII 1;i t'orin:~ de IIII estado ,le :iccpi;ici(ín. C'c~iiici~tareinc,\ esto ciiat~clo hc i - ibarnos los :II- goriririos (le ariálisis iiiil;ictico (pie risa c l ;tirtii~n;it;i.i

F i ~ o coinpletii \ i r t i c w i p c i h ilcl UFA de ~ l e r i x r i t ~ ~ s I.R(O). ..\lioni \ o l r c r e i i ~ ~ i ~ :i las Sra- 111ci1ici15 si~iiples de t!(is cjcrnpliw :~ntcsi i~sc~ ! coiist~.~rir-crticis VI::\ de c I ~ i i i e i ~ t < ~ ~ l .R(O).

Page 208: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

294

Ejemplo 5.5

Figura 5 1 El NFA de elementos LR(0) para la gramática del ejemplo 5.5

jemplo 5.6

F i g ~ i r a í 2 E l #FA de elementos LR(0) para la gramat~ca del ejemplo 5.6

En e1 ejeriiplo 5.3 enumeramos los ocho elementos LR(0) de la gratnitica del ejemplo 5.1. lo tanto, el NFA tiene ocho estados, como se niuestra eil la figura 5.1. Advierta que c elemento de la figura con un punto antes que el no terminal S time una transición o para do eleiiiento iriicial de S.

En el cjcmplo 5.4 cnurnerarrios los elementos L,R(O) asociados con la gruinitica del eJe plo 5.2, El NFA de los elementos aparece en la figura 5.2. Advierta que el elerneiito inic E --+ . E + n tiene tina transicicín E hacia sí mismo. (Esta situacicín se presetmrá en toda l;~s graniiiicas con rectirsión por la izquierda inmediata.)

Para completar la descripción del uso de elementos en el seguimiento del estado d aniiisis sintáctico, debemos cunsiruir el DFA de conjuntos de elententos correspoiidient al NFA de los elcriientos de acuerdo con la constritcciítn de suhcoiijuntos del capítulo L. Entonces podremos establecer el elgoritnto de análisis sinkíctico L.R(O). Kealirar-emos la consiruccicín de1 s~ihcoi?jurito para I«s dos ejernplos de NFA que ~rcühamos de dar.

Page 209: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Authatas fin~tor de elementos LR(0) y analiris r~ntactlca lR(0) 205

[S' -9 .S. .S-+ . ( S ) S. .S --+ . 1. I'uesto que existe m a trnrihición t l cde S' -+ .S h:ista S' +

S . eri S. existe una ir;irisición corrcspondierite clcl estaJo inicial ;iI estado DFA ( .Sf -+ S . 1 (sin que haya traiisicionci c: desde S' -- S . a i i ingun otro clcriiento). I'ainhiéii existe riwt rl-ünsicitiri en ( del cstaelo itrici;tl al cstnclo 1>F:\ ( .S 4 (. S ) S, .S -e ~ ( ,S ) .S, 5 -+ . / (1;i ccrr:itlur;t c: de { .S -, ( . S ) S ) ). E l est;itht LWA ( S -+ (. S ) .S. 5 --* . ( .S ) S, .S --+ .) tic- tic tmrisiciorres hacía \i tiiisrilo e11 ( y :t { 5 -+ ( S . ) S / en .S. I s i e e~titdtr tiene uiia trarisi- ciótr al est:tdo ( S ( S ) . S. .S ---. . ( S ) S, S --. . / en ) . Fir~dmcnte, e\te rílti~rio estado tictre tri~nsicioiics en ( ;iI estado :irites ciriistrriiih / S -4 (. S ) S. S -- . ( S ) S. .S --3 . j y IIII;~ traii- siciti i i en S ;II cst;iiio { S - + ( S ) S . 1. E l »FA cornplcto se propi~rciona en la figura 5.3, tltrntle nrilncrnmos l o i est:itlos corrio i-ekrerrcia (el estiido O se asigna t f i ~d ic io i~~ t l r i~c r~ te al es-

tnclo inicial).

F i g ~ i i a 5.3 ----c ,y '~~ .~ . .~ 3 .S

DfA de canjuntar de &mentas correrpandientes -- + . ( ~ s ) .S y

al NfA de la figura 5.1

( .5 -~-- ( . S ) .s. S - - - - .(S) S

S . - -

Ejemplo 5.8 Cotisiclerc el NIy,\ tic la figura 5.2. E l citado inicial del DFA asociado rc cimpone tic1 coii- junto de los tres elerrientos (E' - .E, 1: -- .E + n . E 4 .n/. Existe tina iransici~iri del cietiretito E' - .E al t:leinetito -+ E . rn E, pero también hay uria transici6ri en E del cle- iuento E -+ .E + n al eiertienkr E -, E. i n. De este modo, existe tina transici6n en E (Ir1 estado inicial del DFA 3 IU ~erracl i i r~t del coti j~it t to {E' --t E.. E 4 E. t n/. Puesto q ~ t e no hay transicioiies 6: i leidc c~t;~ltpiera de estos eletncntos, este conjunto es sil propia cerrndurit t: y forina un estado DF1\ completo. Existe otra tr:msicióri de1 c m d o inicial. c11- rmptmtl iente 3 la transici6n en el símbolo n de E 4 . n ü E -+ n.. Pricstit que no se tic- iicri tr;insiciones c: tiel elenicilto E + n.. este eletiieirto es \ t i propia cerr;i<ium E y l i m i a el estado DFA ( E --. n. l . N o Ira! tr;ti~sicioriei tleitlc este estado. de nrodo i p e las únicas tr: t i r~ici tr~ics por ser C:I~CLI~:I<~:~S t od i~v í :~ vienen del conjunto {E? - k . . E - E . + n/. S610 hay uitn trarisiciii~i de e t c i:<lt!jriiito. i:r~rrcspot~tlicntc a la tr.;~tisici~iri dcl eleir~ciitir E ---

I:' . + n id clenicnto I: --a 1 +.n 2011 el ií1nh010 +. El ~ ' le~r iento E ---, E +.n I:IIIIIW~O ticric ;~I~uII~I ~ r x ~ s i c i i h 6:. por 111 I : t i ~ í t ~ , S~rr~iia un c t~ i i~ tmto u111 u11 w l i t cIci~1ei110 cn d DFA. F i ~ r a - ~ c i S t i t i s i c t i I n 1 c t ~ i 1 - + F; t. 1 1 ~ i t r 1 --3 E + n. /

Page 210: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Fiqura 5.4 E l DfA de tonluntos de elementos correspondientes al N f A de la ligura 5.2

CAP. 5 1 A N A L ~ I S SINTÁCTICO ASCENDENTE

," - E'-+ .E E ---+ E . + n

E - . E + n

E --- . n o

\ O, + n

T

Al construir el DFA de los conjuntos de elementos LR(O), en ocasiones se distingit los elementos que se agregan a un estado durante la etapa de la cerradura E de los que originan como objetivo de transiciones no E . Los primeros se denominan elementos cerradura, mientras que los últimos se conocen como elementos de núcleo. En el esta O de la figura 5.4, E' -+ .E es un elemento de núcleo (el único), mientras que E -, .E y E-, . n son elementos de cerradura. En el estido 2 de la Figura 5.3, S -+ ( .S ) S eleinento de núcleo, mieniras que S -+ . ( S ) S y S -, . son elementos de cerradura. De acue do con la definición de las transiciones o de los NFA de elementos, todos los elementos cerradura son elementos iniciales.

La importancia de distinguir entre elementos de núcleo y de cerradura es que, dada gramática, los elementos de núcleo determinan unívocamente el estado y sus trünsicione De este modo, stílo es necesario especificar los elementos de núcleo para caracterizar co pletamente el DFA de conjuntos de elementos. Los generadores de analizatiores sintáctic que construyen el DFA pueden, por lo tanto. informar sólo de los elementos de núcleo (e cs cierto para Yacc, por ejemplo).

Si en vez de calcular primero e1 NF.4 de los elementos y posteriormente aplicar la cons- truccicin de subconjuntos, se calcula direck~mente el DFA de los conjuntos de elementos, se presenta una simplificación adicional. En realidad, es fácil determinar de inmediato a partir de un conjunto de elementos cuáles son las transiciones E y a cuáles elementos iniciales apuntan. Por consiguiente, los generadores de analizadores sintácticos como Yacc siempre calctilan el DFA directamente desde la gramática, lo que haremos también en lo que resta del c;tpítulo.

52.3 El algoritmo de análisis sintactico LR(0) Ahora estamos listos para establecer el algoritmo de análisis sintáctico I.R(O). Como el algo ritmo depende del seguimiento del estado actual en el DFA de conjuntos de elementos, de- bemos modificar la pila de análisis sintáctico para no sólo almacenar sírrtbolos sino también números de estado. Haremos esto mediante la inserción del nuevo número de estado en la pila del análisis sintactico después de cada inserción de un símbolo. De hecho, los estados mismos contienen toda la información acerca tle los símbolos, así que podríamos prescindir del todo de los símbolos y mantener sólo los números de estado en la pila de análisis sintác- rico. Sin embargo, mantendremos los símbolos en la pila por conveniencia y claritlad.

Para comenzar un anilisis sintáctico insertamos e1 n~itr~ad«r inferior $ y el estado inici;il O en la pila, de riranera que al principio del aniílisis la situacicin se pucde representar como

Pila de análisis sintáctico Entrada

3 O ~'<l<l<~lf~lf~~ftr~l<l<l '3

Page 211: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Autómatas hitos de elementos LR(0) y analiris rintáctico LR(0) 207

Siipmganios ahora que el siguiente paso es tlesplazar un tokcii n a la pila y v;iyairicts ;i1 estado 2 (esto ocitrrirá en realidad cuando el D F A esté corno en la f i g ~ i r ~ 5.4 y n sea el to-

k r n sigiiierite en la entrada). Esto se representa de la manera siguieiite:

Pila de análisis sintáctico Entrada

E l algoritnio de análisis sintáctico LK(0) elige nnit acción basado e11 el estado D F A actual. que es siempre el estado que aparece en la parte superior de la pila.

Definición

El algoritmo de análisis sintáctico LR(0). Sea s el estado actual (en la parte superior de la pila de análisis sintictico). Entonces las acciones se definen como sigue:

l. Si el estado s contiene ccialquier elemento de la forma A -+ a.X P, donde X es un terminal. entonces la acción es desplazar e l token de entriida actnal a la pila. Si este token es X, y el estiido s contiene el elemento A -+ u. X f3, entonces el nuevo estado que se insertará en la pila es el estado que contiene el elemento A -, (YX. P. Si este token no es X para algún eleniento en el estado .S de la f t~r ina que se acaba de descri- bir, se declara un emtr.

2. Si e l estado s conticite cualquier elemento co~npleto (un elc~nento de la forrna A -9 (Y. ), entonces la acción es re(1ucir mediante l a regla A -. u . Una redoccióii ine- tliarite l a regla S' 4 S, donde S es el estado inicial, es equivalente a la aceptación. siempre que la entrada esté vacía, y a un error s i no es así. E n todos los otros casos e l nuevo estado se calcula como sigue. El irni i ie la cadena (Y y todos sus estados correspondientes de la pila de análisis siritáctico ( la cadena tu debe estar en la parte stiperior de la pila, de l cuerdo con la nianeru en qiic esté construido el DFA). De la misma Inanera, retroceda en el D F A hacia el estado en el cual comenzó la cotistrlic- ción de u (&e debe ser el estado descubierto por la e l im inx ión de a ) . Nuevamen-- te. mediaiite la constrticción del DFA, esle estrtdo dehe contener riri elernerito de la forma B -+ a.A p. inserte A en la pila, e inserte (conlo el nuevo estadoj el csta<lo cpe contiene el elemento H - LYA. p. (Advierta que esto corresponde a seguir la t r m - sici<in en A en e1 DFA, lo cual es en realidad faronable, puesto que estanios insertan- do A ert la pila.)

Se dice qrie tina gramática es tina gramática LR(0) s i las ritglas anteriores son no arn- bigcias. Esto significa que si i in estado contiene un elemento completo A 4 u., i io puede contener otros. E n efecto, si un estado así también contiene un clcinento "ctesptazado" A -P i r . X p (dortde X es un tcrmiiial). entonces surge una amhigüed;id, ya sea que se realice una . , accicín 1 ) o una accion 2 ) . Esta situación se conoce como conf l ic to en r e d u c c i h por desplazamiento. De iiianern iiriiil;tr. s i un estado de esta clase contiene otro elemcnto com- plcto I j -+ P., surge una ni~ibipiictlütl al decidir qué produccicíri r i t i l i ~ a r p r a la rediiccicíri i , l - (2 o H -+ (3). Esta s i tuxi i i t i i c coimce corno conflicto (le reduccihn-reduccihn. De este ir~ocio. una gr;tiiiátic;t cs I.Iií0) s i y slilo s i cada estado cs un c.sindo (le ilcsplazartiicnto ! rin cst:1110 i p e contieiie síilo c l c i i ~ e i ~ k ~ s de "tlcspl;ir:ii~iic~tto"i o un cst:~tlo de 1-educcicín q ~ i c .:~~!iticiie stilo i in elcinctito ctxnpiecii.

Page 212: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. S 1 A N ~ L I S I S ~INTACTICO ASCENDENTE

Advertinios que ninguna de las dos gramáticas que hemos estado utilizando co nuestros ejemplos son gramáticas LR(0). En la figura 5.3 los estados O, 2 y 4 contie todos conflictos de reducción por desplazamiento para el algoritnto de análisis sintáct LR[()), mientras que en el DFA- de la figura 5.4 el estado 1 contiene un cantlicto de red ción por despla~aniiento. Esto no es de sorprender, puesto que casi todas las gramáti "reales" no son LR(O). Sin embargo, preseiitarnos el ejemplo siguiente de una gramática es L,R(O).

Ejemplo 5.9 Considere la gramática

A - + ( A ) / a

La gramática aumentada tiene el DFA de los conjuntos de elementos que se muestran e figura 5.5, que es LK(0). Para ver cómo funciona el algoritmo de análisis sintictico LR consideremos la cadena ( ( a ) ) . Un aniílisis sinthctico de esta cadena de :tcuerdo con el goritnio de análisis sintáctico LR(O) se da mediante los pasos de la tabla 5.3. El análisis e niienza en el estado 0, el cual es un estado de desplazatniento, de modo que el primer to ( se desplaza a la pila. Luego, conlo el DFA indica una transición del estado O al estad en el símbolo (, el estado 3 se inserta en la pila. Este estado también es un esta& de desp mniento, de modo que el siguiente ( se desplaza a la pila, y la transición con ( regresa estado 3. Al desplazar de nueva cuenta se pone a a en la pila, y la transició~i con a del tado 3 se va a1 estado 2. Ahora nos encontramos en el paso 4 de la tabla 5.3, y alcanzamo el primer estado de reducción. Aquí e1 estado 2 y el sín~bolo a se extraen de la pila y en proceso se regresa al estado 3. Entonces se insertaA en la pila y se lleva a cabo la transició de A del estado 3 al estado 4. El estado 4 es un estado de ilesplazarniento, de modo que ) se desplazü a la pila y la transición tt-m ) lleva el análisis sintáctico (11 estado 5. Aquí se presen- ta una reducción ntediarite la regla/% -+ ( A ) , al extraer los estados 5, 4, 3 y los símbolos A, ( de la pila. El análisis sintáctico está ahora en el estado 3, y nuevamente A y el esta 4 se insertan en la pila. De nuevo, ) se desplaza a la pila y se inserta el estado 5. Ot reducción mediante A -+ (A) elimina la cadena ( 3 A 4 ) 5 (hacia atrás) de la pila, lo deja el análisis en el estado O. Ahora se inserta A y se lleva a cabo la transición de A del e tado O al estado l . El estado 1 es el estado de aceptación. Como ahora la entrada está vacía. acepta el algoritnio de análisis.

t'iglira 5 .5 t, - El DFA de conjuntos de elementos para el ejemplo 5.9

.A'--.A

A - .(A)

A --+ .a L .1 -+ a.

( O f

A ----- ( . A )

.(<1)

A -- .a ( * \

<' ~+ c!J

\.. . .--/.' 1

Page 213: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

E l DFA de los conjuntos tic cleiiientos y las ;rcciories espccific;itlns por el cilgoritriio de :inálisis sintáctico L.R(O) se pueden coiiibinar en iina t:ihla de máiisis sintáctico. 'le tiiancra que el atiálisis sintáctico 1..11(0) se convierta eri riti rnitotlo de iinálisir siritáctico control;itfo por tabla. t ina org:inilacióri típica es qiie los renglones de la tabla m i r t etiqcietidos con los estados del DFA, y las colurr~iias se etiqrieten coriio sigue. Pircsto que los estados del análi- sis sintáctico L K ( 0 ) son estados "de rleiplazamiento" 0 estados "de rcducci6n". .;e reserva un:i coltiriioa piira indicar esto en cada estado. En el caso de un estado "de retlucción" se titi-. l i l a otra colt~rnna para iridicnr la regla grainatical que se usará en I;i redricciíin. En el caso de un estado de despl:izarniei~to el sírribolo por ser desplazxlo determina el cstiido sigtiicri- Le (ti~cclinritc el DP\) , y así tlclie haber ritra colurrina para cada token, cuyos eiitradai son los niievos cstndos por intrtidueirse cii un despl;tmniento de ese tokeri. !,as transiciones tle 110

terminales (que se insertan rlirfiiote una reducción) representan un caso especial. puesto qiic no se ven realniente rri la entrada, rttiiiqrie el analizador sititáctico actúa conio si estrivict-an tlespl:ir;ttlos. De este r n d o . eri un estado "(le ctesplarnriiierito" tairibibn se riccesila twer una columna p:m cada no terminal, y tradicionnlr iete estas coliiiiiiias se enlist:in en kina parte sepraila de la tabla cleiit~rnitioila sección Ir a (Goto).

U n qjemplo de tina tabla tle análisis sii it ictico de esta naturaleza es la tilhl:t 5.1. que es la tabla para la gramática del ejeniplo 5.9. Se alienta al lector p;ira que verificliie que 6sta con- duce a ;icci«ires de ariilisis sintáctico para ese ejernplo corno I:is dudas en la tabla 5.0.

Tibln 5.4 Tabla de análisis rintactito Estado para la gramática del ejemplo 1.9

O I 7 - 3 -1 .í

Acción Regla Entrada Goto -

A -

dcspla~:iiiiient~~ I redticcicin , \ ' -+ A reduccicin cIcsl~luLnniicric<> 4 s p 1 1 1 i 1 5 i-c<liicciijn 1 TI-+(.\)

Page 214: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 5 1 ANALISIS SINTÁCTICO ASCENDENTE

iiecesitaremos especificar con precisión qu6 acción torna el analizador sintictico para ca una de estas entrados en blanco. Pospondremos este análisis hasta una sección posterior

53 ANÁLISIS SINTÁCTICO SLR(1) 5.31 El algoritmo de análisis sintáctico SLR(1) El análisis sintáctico L.R( 1 ) simple, o SLR(1) utiliza el DFA (le conjuntos de element I.R(íff como se construyeron en la sección anterior. Sin embargo, incrementa de manera im portante la potencia del análisis sintáctico t.R(O) al utilizar el token siguiente en la cade de entrada para dirigir sus acciones. Lo hace de dos maneras. La primera consulta el token entrada cintes de un tlesplazaniiento para asegurarse de que existe una transición 11 apropiada. La segunda utiliza el conjunto Siguiente de un no terminal, como se constru en la seccitín 4.3, para decidir si debería efectuarse una reducción. Sorprende que este si ple uso de la búsqueda hacia úelante sea tan poderoso que permita analizar casi toda con truccitín de un lenguaje que se presenta comúnmente.

El algoritmo de análi.sis sintáctico SLR(1). Sea s el estado actual (en la parte superior de pila de análisis sintáctico). Entonces las acciones se definen como sigue:

1. Si el estado s contiene cualquier elernento de la forma A + a.X P, donde X es u terminal, y X es el siguiente token en la cadena de entrada, entonces la acción es de plazar el Loken de entrada actual a la pila, y el nuevo estado que se insertará en la pil es el estado que contiene el elemento A -+ aX . p.

2. Si el estado s contiene el elemento completo A + y., y el token siguiente en la e dena de entrada está en una Siguiente(A),, entonces la acción es reducir median la regla A -t y. Una rediicción mediante la regla S' -+ S, donde S es el estado in cid,-es equivalente a la aceptación: esto ocurrirá sólo si el siguiente token de entr da es $! En todos los otros casos, el nuevo estado se calcula como sigue. Elimine cadena a y todos sus estados correspondientes de la pila de análisis sintáctico. De la misma manera, retroceda en el DFA hacia el estado en el cual comenzó la cons trucción de a. Por construcción, este estado debe contener un elemento de la fo nia B -t y.A P. Inserte r l en la pila, e inserte el estado que contiene el elemen B - t a ' 4 . p .

3. Si el siguiente token de entrada es de natur&m tal que no se aplique ninguno de lo dos casos anteriores' se declara un error.

Decimos que tina gramática es una gramática SLR(1) si la aplicación de las auterio- res reglas de análisis sintáctico SLR( l j no producen una ainbigiiedad. En particular, una eraniática es SLR(I) si y sólo si, para cualquier estado .S, se satisfacen las siguientes dos ',

condiciones:

Page 215: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

l . Para cu;tlquier elcmento A - (Y. X B cn s con un terminal Y, tio hay elcrnciito co~ri- pleto B -+ y. en .Y con X en Siguiente(B).

2. Para cualesquiera dos elcnientos conipletos A -+ (x. y R -t (3. en S , Sigtiie~ite(A) n Siguiente(B) cs vacía.

Una violación de Iii primera de estas condiciones representa un conflicto de reducción por despIazamicnt«. Una violación de la segun<la de estas coiidiciories representa un ron- tlicto de reducción-reducción.

Estas dos coiidiciones son seriiejantes en esencia a las dos condiciones para el análisis sintáctico LL(1) establecidas en el capítulo anterior. excepto que, como con todos los méto- dos cle análisis sintáctico de redticcicín por desplaramiento, las decisiones sobre cu i l regla gramatical utilizar se pueden posponer hasta el úttinio rnoincnto posible, lo que da cotrio re- sultado un aiidirador sintáctico mis poderoso.

Una tabla de análisis para el análisis sintáctico SLRi 1 j türnbi6n puede constn~irw de inariera siniilar a la del análisis sintáctico LR(0) descrita en la sección anterior. Las dife- rencias son las siguientes. Como un estado puede tener tanto desplazamientos corno re- ducciones en tin analirador sintáctico SILR(1) (dependiendo de la búsqueda hacia tielanre), cada ingreso en la sección de entrada tlebe tener ahora una etiqueta de "<lesplararriiento" o "reducción", y las selecciones de regla gramatical también deben scr colocadas en entradas tnllrcadas "reducción". Eso también provoca que las colurnnas de accióti y reglas sean innece- sarias. Puesto que el símbolo $ de fin de entrada también puede ser uno de búsqueda hacia delante Legal, se dehe crear una nuevd columna para este símbolo en la seccióri de entrada. Deniostraremos la construcción de la tabla (le análisis sintáctico SI,[<( 1) junto con nuestro primer ejemplo de anilisis S[.R(I).

Ejemplo 5.10 Consideremos la gramática del ejemplo 5.8, cuyo DFA de conjuntos de eleiiieiitos esti da- do en la figura 5.4. Corno establecirnos, csta gramática rlo es I,R(O), pero es SLR(1.). Los conjuntos Siguiente para los no terminales son Siguiente(E') = ( $ 1 y Sigtiiente(LJ) = ($, + ) La tabla de análisis sintictico SLR(1) se proporciona en la tabla 5.5. En la tabla se indica un desplazamiento mediante la letra s en la entrada, y una reducción mediarite la letra r. De este rnoilo, en el estado I en la entrada + se indica un desplaza~niento, junto con una transición al estado 3. En el estado 2 en la eiitratlii +, por otra pirte, se iiidica una reducción niediante la pr«ducción E -+ n. También escribimos la acción "aceptar" en el estado l en la eritrada $ en lugar de r (E' --. E).

Fi~ulizanios este ejcinplo con un m d i s i s sintáctico de la c:~der~a n + n + n. f.os piisos tlcl niiilisis sintáctico se proporcioii;iri cn la i;thl;t 5.6. El paso I de esta 1igur:i coniienza en cl cst;~tlo O cn el token ile c~itr;i(lo n. y Iii t;tt)l;i de ;~irdisis .;int:ictico ir~tlica la ;iccicin "S?",

Tabla 5 . 5 Tabla de análisis sintáctico Estado Entrada Ir a SLR(I) para el ejemplo 5.10

n + $ E

o I 1 - 3 4

s2

s4

s3 r (E-+ n)

r ( E - t E + n )

aceptar r (E+ n)

r t E - K + n )

I

Page 216: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tabla 5 6 Atc~ones de analis~r rintáctico para el ejemplo 5.10

I 2 3 -1

5 6 7 8 9 -

CAP. S I ANALISIS SINTACTICO AICENDENTE

es decir, desplazar el token a la pila e ir al estado 2. Indicatnos esto en la tabla 5.6 con frase "desplüzar 2". En el paso 2 de la figura, el ;tnalizador sintáctico está en el estado 2 c el tokcn de entrada +, y la tabla indica una reducción mediante la regla E -r n. En este L so el estado 2 y el token n son extraídos de la pila, exponiendo al estado O. Se inserta el S' tioto E y se toma la acción Ir a para E del estado O al estado l . En el paso 3 el analiza está en el estado 1 con el token de entrada +, y la tabla indica un desplazamiento y una t sición al estado 3. En el estado 3 en la entrada n la tabla también indica un desplazarni y una transición al estado 4. En el estado 4 sobre la entrada + la tabla indica una reducci mediante la regla E -+ E + n. Esta reducción se cumple al extraer de la pila la cadena E + sus estados asociados, exponiendo nuevamente el estado O, insertando E y Ilevaiido Ir a estado l . Los pasos restantes en el análisis son semejantes.

Pila de análisis sint&ctico Entrada Accidn

desplazamiento 2 reduccih E + n <lesplazamiento 3 desplazatniento 4 reducción E -, E + n dcsplazainiento 1 desplarairiiento J sedncciiín E -+ E + n aceptar

Ejemplo 5.1 l Considere la gramática de los paéntesis balanceados, cuyo DFA de elementos LK(0) est dado en la figura 5.3 (página 105). Un cálculo directo produce Siguiente(S8) = { $ J y guiente(S) = {$. ) J . La tabla de ariálisis sintáctico SLR(1) se proporcionan en la tabla 5. Advierta cómo los estados no-LR(O:) 0 ,2 y 4 tienen tanto desplazamientos como reducci nes mediante la producción E S -+ E. La tabla 5.8 proporciona los pasos que el algoritmo aiiálisis sintáctico SLR(1) lleva a cabo para analizar la cadena ( ) ( ). Observe cómo la pi continúa creciendo hasta las reducciones findes. Esto es caractenstico de los analizadores sin tácticos ascendentes en presencia de reglas recursivas por la derecha tales como S -+ ( S De esta manera, la recursión por la derecha puede ocasionar la sobrecarga de la pila, y se debe evitar si es posible.

rabli 5 1 [&la de analsir rtntáa~co Estado Entrada Ir a ilR(1) para el ejemplo 5.1 1 - '

( ) F 5

Page 217: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Análisis iintactico SLR( 1 )

Ejemplo 5.12

Pila de analiris sintactico Entrsda Accion

<icspl:irarnicnto 2 redtic~i6n S i i. dcspic~ratniento 4 desplatamiento 2 reducción S -i rr dcsplararniento 4 reJucci6n S -+ rr

rediicci6n S -+ (.Y ) S rrtiiiccidn S -t (S) S aceptar

5.3.2 Reglas de eliminación de ambigüedad para conflictos de análisis sintactico

1.0s conflictos de anilisis en el análisis sintáctico SLK(li, como con todos los métodos de ;iriálisis sintdctico de rcducciíin por desplazamiento. pueden ser de dos clases: conflictos de reducción por desplazamiento y conflictos de reducción-reducción. En el caso tle conflic- tos de reducción por desplazamiento existe una regla de eliminación de artibigiiedad riatu- ral, que consiste en preferir siempre el desplazantiento en ver de la reducciíin. 1.a mayoría de los analizadores sintácticos de reducción por des&a~miento resuelven, por lo tanto. rle manera autotnitica los contlictos de reducción por desplaraniiento prefiriendo el despla~a- miento a la reducción. El caso de los conflictos de reducci6n-reducción es mis difícil: tales

.c«ntlictos con frecuencia (pero no siernpre) indican un error en el diseño de Iü gramática. (Mis - ;tdel;tnte se proporcionan ejemplos de este tipo (le conflictos.) Preferir el desplazxniento a la reditcciún en u11 conflicto de redticción por desplazamiento incorpora :iutomática~nerite la regla de anidación más próxima para la ambigüedad del else en sentencias if. como se irtues- tra en el siguiente ejemplo. Esto es una r a~ón para dejar que la ambigüedad permanezca en una gramática de lenguaje de programación.

Considere la gramilica de sentencias if siinplificrdas utilizada eii los capítulos anteriores (véase, por ejemplo, el capítulo 3, sección 3.4.3):

Puesto que ésta es una gramática ambigua, debe haber un conflicto de análisis etl alguna parte en cualquier ctlgoritmo de análisis sintáctico. Para ver esto en LIII aiialitador SL.R(I )

rirnplific;rmos aún nik In grarnática, lo qiie hace mis nimejable la coiistriicci6ri dcl DFA de conjuntos de clenleiitos. Incluso eliniinainos tiel todo la cxprcsi6n de prueba y escribimos la gmmitica como sigue ítotlavía con la ambigiictlad del clse):

S - + / ] otro I - t i f S / if .Se lae .S

Page 218: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Fiqura 5.6 DFA de conjuntos de elementar ¡&(O) para el ejemplo 5.12

CAP. S I ANALISIS IINTACTICO ASCENDENTE

El DFA de conjuntos de elementos se muestra en la figura 5.6. Para constrtiir las accio del tinzílisis sintictito SLR(1) necesitaruos los conjuntos Siguiente para S e I. Éstos son

Siguiente(S) = Siguiente(0 = {S, else)

Ahora podemos ver el conflicto de análisis provocado por el prohlenia del else amhiguo. presenta en el estado 5 del DFA, donde el elemento'completo 1 -, if S. indica que va a

el elemento 1 - if S. else S indica que va a ocurrir un desplazamiento del toke entrada en else. De este modo, el else ambiguo resultará en un conflicto de reducción desplearniento en la tabla de análisis sintáctico SLR(1). Evidenteinente, una regla de el

ción sohre el desplazamiento, no habna rnanera de introducir los estados 6 o 7 en el DF lo que resultaría en errores espurios de análisis sintáctico.)

S ---. .otro I 4 . i f S

1 - i f .S 1 -----+ i f S e l s e .S 1 -+ i f .S e l s e S

S - .otro S 4 .otro 1 - . i f S 1 - 4 . i f S 1 ---. . if S e l s e S

La tabla de anzílisis sintáctico SLR(1) que resulta de esta graniitica se muestra en la tabla 5.9. En esa tabla utilizamos un esquema de numeración para las selecciones de regla gramatical en las acciones dé reducción en lugar de escribir las reglas misinas. [.a nume- ración es

(1) S-+¡ (2) S - t otro (3 ) 1 4 if S (1.) ¡ -- if S else .S

Page 219: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Advierta que no es necesario numerar la produccicín de aurnento S' -t S, puesto que tina reduc- ción mediante esta regla corresponde a la aceptacicín y se escribe como "acepta$' en La tabla.

Advertimos al lector que los núrneros de producción utilizados en entradas de reducciitn son ficiles de confundir con los números de estxlo empleados en las entradas para dcspla- zarniento y direccionaniiento (Ir a). Por ejcinplo. en la labla 5.9, en el estado 5 la entrada bajo el encabeiatlo de entrada else es s6. lo cltie indica un desplazainieuto y una traiisicicin al estado 6, mientras que la entrada bajo el encabezado de entrada $ cs r3, lo que iiidica una reducción inediante el número de producción 3 (es decir, 1 -. if S).

En la tabla 5.9 también elimin~nws el cooflicto de rediicciiín por desplazamiento en la tabla en favor del desplazamiento. Sortibreamos la entrada en la tabla para mostrar dónde se habría presentado el conflicto.

533 Límites de la potencia del análisis sintáctico SLR(1)

Estado (1) para el ejemplo 5.12

-

0 I 2 3 4 5 6 7

El análisis sintáctico SLR(1) es una extensión simple y efectiva del an6lisis LR(0) que cs tan potente que puede controlar casi cualq~iier estructura prictica de lenguaje. Por desgracia, existen algunas situaciones en las que el análisis sintúctico SLR(1) no es lo suficientetnrnte pwleroso, esto nos coiiducirá a estudiar el análisis sintáctico LAI.R( 1) y LR(1) general coi1 más poder. E l ejemplo siguiente es una situación típica en la que falla el análisis sintáctico SLK(1).

Ejemplo 5.13

§

Considere las siguientes reglas gramaticales para sentencias, extraídas y sirnplific:idas de Piscal (una situación seinejante ocurre en C):

Entrada

sent -+ llamad-sent / sent-trsignac Ilrrrnud-sent 4 i d e n t i f i c a d o r st~nt-asignric + w r : = cxp r i r [ e . 1 / i d e n t i f i c a d o r <'.Y!> --t wr 1 número

i f

S&

s4

s4

Ir a

S

1

5

7

else

1- l

r2

y6

r4

1

- 1

2

2

otro

~3

$3

s3

--

$

aceptar r I r 2

r i

r4

Page 220: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

., .,~* .S ,a .,:. . .

CAP. S 1 ANALISIS SINTACTICO ASCENDENTE :<:.

.A<

. :-

sentencia es una asignación o una llamada. Simplificamos esta situación para la grarná siguiente, donde eliminamos las opciones alternativas para una variable, e hicimos rn ples las opciones de sentencias, sin modificar la situaciún básica:

S - + i d I V : = E V - + i d E-;.vln

Para mostrar cómo esta gramática produce un conilicto de análisis en el análisis sintác SLR( 1 ) considere el estado inicial del DFA de los conjuntos de elementos:

S' -+ .S S-. . i d S - + . V := E V - t . i d

Este estado tiene una transición por despl~tzamiento sobre i d al estado

V-+ i d .

Ahora, SiguienteíS) = ( $ 1 y Siguiente(V) = { : =, $ ) f : = debido a la regla V-+ V : =E , porque un E puede ser un V) . De este modo, cl algoritmo de análisis sintáctico SLR(I una reducción en este estado tanto por la regla S -+ i d como por la regla V -+ i d h símbolo de entrada $. (Éste es un conflicto de reduccicín-reducción.) Este conflicto análisis sintáctico es en realidad un problema "falso" ocasionado por la debilidad del todo SLR(1). Efectivamente, la reducción mediante V-+ i d nuncci debería hacerse en estado cuando la entrada es $, puesto que una variable nunca puede presentarse al final una sentencia hasta después que el token : = se haya visto y desplazado.

En las dos secciones siguientes mostraremos cómo se puede eliminar este problema de análisis mediante el uso de métodos de análisis sintácticos más poderosos.

5.3.4 Gramáticas SLR(k) Como con otros algoritmos de análisis, el algoritmo de análisis sintáctico SLR(1) se p extender al análisis sintáctico SLR(k) donde las acciones de análisis están basadas en k sírnbolos de búsqueda hacia delante. Utilizando los conjuntos Primerok y Siguientek c se definieron en el capítulo anterior, un analizador sintáctico SLRlk) utiliza las sigui dos reglas:

1. Si el estado .r contiene un elemento de la forma A -+ a. X p, (donde .Y es un token) y Xw E Primerok(X P) son los siguientes tokens k en la cadena de entrada, entonces la acción es desplazar el token de eutrxia actual a la pila, y el nuevo estado que se insertará en la pila es el estado que contiene el elemento A -t a X . P.

2. Si el estado s contiene el elemento completo A -+ a., y rv E Sig~iente~íil) son los siguientes k tokens en la cadena de entrada, entonces la itcción es reducir mediante la regia A -. (Y..

El análisis htáctico SL,R(k) es rnás poderoso que el anilisis SLK(I) cuando k > 1 : pe- 1.0 con un costo sustancial en cornpkjidad, ya que la i:iblii de anilisis siritictico ;iirmcnta cu- ponrncinliiiente su L;irnaño sespecto ;I k. 1.3s c«nstri~ccioncc de lenguaje tipic;ts y e no son

Page 221: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]
Page 222: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. S 1 ANALISI$ SINT~CTICO ASCENDENTE

Observe que cn este caso la misma a de búsqueda hacia delante aparece en ambos elem tos. De este modo, estas transiciones no provocan la aparición de nuevas búsquedas hac delante. Sólo las transiciones E "crean" nuevas búsquedas hacia delante de la manera s i guiente.

Definición

Definición de transiciones L R ( f ) (parte 2). Dado un elernento LR( l j [A - a.B y. ti], don- de B es un no terminal, existen transiciones E para elementos 1.B -+ .& h] para cada produc- cicín A -+ p y ptrm cutfu tokeiz b en Primero(yu).

Advierta có~no estas transiciones E se mantienen al tanto del contexto en e l cual se necesi ta reconocer la estructura B. Efectivamente, el elemento 1.4 -+ a.B y, u1 dice que en est punto del análisis sintáctico podemos querer reconocer una pero .xílo s i esta B está segu da por una cadena derivable de la cadena y(¡, y tales cadenas deben conienzar con un toke en Prirnero(yu). Puesto que la cadena y sigue a B en la producción A -+ aNy, s i n se cons- truye para estar en Siguiente(A), entonces Primerofya) C SiguientefB), y las h en los ele- mentos [N -+ .f3, b] siempre estarán en Siguiente(3). La potencia del método I,R(I) general reside en el hecho de que el conjunto Prirnero(yu) puede ser un subconjunto propio de Si- guiente(@. (Un analizador SLR(I) tonta esencialmente las búsquedas hacia delante b del con.junto Siguiente completo.) Advierta tanibién que la búsqueda hacia delante original a :tparece como una de las h sólo s i y puede derivar la cadena vacía. Gran parte de las veces (especialmente en situaciones prácticas), esto ocurriri sólo s i y es s misma, s i esto sucede obtenemos el caso especial de una transición e de [A -+ a.B, al a [B -+ $3, a ] .

Para completar la descripción de la construcción del DFA de los conjuntos de elemen- tos LR(I) resta especificar el estado inicial. Corno en el caso LR(O), hacemos esto aurnentan- do la gramitica con un nuevo sínibolo inicial S' y una nueva producción S' -. S (donde S es el símbolo inicial original). Entonces el símbolo inicial del NFA de elementos LR(1) se convierte en el elemento [S' -+ .S, $1, donde el signo monetario $ representa e l marcador de

-. ,-< terminación (y es el único símbolo en Sigiiiente(S1)). En efecto, esto nos dice que comenza- :;g .. r i m reconociendo una cadena derivable de S, seguida por el símbolo $. y

4 Ahora examinaremos varios ejemplos de la construcción del DFA de elementos LR(I). .

fjemplo 5.14 Considere la gramática

del ejemplo 5.9. Iniciamos la constnicción de este DFA de conjuntos de elementos LR(I) atnnentando la gr~imática y formando el elemento LR(I) inicial [A' + .A, $1. La cerradura s de este elemento es el estado inicial del DFA. Comoit no está seguida'por ningún símbolo en este elemento (en la t&rninología del análisis anterior acerca tle transiciones, la cadena y es E). existen transiciones F para 10s elerne~itos [A -+ . ( A ), $ 1 y [A + .a, $ 1 (es decir, Pri~nero($) = ($1, en la Lerminología del análisis anterior). El estado inicial (estado 0) es entonces e l conjunto de estos tres elementos:

Page 223: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Rnáiis~r s~ntáctico MLR(I) y LR( I ) general 21'

( h t e cs c l estado tlcsdc d cual e l ;ilgoritiiio de air5lisis sititáctico LR(1 ) generririí las x c i o - ties de aceptaciúti.)

A l regresar al estado 0. existe tariibién una trarisicih con el token ( para la cerradura del conjui~to que es t i conipuests del elernenio [ A -, ( . A ), $1. Debirlo a que existen transicio- nes c: desde este elemento, esta cer~tdura es IIO trivial. En efecto, existen transiciones e des- de este elemento n los elcirientcis [A 4 . ( A ) , ) / y LA -+ .a. ) 1. Esto e debe a que. en el eleiiicrito [A -t (. A ), $1, A. se está recoiiocientlo en el lado derecho en el cor~tr.rto de los ~~<trhrfr,vis. Es decir, el conjunto Siguierite de In A del lado derecho es PrinreroO $) = ( ) 1 De cste tnodo, tihtoviinos un ~ iuevo tokeii de hiisquetla hacia ilel;inte en esta situación, y e l nuevo estado DFA se compone de los elementos siguientes:

Regresamos una vez m i s al ~ s r ~ i t l o 0, tlonde enconvanitts una última tK~risici6ii al eslatlo gerteradu por el clernerito [A -+ a., $1. Y ya que esto es un elerrreiito corripleto. es un esta- tlo l le un elemento:

Ahora volveinos al esrado 2. Desde este estado existe uria transición cori /\ para Iri ce- rradtira +: de [ A -+ ( A .) , $1, que es 1111 estado con un elemento:

Ttinibién existe una trrinsicih coii ( a la eerraJi~r:i e de [A + (.A ). )l. Aquí tanibién gene- rmios los elenientos tle cerradura [ A -t . ( ,4 ), ) 1 y [ A + .a, ) 1, por las i r i i sms razones queen la coilstrucci6n del estado 2. Por corisiguiente, ubtcnenios el nuevo estado:

t2civicrtrt que cste estatio es el mismo que el estado 2, excepto por la búsqeda hacia delan- te del primer elerrrerito.

Finrilniente, teiiemos una trmsición con el token a del estado 2 al estado 6:

Ohserve de nueva cuenta que esto es casi l o misino que el esrado tres, excepto por I:i hús- qi ieth Iiacia deliiirte.

'1 siguiente estatlo qtic l i c w ~III;~ traiisici011 es el cstiitlo $. que tictte utia ri.;iir\icih~ ciin el tokcn ) al c.\tado

Page 224: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

:igura 51 El DFA de conjuntos

1e elementos M(!) para ?I ejemplo 5.14

CAP. S 1 ANALISIS IIHTÁCTICO ASCENDENTE

Al regresar al estado 5 , tenemos una transición de este estado hacia sí ntisrno con (, un transiciiin con A al estado

Estado 8: [ A + ( A . ) , ) ] y una transici6n al estado 6 ya construido con a.

Finalmente, existe una transición en ) del estado 8 a

Estado 9: [A -+ ( A ) ., ) 1.

De este modo, el DFA de elementos LR(1) para esta granática tiene diez estados. El DF conipleto se muestra en la figura 5.7. Al comparar esto con el DFA de los conjuntos (le cle- mentos LR(0) para la misma gramática (véase la figura 5.5, pagina 208), observamos q el DFA de elementos LR(1) es casi el dohle.de extenso. Esto no es inusual. En realidad, número de estados I.R(l) puede exceder el número de estados LR(0) por un factor de 10 situaciones complejas con niuchos tokens de búsqueda hacia delante.

5.4.2 El algoritmo de análisis sintáctico LR(1) Antes de considerar ejemplos adicionales necesitamos cornl ~letar la exposicih acerca del trndisis sintActico L R í I ) rnediantc cl rcestableciiriiento del :rlgoritmo de an5lisis sin- ticrico basado en la nueva construccih DFA. Esto es frícil de hacer, puesto que sólo es un reestablecitnieiito del algoritino (le aiiálisis sintríctico SLR(I), excepto porque utiliza los tokens de húsqut.da hacia (lelante en los eletnenros L.Rt 4 ) en lugar de los conjuntos Siguiente.

Page 225: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]
Page 226: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

222 CAP. S 1 ANALISIS S ~ N T ~ C T ~ C O ASCENDENTE

sintactico LR(1) general

la construcción explícita de la tabla de análisis sintáctico, porque se puede obtener muy cilmente a partir del DFA.

LR(I), a menos que sean ambiguas. Naturalmente, se pueden construir ejemplos de gram

los ejercicios). Tales ejemplos tienden a ser artificiales y por lo regular se pueden evitar e situaciones prácticas. De hecho, un lenguaje de programación rara vez necesita el poder q proporciona el análisis sintáctico LR(1) general. En realidad, el ejemplo que utilizamos p

tanto, también SLR(1)).

para ser SLR(I).

Ejemplo 5.16 La gramática del ejemplo 5.13 en forma simplificada es como se presenta a continuación:

Construimos el DFA de conjuntos de elementos LR(L) parü esta gramática. El estado inicial es la cerradura del elemento [S' -+ .S, $1. Contiene los elementos [S -. .id, $1 y [S -+ .V : = E, $1. Este último eleniento también da origen ü1 elemento de cerradura 1V-t .id, : -1, puesto que S -+ .V : = E indica que se puede reconocer una V, pero sólo si está seguida por un token de asignacibn. De este modo, el tvken : = aparece como la búsqueda hacia delan- te del elemento inicial V-, .id. En resumen, el estado inicial se compone de los siguientes elementos LR(I):

E~ tado O: [S' + . S, $1 [S-. .id, $ [S- . V : = E , S ] [V - i .id, : =1

Page 227: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Análisis rintáctico LALR(1) g LR(l) general

A partir de este cstado existe una trmsición con S al estado de un elernento

Estado I : [S' -+ S .. $1

y una t r m s c i h coi1 id al emdo de dos elementos

También existe una transicibn con V del estado O al estado de un elemento

Estado 3: [S + V . : = E , $1

Desde los estados I y 2 no hay transiciones, pero existe una transición del estado 3 con : = a la cerradura del elemento [S -+ V : =. E, $1. Como E no tiene símbol«s siguiéndolo en es- te elemento, esta cerradura incluye los elementos [ E -+ .V, %] y [ E -+ . n. $1. Finalmente, el elemento [E-+ .V, $1 conduce al elemento de cerradura [V-+ .id, $1, ya que V tariipoco tie- ne símbolos siguiéndolo en este caso. (Compare esto con la situación en el estado 0. donde una Vestaba seguida por una asignación. Aquí V no puede estar seguida por una asignación, debido a que ya se vio un token de asignación.) El estado completo es entonces

Los estddos y las transiciones restantes son fáciles de construir, y las tlejarnos para que el lector las efectúe. El DFA completo de los conjuntos de elementos LR(1) se muestra en la figura 5.8.

Ahora consideremos el estado 2. Éste era el estado que daba origen al conflicto del análisis sintáctico SLR(1). Los elementos LR(1) distinguen clüramente las dos reducciones mediante sus búsquedas hacia delante: reduccibn mediante S -+ id con $ y mediante V --t id con : =. Por consiguiente, esta gramática es LR(1).

el ejemplo S 16

Finura 5 8 / ! S ' - . s . $ ¡ El DFA de conjuntos - de elementos LR(1) para

S [S' - s . ,$i [ y * .id, $1 [S - . " : = E . $ ] 0

Page 228: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. S i ANALISIS IINTÁCTICO ASCEMDENTE

5.4.3 Análisis sintactico LALR(1) El análisis sintzíctico LALR(1) es16 basado en la observaci<in de que, en muchos casos, el ta ño del DFA de conjuntos de elementos LR(1) se debe en parte a la existencia de mucho krdos diferentes que tienen el mismo con,junto de primeros componentes en sus eleme (los elementos LR(O)), mientras que difieren sólo en sus segundos componentes (los sím los de búsqueda hacia delante). Por ejemplo, el DFA de los elementos LR( 1) de la figura 5.7 ti ne 10 estados, mientras que el DFA correspondiente de los elementos LR(0) (figura 5.5) tie s6lo 6. Efectivamente, en la figura 5.7 cada estado de los pares de estados 2 y 5 , 4 y 8, 7 3 y 6, ditkren del otro sólo en los componentes de búsqueda hacia delante de sus element Considere, por ejemplo, a los estados 2 y 5. Estos dos estados se distinguen sólo en su pn elemento, y únicamente en la búsuuedü hacia delante de tal elemento: el estado 2 tiene el rner elemento [A + ( . A ) , $1, con $ como búsqueda hacia delante, mientras que el estad tiene el primer elemento [A -+ (. A ) , ) 1, con ) como búsqueda hacia delante.

El algoritmo de antílisis sintáctico LALR(1) expresa el hecho de que tiene sentido i tificar todos esos estados y combinar sus búsquedas hacia delante. Al hacerlo así, siem debemos finalizar con un DFA que sea idéntico al DFA de los elementos LR(O'), except cada estado se compone de elementos con conjuntos de búsqueda hacia delante. En el de elementos cornuletos estos conjuntos de búsqueda hacia delante son frecuentemente pequeños que los correspondientes conjuntosSiguiente. De este modo, el análisis sintácti LALR( 1) retiene algunos de los beneficios del análisis LR(1). mientras que mantiene el t inaño más pequeño del DFA de los elementos LR(0).

Formalmente, el núcleo de un estado del DFA de elementos LR(1) es el conjunto de e rnentos LR(0) que se compone de los primeros coinponentes de todos los elenientos LR(1) el estado. Puesto que la construcción del DFA de los elementos LR(1) utiliza transiciones son las mismas que en la construcción del DFA de los elementos I,R(O), excepto por sus e . .

tos sobre las partes de búsqueda hacia delante de los elementos, obtenemos los siguientes d hechos, que forman la base parta la construcción del análisis sintáctico LALR(1).

PRIMER PRINCIPIO DEL ANALISIS S ~ N T ~ C T ~ C O LALR(1)

El núcleo de un estado del DFA de elementos LR(I) es un estado del DFA de elemen- tos LR(0).

SEGUNDO PRINCIPIO DEL ANALISIS SINTACTICO LALR(1)

Dados dos estados sl y s2 del DFA de elementos LR(I) que tengan el mismo núcleo, ponga que hay una transición con el símbolo X desde ,rl hasta un estado rl. Entonc existe también una transición con X del estado s2 al estado t2, y los estados tl y tZ tie el niisnio núcleo.

'Tomados juntos, estos dos principios nos permiten construir el DFA de los element LALR(I), el cual se construye a partir del DFA de elementos LR(1) al identificar todos 1 estados que tienen el mismo núcleo y formar la unión de los símbolos de búsqueda hacia d lante p a a cada elemento LR(0). De este modo, cada eleniento LALR(1) en este DFA ten& un elemento LR(0) como \u pnmer componente y un conjunto de tokens de búsqueda hacia 4 delante como su segundo componente.5 En ejemplos posteriores denotaremos las búsque- das hacia dclante múltiples escribiendo una / entre ellas. Así, el elemento I.ALR( 1) [A -+ u$, a / h / c ] tiene un conjunto de búsqueda hacia delante compuesto de los sírnbolus a, b y c.

- 5. Los DPh de elcrneritos LR(I). de hecho. tnmbiéii podrían utilirar conjuntos de sírnbolos de hús-

ili~cúa ha& adelante para rcpresmtar clenientos niúltiplrs en el mismo estado cn qiiz comprten sus pri- meros coinponerircs, pero ionsidcr;irnos conveniente einplear esta representüciím para la coiistruccih l..Al.X( 1 ). donde cs iiiís :ipropia<lo.

Page 229: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Aniliris iintácttco L A L R ( I ) y L R ( I ) general

Proporcionareiiios un tjcnq?lo para demostrar esta construcsióri.

figura 5 9 f l DiA de conjuntos de dementor MLR(l) para el epmplo 5 17

Considere la gramática del +mplo 5.14, cuy<) DFR de elementos LK( I ) se dan en la figu- ra 5.7. La identificación de los estados 2 y 5, 4 y 8. 7 y 9, 3 y 6 proporciona el DFA de los elementos LALR(I) en la figura 5.0. Conservamos en esta figura la numcracicin de los es- tados L. 3.4 y 7, y agrccamos las bíisquedas hacia delmte de los estados 5.6. 8 y O. Corno csperábainos, este DFA es idéntico al DFA de los elenie~itos LRíO) (figura 53) . excepto por las búsquedas hacia delante.

El algoritmo para el análisis sintáctico LALR(1) utilizando el DFA condensado de los elenientos LALR(I) es idéntico al algoritmo de análisis sintáctico LR( 1) general descrito en la sección anterior. Como antes, denontinarnos gram6tica LALR(1) a una gramática si no surgen conflictos de análisis en el algoritmo de análisis sintáctico LALR(I). Para la cons- tnicci6n LALR(1) es posible crear contlictos de análisis que no existen en el análisis sintác- tico LR(1) general. pero esto rara vez ocurre en la práctica. En realidad. si tina gramática es LR(l), entonces la tabla de análisis sintáctico LALK(1) no puede tener ningún conflicto de reducción por desplazamiento; no obstante, puede haber conflictos de reducción-reducción (véase los ejercicios). Sin embargo, si una gramática es SLR(I), entonces por supuesto es LALR(L), y los analizadores LALR(1) a menudo funcionan tan bien como los analizadores LR(1) generales en la eliminación de conflictos típicos que se presentan en el análisis sin- táctico SLRí 1). Por ejemplo, la gra~tiática no SLR(1) del ejemplo 5.16 es LALR(1): el DFA de los elementos LR(1) de la figura 5.8 es también el DFA de los elementos LALR( 1). Si, como en este ejemplo; la graniatica ya es LALRf l), la única consecuencia de utilizar el aná- lisis sintictico LALR(1) en vez del análisis sintáctico LR general es que, en presencia de errores, pueden hacerse algunas reducciones espurias antes que se declare un error. Por ejemplo, en la figura 5.9 observamos que, dada la cadena de entrada errónea a ) , un anaii- zador sintáctico LALR(I) efectuará la reducción A -+ a antes de declarar el error, iiiieritras que u n analizador sintáctico I.R( 1 ) general declarará el error inmediatamente después de un tlespla~ariiiento del token a.

L a combinación de estados LR(I) para forriiar el DFA de los elementos LALR( 1 ) re- srielvc el prohleina (le las t;thlas de análisis sintáctico grandes, pero todavia requiere del »FA coinpleto de elenientos LK( 1) paracalcularse. De hecho, es posible calcular el DF:\ dc Iiis clcnientos 1.ALRI I ) directamente del D F h de elementos LK(O) a irav6s de u n pso- ceso (le propagacih de híisqiredas f~acia delante. ,Zuiiqiie no clcscsihircti~os tste pi.i~ce<o

Page 230: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. S / ANALISIS SINTACTICO ASCENDENTE

de manera formal, es formativo ver c6mo se puede hacer esto de manera relativam l'icil.

Considere el DFA LALR(I) de la figura 5.0. Camcnzarnos construyendo búsque cia delante al agregar el rnarcador de finalización $ a la búsqueda hacia delante del el to de aumento A' -+ .A en el estado O. (Se dice que la búsqueda hacia delante $ se espontáneamente.) Entonces, por las reglas de la cerradura E, el $ se propaga a Los iientos de cemidura (la A en el lado derecho del elemnto de núcleo A' -+ . A es segiri la cadena vacía). Al seguir las tres transiciones desde el estado O, el $ se propaga a lo mentos de núcleo de los estados 1 , 3 y 2. Continuando con el estado 2, los element rradura obtienen la búsqueda hacia delante), nuevamente por generación espontánea a que A en el lado derecho del elemento de núcleo A -+ (.A ) viene antes de un p derecho). Ahora la transición en a al estado 3 causa que el ) se propague a la búsque cia delante del elemento en ese estado. 'También, la transicih con ( del estado 2 h mismo causa que el ) se propague a la búsqueda hacia delante del elemento de núcle se explica por qué el elemento de núcleo tíene tanto $ como ) en su conjunto de b hacia delante). Ahora el conjunto de búsqueda hacia delante $0 se propaga al estad terionnente d estado 7. De este modo, a través de este proceso, obtuvimos el DFA d mentos LALR(I) de la figura 5.9 directametite del DFA de elementos LRJO).

5.5 Yacc: U N GENERADOR DE ANALIZADORES SINTÁCTICOS iALR(1 ) Un generador de analizadores sintácticos es un programa que toma corno su entrada una :i '. . especificación de la sintaxis de un lenguaje en alguna forma, y produce como SU salida unr:2 procedimiento de análisis sintáctico para ese lenguaje. Históricamente, los generadores de.; analizadores sintácticos fueron llamados compiladores de compilador, debido a que todos los pasas de compilación eran realizador de manera tradicional como acciones inclui dentro del analizador sintáctico. La visión moderna es considerar al itnalizador cotrio s una parte del proceso de compilación, de modo que este término se volvió obsoleto. Un nerador de analizadores sintácticos ampliamente utilizado que incorpora el algoritmo análisis sintáctico LALK(1) se conoce como Yacc ("Yet Another Compiler-Compile decir, "otro compilador de compilador mis" en inglés). En esta sección daremos una pectiva general de Yacc, y en la sección siguiente lo utilizarenios para desarrollar un ana dor sintáctico para el lenguaje TINY. Debido a que existen varias implernentüciones d rentes de Yacc, además de varias versiones del dominio público comúnmente denomina ~ i s o n , ~ existen nunicrosas variaciones en los detalles de su operación, las cuales pueden Ferir en alguna forma de la versión que presentamos aquí.'

5.5.1 Fundamentos de Yacc Yacc toma un archivo de especificación (por lo regular con un sufijo .y) y produce un chivo de salida compuesto del código fuente en C para el analizador sintáctico (por lo neral en un archivo denominado y. tab. c o ytab. c o, más recientemente <nombre d archivo>. tab. c, donde <nombre de archivo>. y es el archivo de entrada). Un archivo de especificación de Yacc tiene el formato básico

6. ünn vcrsi6n popular, G n u Bison, Swnia parte del ~oftware Griii <lile distribuye la Frce Software Foundotion: véase la secci6n de notas y referencias.

7. Efectivmricrite, utilizanios varias versiones diferentes para gelirir;ir los cjjeniplo\ posccriolrs.

Page 231: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Yaci: un generador de analizadorei itnrácator LALR(1)

(definiciones) %%

{reglas)

%%

{rutinas auxiliares)

[>e este niodtr, existen tres secciones (la sección cle definiciones, la seccitin de reglas y la ice- c i k de rutinas auxiliares) scparüdas niediante Iíncab que coniicneii doble signo de prircciilei~e.

IA seccitiri de definicicmcs contiene itiliirinación acerca de los rokens, tipos de k i tos y reglas grarnatic;iles que Yace ~ieccsita para cot i t ru i r el ;inali/ador sintcicticct. 'Tatiibi6ri in- cluye crialqriier ctidigo en C que debería i r direcianiente en el archivo tlc sdicla a su inicio (sobre todo directivas #include de otros archivos de ctidigo firente). Esta secciiíir del itr- chivo de cspeciiic:ii:icíii prietle estar v~rcía.

sección de reglas contiene reglas gr.tiiiaticnles en una k x m a BNF modificada. junto coi1 acciones cti ccídigo C que se ejecutarciri sicnipre que se reconozca la regla graiiiatical asociada (es decir, se emplee en tina reduccitin, de acuerdo con el algoritnio de tniilisis sin- ~:íctico LAL.R(I)). [.as convenci«ries de riietasínibolos iitilizcidas en Iiis reglas graniaticides wi de la nrntierli siguiente. Coriio es habitual, la barra vert icd se r i t i l i ~ a para las altertiati- vas (las alicrnatic~is tatnhi6n se puedcti escribir por separado). E1 sitiiholo de flecha -+ que licnios enipleado para separar los lados izquierdo y derecho de una regla grariinrical se reen- plaza en Yace por un sigtio de dos puntos. Tainhién un signo tle ptiriio y ctrriia dehe finalizar cada regla grlirn~tic:tl.

1.a tercera sección. de r~itinas auxiliares. coritiene tleclnraciiriit:~ de procediitiientos y frincioiies que (le otra Iniinera p~tederi no esi:tr disponibles 21 través de ;irchivos #include y que son nwemrias pare complctnr el analizador sintlictico y10 el compilaclor. Estii sección puede estar viicía. y s i éste es el c:iso se puede otnitir el scgitndo ~iietnsínrholo clc porceiiiiije doble del archivo de especificación. De esta iiiancra, iiii ;irchivo de cspecificacióri iníniti it i de Yace consistiría scílo (le %% seguidos por reglm grarriaticales y acciories (las :icciones t:unhién se pueden rrntitir s i sólo ir;rt;inios (le malizi ir la gr;ini:ític;i. tlo lema que se tmtn pos-. teriornieiitc e11 esta sccci6n).

y, A L . . tcnnbihi perniite insertar conientzirios al estilo de (? en el archivo de espccif icacih

en cualquier punto donde no intcrfierari con el Sorniaio hisico. E.~plicarcnicis el contenido clel :irchivo de especificzrcicín de Yücc con mcis dci;ille util i-

,anclo LIII t$miplo siiiiple. El ejemplo es una calcu1ador;i de expresiotles ariirn6tictis enteras sin>ples con la gn\iii;ítica

Page 232: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

228 CAP. 5 1 ANÁLISIS IINTACTICO AICElDENTE

co,mmand : exp I printf(""ad\n",$l); 1 ; / * permite la impresión del resultado * /

exp : exg ' + ' term { $ $ = $1 + $3;) I exg ' - ' term ( $$ = $1 - $3;) I t e m ( $ S = $1;)

term : term ' * ' factor ( $ S = $1 * $3;) I factor ( S $ = $1;)

int yylex(void)

í int c; while((c = getcharo) == ' ' ) ;

/ * elimina blancos * / if ( isdigit(c) ) (

ungetc(c,stdin);

scanf("%d",&yylyal);

return(NüMI3ER);

1 if (C == '\n') return O; / * hace que se detenga el análisis sintáctico * /

return(c);

void yyerror(char * S )

( fprintf (stderr, "%s\n",s) ;

} / * permite la impresión de un mensaje de error * /

Page 233: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Yatc: un generador de analizadores s~ntácticos LALR(I) 229

Yacc tiene dos ritaneras de reconocer tokens. Ei i primer lugar, c~ialyuier ca~ íc te r ence- rrad« entre comillas sintples en una regla gramatical ser6 reconocido conio él tnisrno. Así. los lokens de un solo caricter se pueden iricluir directamente en reglas gramaticales de esta manera, como los tokens de operador +, - y * de l a figura 5.10 (así corno los tokens de pa- ri-ntesis). Ei i segundo lugar. los tokens siiribólicos se pueden declxar en una declilracibn % t o k e n de Yacc, cotilo el token m B E R ("número") de la figura 5.10. A tales tokens se les asigna un val t~r nuinérico iiiediante Yacc que ~ i o entre en cotitlicto con ningún valor de cüricter. Tipicaiiicnte. Yacc coiriienza asignando valores de tokeii con el núrncro 158. Yacc inserta estas definiciol~es de tokcn como sentencias # d e f i n e en el c<itligo (le saliilii. De este nod do, en el archivo de salida probabler~icnte e~icoiitr;~rínmos la línea

como respuesta de Y:icc n la declaración % t o k e n NüMBER en el archivo de especificación. Yacc insiste en definir todos los tokens sirribólicos por sí rnismo, en vez (le iriiportar una tle- finición de alguna otra parte. Sin embargo, es posible especificar el valor nutiiérico que se asignará al token escribientlo L I ~ v;ilor después del nombre del token en I:I dec1ar;ición tle token. Por ejemplo, al escribir

se asigna a ~ B E R el valor trurriérico I S (en lugar de 258). En la seccicín de reglas de la figura 5.10 «bscrvamos las reglas para los no terminales

r.rp. tcrrrl y j%c.tor. Como también querernos irnprirnir el valor de una expresión, tenemos una regla adicional, que denoininaremos cr~rnrriirnd, y a la ctial asocianios la x c i ó r i de irn- presicín. Debido a que la regla para cornrnimd se enuniera primero, cor~~ttzcir~rl se toma corno el síiribolo inicial de la gramática. De nianera alternativa. po<lríam«s ti:iber+iiicluido la línea

en la seccibn de definiciones. en cuyo caso no tendríatilos que poner la regla para roritrticiiitt primero.

Las iicciones se especifican e11 Yace al escribirlas c o ~ i i o ccídigo en C real (dentro de llaves) con cada regla grüinatical. De tiranera característica, el código de acción se ubica a1 fiiial de cada seleccicín de regla gr;iiriatic;il (pero antes de la barra vertic;il o del signo de punto y conia), aunque también es posible escribir acciones incrustadas en una selec- ciúi i (comentaremos esto en breve). A l escribir las acciones podemos ciprovecharnos de las pseudovariables de Yacc. Cuando se reconoce una regla gramatical, cada sínrbol~) cn la regla posee un valor, que se supone es un enteru a menos que sea cambiado por e l progra- mador (posteriormente veremos cdmo hacer esto). Esos valores se conservan en una pila de valor niediante Yacc, la cu:d se rtrantieiie paralela a l a pi la de análisis sintictic«. Se puede hacer referencia n c~tda valor [le símbolo en la pi la utilizando una pseudovariable que coiriierrce con un signo de tfr. $$ representa el valor del no termind que se cicaba de reconocer, es decir, e l sirtibolo en el 1:ido izquierdo de la regla grarnotical. Las pseodovaria- bles $1, $2. $3, y así sucesivamente, representan los valores de cada sínibolo en s~iccsión en el lado dcreclio de ia regla graniaticai. De esta ri~anerü, en la figura 5. 11) la regla p r r ~ a - tical y :iccicín

exp : exp ' + O term ( S $ = $1 + $ 3 ; )

Page 234: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 5 1 ANALISIS IINT~CTICO ASCENDENTE

Todos los no terminales obtienen valores mediante tales acciones suministradas por usimio. Los tokens también pueden tener valores asignados, pero esto se hace durante eL pr ceso de análisis Iéxico. Yacc supone que el valor de un token se asigna a la variable yylva la cual se define de manera interna por Yacc. y debe ser asignada cuando se reconoce token. De este modo, en la regla gramatical y acción

, . . factor : WNBER { $ S = $1;)

ei valor $1 se refiere al vaior del token NOMBER que fue ~tsignado previamente a yylv cuando se reconoció cl token.

La tercera sección de la fígura 5.10 (la sección de nitinas auxiliares) contiene la definici de tres procedirnientos. La primera es una definición de main, que esta incluida de nta que la salida resulímte de Yacc se pueda compilar directamente para un programa ejecuta El procedimiento main Ilania a yyparse, que es el nonrbre que Yacc proporcima al pro. diiniento de atálisis sintáctico que produce. Este procedimiento se declara para devolver valor entero, que es siempre O si el anáfisis tiene éxito, y 1 si no es así (es decir, si se Iia p sentado un error y no se ha realizado recuperación de errores). El procedimiento yypar generado por Yacc llama a su vez a un procedimiento de analizador Iéxico, que se sup tiene el nombre yylex para propósitos de compatibilidad con el generador de analizador léxicos [.,ex (véase el capítulo 2). De esta manera, la especitlcación Yacc de la figura 5.10 tam- bién incluye una definición para yylex. En esta situación particular el procedimierito yylex es muy simple. Todo lo que necesita hacer es devolver el siguiente carácter que no sea bian co, a menos que este carácter sed un dígito, en cuyo caso debe reconocer el token simple de cadcter múltiple N'üMBER y devolver su valor en la variable yylval. La única excepción a esto es cuando el analizador Iéxico ha llegado al final de la entrada. En este caso, el final de la entrada se indica mediante un carácter de "retorno de Iínea" o "nueva Iínea" ('\n' en C), ya que estamos suponiendo que una expresión se introduce en una sola Iínea. Yacc espera que el tina1 de la entrada sea señalado mediante una devolución del valor nulo 0 por yylex (nueva- mente, esto es una convención compartida con Lex). Finalniente, se define un procedimiento yyerror. Yacc utiliza este procedimiento para imprimir un mensaje de error cuando se encuentra un error durante el análisis sintáctico (Yacc imprime específicamente la cadena

f.:<

"syntax error", pero este c ~ m p o ~ m i e n t o puede ser motlificado por el usuario). ,X .Y - :<

5.5.2 Opciones de Yacc Por lo regular habrá muchos procedimientos auxiliares a los que Yacc necesitará tener acceso además de yylex y yyerror, y éstos a menudo se colocan en archivos externos en lugar de directamente en el archivo de especificación Yacc. Es fácil proporcionar acceso a Yacc a es- tos procedimientos escribiendo archivos de encabezado apropiados y colocando directivas #include en La sección de definiciones de la especificación Yacc. Es más difícil dejar dis- ponibles las definiciones específicas de Yacc para otros archivos. Esto es particularmente cier- to para las definiciones de token, las cuales, como hemos dicho, Yacc insiste en generar por sí mismo (en lugar de importarlas). pero que deben esrar disponibles para muchas otras de un compilador (en particular el analizador léxico). Por esta razón, Yacc tiene una opción dis- ponible que automáticamente producirá un archivo de cabecera que contiene esta información, la cud entonces se puede incluir en cualquier otro archivo que necesite las definiciones. Este airchivo (le cabecera por lo regular se denomina y. tab. h o bien ytab. h, y es producitlo con la opción -d (por el térniino :irchivo de cabecera en inglés: "heaDer filc").

Por ejemplo, si I:r especificación Yacc de la figum 5.10 está contenida en el ;~rciii\w calc. y. entonces el coniando

Yacc -d calc . y

Page 235: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 5 l l Una espeafil::acion en eiqueleto Ya« para su uso ton la opcrbn - V

Yacc: iin generador de analizadores iintacticos UILR(I) 23 1

prodrtcirá iai lei i i is del itrcliivo y. tab.c) el archivo y. tab.h I t in non~hre similiir), cuyo co~itenido varía, pero por lo regular incluye cosas colno la sigriicrite:

#ifndef YYSTYPE

#define YYSTYPE int

#endi£

#de£ ine W B E R 258

extern YYSTYPE yylval;

(Ikscribiremos el significado de YYSTYPE con m i s detalle en breve.) Con este ;irchivo se puede cctlocar el ccídigo para yylex cri un archivo diferente insertando la lírica

cri ese ; i r ~ h i v o . ~ Una segunda o p c i h imuy útil de Yacc es la opción verbose (opcibn descriptiva) itcti-

v d a niediarrte la señal - v e n In línea de comando. Esta opciún prodtlce totlavía otro archivo, con c l rioinhre y. output (o uii nombre semejante). Este archivo contiene una descripcicíir textual de la tnhla tle análisis sintáctico LAI.R( 1) que es utilizada por el ari:ilil;idor. L:i lec- tura de este archivo permite al usuario determinar exactamente qué aceicín toniari e l ;riiali- zador sirttictico genera&> por Yacc cri ciialq~iier situ:iciúri. y &t:i puede ser una manera muy efectiva para rastrear üirihigiied:i<les e irrtprecisiones en mta grnntitica. En realidad, cs una hucna idea cjeciitar Y x c coi1 esta i)pciiiii sobre la gramática por sí misma. antes de ;rgregar las acciones r i procedimientos nitsiliares a la especificación, püra asegurarse de que el ana- lizatior sintácticn gencr:tdo por Yacc futici«n;ir~í en realidad coriio se espera.

Cotiio ejernplo corisitlere k i especificación Yacc de Ia figurrt 5.11. Estü es súlo una vcr- si611 escueta de I:I especific;icicín Yacc de la figura S . LO. Ambas especificaciotles generará11 el niisirio archivo de salida cuantlo Yace se utilice con la opción verbose:

yacc -V ca1c.y

%token NIJMBER 0 0 64'

command

exp

l I

t erm

I

factor

I

exp 't' term

exp - term

term

term ' * ' factor factor

NIJMBER

' ( ' exp ' ) <

Page 236: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. S 1 ANALISIS IINTACTICO ASCENDENTE

Un archivo típico y. outgut se lista en su totalidad para esta gratnática en la figur Analizaremos la interpretación de este archivo en los párrafos siguientes.

El archivo de salida de Yacc se compone de un listado de todos los estados en el seguido por un resumen de estadísticas internas. Los estados se enumeran conienzan el O. Bajo cada estado el archivo de salida enumera primero los elementos (le núcl elementos de cerradura no se enumeran), luego las acciones correspondientes a di búsquedas hacia delante, y finalmente las acciones ir a en varios no terminales. Especí mente, Yacc utiliza un carácter de subraya o guión bajo para marcar la posición disti da en un elemento, en lugar del punto que hemos estado utilizando en este capítulo. utiliza el punto en cambio para indicar una opción predeterminada, o un token de hús hacia delante "sin importancia" en la sección <?e acción de cada listado de estado.

Yacc comienza en el estado O exhibiendo el elemento inicial de la producción de au to, que es siempre el único elemento de núcLeo en el estado inicial del DFA. En el arc de salida de nuestro ejemplo, este elemento se-escribe como

Saccegt : -command Send

Esto corresponde al elemento commund' -+ . comrnand en nuestra propia terminología. proporciona al no terminal de aumento el nombre Saccegt. También lista el pseudot de final de entrada de manera explícita como $end.

Examinemos brevemente la sección de acción del estado 0, que sigue a la lista d mentos de núcleo:

NüMñER s h i f t 5

( s h i f t 6

. e r r o r

connnand goto 1

exp goto 2

term goto 3

f ac to r goto 4

La lista anterior especifica que el DFA se desplaza al estado 5 con el token de búsqu hacia delante NUMBER, se desplaza a1 estado 6 en el token de búsqueda hacia dela y declara error en todos los otros token de búsqueda hacia delante. También se enum cuatro transiciones Ir a, para utilizar durante las reducciones de los no terminales dad tils acciones están exactamente como aparecerían en una tabla de análisis sintáctico truida a mano empleando los métodos de este capítulo.

Consideremos ahora el estado 2, con el listado de salida

s t a t e 2

connnand : exp- (1)

e x p : e--+ term

exg : exp-- term

+ s h i f t 7

- shdft 8

. reduce 1

Page 237: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Yacc: un generador de anal~radorer i~n tác t~ tos LALFi(1)

s t a t e O

Caccept : -comand $end

NUMBER s h i f t 5

( s h i f t 6

. e r ro r

command goto 1

exp goto 2

term goto 3

fac tor goto 4

s t a t e 1

$accept : command-$end

Send accept

. e r ro r

s t a t e 2

command : exp-

exp : exp-+ term

exp : exp-- term

+ s h i f t 7

- s h i f t 8

. reduce 1

s t a t e 3

e x p : term- ( 4 )

term : term-* factor

* s h i f t 9

. reduce 4

s t a t e 4

t e r m : factor-

. reduce 6

s t a t e 5

f ac to r : N ~ B E R -

reduce 7

s t a t e 6

f ac to r : (-exp )

NUMBER s h i f t 5

( s h i f t 6

. e r ro r

exp goto 10

term goto 3

f ac to r goto 4

s t a t e 7

exp : exp +-term

NUMBER s h i f t 5

( s h i f t 6

. e r ro r

term goto 11

fac tor goto 4

s t a t e 8

exp : exp --term

NUMBER s h i f t 5

( s h i f t 6

. e r r o r

term goto 12

fac tor goto 4

3tate 9

term : term *-factor

NUMBER s h i f t 5

( s h i f t 6

. e r ro r

fac tor goto 13

Page 238: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 5.12 íronilusión) Un archivo ripico y, output generado por la especifitatión Yacc de la figura 5.10 utilizando la opcion "verbose"

Aquí el elemento de núcleo es un elemento completo, de niodo que habrá una reducció mediante la selección de producción asociada en la sección de acción. Para recordarnos el número de la producción que se está utilizando para la reducción, Yacc muestra el número después del elemento completo. En este caso el número de producción es 1, y hay una acción reduce 1, la cual indica una reducción mediante la producción commtrnd + exp. Yace siempre enumera las producciones en el orden en el que se exhiben en el archivo de especi- ficación. En nuestro ejemplo existen ocho producciones (una para command, tres para exp, dos para tcrm y dos parafactor).

Advierta que la acción de reducción en este estado es una acción predctertnintida: se realizará una reducción en cualquier búsqueda hacia delante diferente de + y -. Aquí Yacc difiere de un analizador sintáctico LALR(1) puro (e incluso de un analizador SLR( 1)) en que no se hace ningún intento por verificar la legalidad de las húsquedas hacia delante en las re- ducciones (tnás que para decidir entre varias reducciones). Un analizador sintáctico Yacc generalmente hace varias reducciones sohre errores antes de declarar finalmente un error (lo cual debe hacer en algún morneiito antes de que ocurra otro iiespl;t~arniento). Esto significa que los mensajes de crror pueden no ser tan iliformaiivos corno deherían, pero la rahla de anilisis siritáctico se vuelve corisider:ibleinente inás sencilla, puesto que se preieiitan titenos casos (este puiito \e volverá a cotnentar en la sección 5.7).

CAP. 5 1 ANÁLISIS IINTÁCTICO ASCENDENTE

state 10

exp : exp-+ term

exp : exp.-- term factor : ( exp-)

+ shift 7

- shift 8

) shift 14

. error

state 11

exp : exp + term- (2)

term : term-* factor

* shift 9

. reduce 2

state 12

exp: exp- term- (3)

term : term-* factor

* shift 9

. reduce 3

state 13

term : term * factor- (5)

. reduce 5

state 14

factor : ( exp ) - (8)

. reduce 8

8/127 terminals, 4/600 nonterminals

9/300 grammar rules, 15/1000 states

O shift/reduce, O reduce/reduce conflicts tegorted 9/601 working seta used

memory: states, etc. 36/2000, parser 11/4000

9/601 distinct lookahead sets

6 extra closures

18 shift entries, 1 exceptions

8 goto entries

4 entries saved by goto default

Optimizer space used: input 50/2000, output 218/4000

218 table entries, 202 zero

maximum spread: 257, maximum offset: 43

Page 239: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

*.. .. . , ., , ". ,.:.. 5.. Yacc: un generador de analizadores sintatlitos IALR(I) 235 ,.,. c.. . .

Concluiremos este ejemplo riiediante la construcción de tina tabla de anilisis siniicticci clel iirchivo de saliti;~ de Y;icc, euiictainerite como lo huhiéra~rios escrito a ni:irio al principio de cstt: c;ipíttilo. La tabla tle ;inilisis sintictico aparece en I;i tabla 5 . 1 1.

torrpspondiente a la salida yace de la figura 5.12

Tabld 5 11

5.5.3 Conflictos de análisis sintáctico y reglas de eliminación de ambigüedad

t ino (le los usos importantes de la opción verbose es i~ivestigar 10s conflictos de análisis sintáctico, de los que Yacc informará en el archivo y. output. Yacc tiene reglas de cl i - rninación (le ambigüedad integradas en él mismo que le perrnitirin producir tin analizaclor sintáctico incluso en la presencia de conflictos de análisis (por consiguiente, hasta para gra- niáticas ambiguas). Con frecuencia estas reglas de eliminación de ambigüedad hacen bien su ti.iib;tjo. pero en ocasiones no es así. A l examinar el archivo y .output se permite al ust~;irio determinar cu:iles son los conflictos de análisis sintkt ico, y si e l ;tndizador sintác- tico pro<lucido por Yacc los resolverá de manera correcta.

F.ti el cjeniplo de la figura 5.10 no había conflictos de análisis s i ~ i t á c k n y Yacc inlor- imí de este hecho en la infbrrnaci6n resnrnicln al final del archivo de salida como

Ir a Tabla de anál~sa r~ndct~to Estado

O ehift/reduce, O reduce/reduce conflicts reported

Entrada

Page 240: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

UP. S I ANÁLISIS SINTACTICO ASCENDENTE

exactamente en los mismos términos, y resuelve la anibigüedíd mediante la misma re de eliminaciiin de ambigüedad. En realidad, la tabla de análisis sintáctico mostrada Yacc es idéntica a la tabla 5.9, excepto por las reducciones precieterminadas que Yacc secta en las entradas de error. Por ejemplo, Yacc infornia de las acciones del estado 5 (1 la manera siguiente en el archivo y.output (los tokens se definieron en letras ~nay Iüs e11 el archivo [le especificaciiin para evitar conflictos con las palabras reservadas lenguaje C):

5: shiftlreduce conflict (shift 6, red'n 3) on ELSE

state 5

1 : IF S- (3)

1 : IP S-ELSE S

ELSE shift 6

. reduce 3

En la información resumida Yacc también informa del conflicto simple de reducción po desplazamiento:

1 shift/reduce, O reduce/reduce conflicts regorted

En el caso de un conflicto de reducción-reducción, Yacc elimina la ambigüedad al pre- ferir la reducción por la regla gramatical que se mostró primero en el archivo de especifica- ción. Esto es más probable que sea,un error en la gramática, aunque también p i d e resultar en un anali~ador sintáctico correcto. Ofrecemos el siguiente ejemplo simple.

fjemplo 5.18 Consideremos la gramitica siguiente:

S 4 A B A -t a B + a

Ésta es una gramática'arnbigua, ya que la cadena legal simple tr tiene las dos derivaciones S * A * a y S * B * a. El archivo y. output citmpleto para esta gramática se proporcio- na en la figura 5. 13. Observe el conflicto de reducción-reducción en el estado 4. el cual es resuelto al preferir la regla A -+ n en lugar de la regla 5 --+ cr. Esto da como resultado que nunca se utilice esta tíltima regla en una reducciiin (lo que indica claraniente un problema con la gramática) y Yace informa de este hecho al final con 1 a 1' inea

Rule not reduced: B : a

¡gura 5.13 state O

rchivo de ialida de Yacc Saccegt : -S Send

ara la gramática del emplo 5.18 a shift 4

. error

S goto 1

A goto 2

J goto 3

Page 241: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

. .

Figura 5.13 ronlinudtinn ~ ~ ~ h i v o de salida de Yacc

': i P la gramática del

tjempIo 5.1 8 i

Yacc un generador de analiradores sintácticos LALRI 1)

state 1

$accept : S-Send

Send accept

. error

state 2

S : A-

. reduce

state 3

S : 8-

. reduce

4: reduce/reduce

state 4

A : a-

B : a-

reduce

(2)

2

conflict (red'ns 3 and 4 on Send

Rule not reduced: B : a

3/127 terminals, 3/600 nonterminals

5/300 grammar rules, 5/1000 states

O shift/reduce, 1 reduce/reduce conflicts reported

Y x c tie~ic, ;itle~iiis de la rcgI;i de c l i m i t ~ a c i ~ í ~ i de ;r~rihigiiedarI ya mcncioriad;~, varios iiteca~iisnios ex prokso para la cspeeificaciciti de la tisociatitidad \. prccedcncia de opera- dores separ;idarnerite cie LII~:I grarrititica q ~ i e (le otro triotlo es mrh ig~ ia . Esto tiene diversas vcnt+jas. En ~?riiricr lugar, la gramitica no necesita cortteiier coristruccioncc explícitas que especifiq~icn risociatividad y precedencia, y esto significa que la graniitictr ~.)uctle ser ~ i i l i s coiicisit y ir l is siiiiple. En segiiitdo l u p r , In tiibla de nixÍlisis sintáctico asociticla taiithiét~ prie- de ser rnis peq~ieña y e l analizador s in t ic t iw rcsulratite. 1r15s eficiente.

Considere. por cjerirplo. 1;1 espccifictrcih Yacc de la f i g r w 5 . 13. En esa t'igrira l a p. titiítica esrli csctita en k~r r t ia mhigu; i sin prccetlericia o asociitt ivid~tl de los opeririiores. ~:;II

c;inihio. I;i prccedcncia y :iwci;i~ivitl:icI dc l i)s oper;rtlotw ~c especificri en 1;1 wccii in clc i lcf i- iiicioncs :iI cscrihir las lincas

Page 242: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

nivel)

F ~ o u r a 5 14 %(:

Espetificacion de Yacc para #include <atdio. h>

una taltuladora smpk #include <ctype.h>

ton gradttca amb~gua %)

y reglar de precedencia y asociatividad para operadores %token

command : exp

i

exp : NüMBER

CAP. S ANÁLISIS IINTACTICO ASCENDENTE

precedencia inás alta que + y - (ya que se nuestra después de estos operadores en &raciones). Las otras posibles especificaciones de operador de Yacc son %rig %nonassoc ("nonassoc" significa que no se permiten operadores repetidos en el in

I exg ' + ' exp ( S $ = $1 + $3;) I exp ' - ' exp ($9 = $1 - $3;) I exp ' * ' exp f $ $ = $1 * $3;)

i

%%

/ * auxiliary procedure declarations as in Figure 5.10 * /

5.5.4 Rastreo de la ejecución de un analizador sintktico Yacc Además de la opción verbose que exhibe la tabla de análisis sintáctico en e1 archivo y.outgut, también es posible obtener un analizador sintáctico generado por Yacc pa- ra imprimir un rastreo de su ejecución, incluyendo una descripcicín de la pila de anilisis sintáctico y acciones del analizador semejantes a las descripciones que ya dimos antes en este capítulo. Esto se hace compilando y. tab-c con el sírnbolo YYDEBUG definido (utilizando la opción de compilador -DYYDEBUG, por ejemplo) y estableciendo la varia- ble entera yydebug de Yacc a I en e1 punto donde se desea la irtformiición de rastreo. Por ejemplo, al agregarse las líneas siguientes al principio del procedirniento main de la figtira S. 10,

extern int yydebug;

yydebug = 1;

Page 243: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Yacc u n generador de analizadores rintáctitos LALR(I) 239

se provocará que el analizador sintáctico prodtizca una salida parecida a la de la Figura 5.15, y dará la expresión 2+3 como entrada. Invitanios al lector a que construya un rastreo a mano de las accio~ies del analizador sintáctico utilizando la tabla de tiriálisis sintríctico de In tabla S. l I y lo comp:rre con esta salida.

la entrada 2+3

Next token is NüMBER

Shifting token m B E R , Entering state 5

Reducing via rule 7, NüMBER - > factor

state stack now O

Entering state 4

Reducing via rule 6, factor - > term

state stack now O

Entering state 3

Next token is ' + ' Reducing via rule 4, term -> exp

state stack nav O

Entering state 2

Next token is ' + ' Shifting token ' + ' , Entering state 7

Next token is NüMBER

Shifting tcken NUMBER, Entering state 5

Reducing via rule 7, NüMBER -> factor

state stack now O 2 7

Entering state 4

Reducing via rule 6, factor - > term

state stack now O 2 7

Entering state 11

NOW at end of input.

Reducing via rule 2, exp ' + ' term -> exp

state stack now O

Entering state 2

Now at end of input.

Reducing via rule 1, exg - > command

5

state stack now O

Entering state 1

Now at end of input.

5.5.5 Tipos de valor arbitrario en Yacc En la figura S. 10 especificrimos las acciows de uiia cdculirdora cmplemtlo las [)scrido\:aria- hles de Y x c awciadas con c ; ~ h síiriholo gi-aintitical en una regla gratnniic;il. Por. . i ~ ~ n p l o , i.scrihiinos $ $ = $1 + $3 para cilahlecer i.1 mlor de iiii;t expresitíii L Y ~ O la w n a tle los

Page 244: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. S I ANALISIS SINT~CTICO ASCENDENTE

valores de sus dos subexpresiones (en las posiciones 1 y 3 en el lado derecho de la re gramatical e.rp -+ exp + renn). Esto está hien mientras que los valores con los que es tratando sean enteros, puesto que el tipo predeterminado de Yacc de estos valores es pre entero. Sin embargo, es inadecuado si, por ejemplo, queremos una calculadora p hxer c&lculos con valores de punto flotante. En este caso debemos incluir una redefinic del tipo de valor de las pseudovariahles de Yacc en el archivo de especificación. Este t (le datos siempre se define en Yacc mediante el síniholo de preprocesador en C YYST La redefinición de este símbolo modificará apropiadamente el tipo de la pila de v de Yacc. De este modo. si queremos una calculadora que haga cálculos de valores en to flotante, debemos agregar la línea

#define YYSTYPE double

dentro de 10s deli~nitadores %{ . . .%} en la sección de definiciones del archivo de esp ficación de Yacc.

En situaciones más complicadas podemos necesitar valores diferentes para reglas rnaticales diferentes. Por ejemplo, supongamos que queremos separar el reconocimie de una selección de operadores de ia regla que calcula con estos operadores, como en reglas

exp -+ exp opsurna term / term op,mnm -+ + / -

(Éstas eran de hecho las reglas originales de la gramática de expresión, las cuales cam mos para reconocer los operadores directamente en las reglas para exp de la figura 5.1 Ahora op,p,Furna debe devolver el operador (un carácter), mientras que e.xp debe devolver valor calculado (digamos, un double), y estos dos tipos de datos son diferentes. Lo qu necesitarnos hacer es definir YYSTYPE corno una unión de double y char. Podem hacer esto de dos maneras. Una es declarar la unión directamente en la especificación Ya utilizando la declaracih Yacc %union:

%union { double val;

char op; >

,Ahora Yacc necesita estar informado del tipo de retorno de cada no terminal, y esto se co sigue mediante el uso de la directiva % t m e en la sección de definiciones:

%tyge <val> exg t e m factor

%type <og> addog mulog

Advierta cómo los nombres de los campos de unión estan encerrados entre picopnrénte s is en la declaración % t m e de Yacc. Entonces, la especificación Yacc de la figura 5. I:.. se modificaría para comenzar de la siguiente manera (y dejaremos los detalles para I m i ejercicios):

%union ( double val;

char og; 1

Page 245: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Y3cc u n generador de analizadorer i;ntatticos LALR(I)

%type <op> addog mulog

%%

command : exp ( grintf("%d\n",$l);)

exp : exp op term ( switch ($2) (

case ' + . , $$ = $1 + $3; break;

. $$ = $1 - $3; break; case ' - ' '

> 1

I term ( $ S = $1;)

op : ' + ' ( $$ = , + ' ; I 1 ' - ' ( $$ = S - * . ' 1

;

La segunda alternativa es definir un nuevo tipo de datos en rtri ;irchivo iriclrtido por se- p:trado (por ejemplo, rtn archivo de cabecera) y posreriormerite tlefinir YYSTYPE coriio tie este tipo. Entotices los valores apropiados se deben coristruir a mano en el código de ;tcciítii aswiado. IJri ejeniplo de esto es el analizador sititictico TINY tle la secci6n siguiente.

5.5.6 Acciones incrustadas en Yacc

Ert ocasiones es iiecesario ejecutar algún cótligo nritcs de coinplet;tr el rccortocirniento de una regla gratrtatical durante el aixílisis sintictic«. Considere corno ejeriiplo el caio (le las cteclnraciones simples:

Nos gnstaría, citando se r~conocier~i nrin vur-iist, ctiquctar cada identificatlor de variable con el tipo act~ial (entero o de punto flotante). En Yacc podertios hacer esto del tlioclo que se muestra a continuación:

decl : type ( current-type = $1; )

var-list

tVpe : INT f $ $ = INT-TYPE; I

I FLOAT ( $$ = FLOAT-TYPE; }

var-list : var-list ' , ' ID

i. setTypeitokenString,current-type);}

I ID

i setType(tokenString,current-lype);}

Page 246: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. S 1 ANALISIS IINTACTICO ASCENDENTE

Observe cómo se establece current-type (el cual para ser declarado necesita una ble estática en la sección de declaraciones) mediante una accirín de incrustación ante reconocin~ierito de las variables en la regla decl. En las secciones siguientes se verá ejemplos de acciones incrustadas.

Yacc interpreta una acción incrustada

A : B ( / * acción incrustada * / ) C ;

como equivaletite a la creación de un nuevo no terrni~ial reteriedor y una prodiiccicín e ese no terminal que, cuando se reduce, realiza la acción incrustada:

A : B E C ;

E : ( / * acción incrustada * / } ;

Para cerrar esta sección resumimos en la tabla 5.12 los nombres internos y mec de definición de Yacc que hemos a~ializado.

Nombre interno de Yacc definición de Yacc

y.tab.h Archivo de cabecera generado por Ylicc que contiene definiciones de token

yyparse Rutina de análisis sintktico de Yacc yylval Valor del token actu~l en la pila werror Jnipresión de mensaje de error definido por el usuario

ritilizado mediante Yacc error Pseudotoken de error de Yace yyerrok Procedimiento que restablece el analizador sintáctico

después de un error yychar Contiene e1 token de hlísqueda hacia delante que causó

un error YYSTYPE Símbolo de preprocesador que define el tipo de valor

de la pila de anúlisis sintáctico yydebug

generación de información eri tiempo de ejecución ace

%token Define símbolos de prcproccs:idor de token %start Define el símbolo iio terminal inicial %union Define un YYSTYPE de uni<in, pemitiendo valores

de tipos diferentes t n la pila del mnliraclor sintáctico %type I>cfine cl tipo de rini~in vari:inte ilevtielto por u n siiriholo % l e f t %right %nonassoc Define 1;s :isociütivid;id y preccOencia (por posicih)

de opmdores

Page 247: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generacion de un analizador sintáctico TINY utilizando Yacc

GENERACION DE UN ANALIZADOR SINTACTICO TlNY UTILIZANDO Yacc En la secc ih 3.7 se expuso la sintaxis de TINY. y en la seccicín 1.4 se dcscribiií un analiza- dor siritáctico escrito a iiiatio, remitimos al lector a esas descripciones. Acliií descrihirenios c l :irchivo de especificaciiín Yacc tiny. y, junto con los cambios necewrios a las defini- ciones globales globals . h (tomiimos pasos para minimil-ar cambios a otros archivos, Lo que t m b i é n se comentará). El archivo cotitpleto tiny. y está listado en el ;ipCndice H. líneas 4000-4 162.

C»tnenz;rmos con un análisis de la secciiín de definiciones acerca de la ecpecificació~i y. GILL .. de T I N Y . E l uso de l a marca YYPARSER (línea 4007) se ctescrihirá posteriorineiite.

Tenertios cuatro archivos #include representando la inforrnaciiíti que ~iccesita Yacc de otra parte en el programa (líneas 1009-4012). ¡.a sección de tlefirticiortes tiene otras crtatro declaracioties. La primera (línea 4014) es una d e f i n i c i h de YYSTYPE que define los va- Lores devueltos por los procedimietitos de análisis sintáctico de Yacc corno apuntadores a estructuras de nodos (TreeNode mismo está definido en globals . h). Esto permite al analizador sintáctico de Y;icc conslr i~ir rin árbol si i i tkt ico. L a segunda declaracicín es de una variable estática global savedName que se ernplea para almacenar temporrrlmc~ite cndetias de identificador que iiecesitan ser insertadas e11 nodos de árbol que todavía no se han con+ truido cuando las cadenas se ven en la entrada (en T I N Y esto es ttecesario s6lo en asigna- ciones). L a variable savedLineNo se utiliza para e l rnismo prcipiisito. de riiaiiera que los números de línea apropiados del código fuente estarán aoci;idos con los identificadores. Finalmente, savedTree se utiliza para almacenar de manera teinporiil e l árbol si~itáctico proclucido por el procedimiento yyparse (yyparse niisnio s61« puede devolver uiia rnarca de valor entero).

Procedereinos a un análisis de las acciones asociadas con cada una <le las reglas gra- maticales de T l N Y (estas reglas son ligeras variaciones de la gramática RNF expuesta en e1 capítulo 3, figura 3.6). En la mayoría de los casos estas acciones representan laconstrucción del árbol sintáctico correspondiente al árbol de unálisis gramatical en ese punto. Específica- tnerite, se necesitan nuevos nodos para ser asigi~ndos mediante llarriadas a newStmtNode y newExpNode desde el paquete util (éstos se describieron en la página IX2), y se tie- cesitan riodos hi jo apropiados del nuevo nodo de árbol para ser asignados. Por +rnplo. los acciones correspondientes a write-stmt de T l N Y (líneas 4082 y siguientes) son con)« se ve a continuacibii:

write-stmt : WRITE exg

{ $$ = newStmt~ode(WriteK) ;

S$->child[Ol 1 $2;

1

i

L a primera instrucci6n Ilaina a newStmtNode y asigna su valor devuelto corno el valor (le write-stmt. Entonces asigna el valor previamente construido de exp (la pseudovoritihle $2 de Yacc, que es un aptintador al nodo de árbol de la expresibn por imprimirse) para que sea el primer hi,jo tiel nodo de árbol de la sentencia de escritura. E l código de accitin para otras scritencias y expresicviei es muy similar.

I.AS acciones para program, stmt-seq y assign-stmt Lr;it;rri coti pcqueiíos prublerrias asociados con ca<lir iina de cstas ccrnstrucci<tries. En el caso de l a reglti 21-imati- cal [):ir:' program III occi6ri asociadti (Iíriea 4020) es

Page 248: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Esto asigna el ái-bol construido para la stmt-seq a la variable estática savedT to es necesario para que el árbol sintáctico pueda ser devuelto posteriormente t11e procedimiento parse.

En el caso de aseign-stmt, ya rnenciouarnos que necesitamos almacenar la c del identificador de la variable que es el objetivo de la asignación de manera que est ponible cuando se construya el nodo (así como su número [le Iínea para una exploración po terior). Esto se consigue utilizando las variablcs estáticas saveüName y saveLineNo neas 4067 y siguientes):

assign-stmt : ID ( savedName = copyString(tokenString): savedLineNo = lineno; )

ASSIGN exp

I $$ = newstmt~ode (AssisnK) ; S$->child[Ol = $ 4 ; $$->attr.name = savedName;

La cadena de identiticador y el número de Iínea se deben guardar como una acción in tada antes del reconocimiento del token ASSIGN, puesto que, a medida que se igualen vos tokens, los valores de tokenstring y lineno son modificados por el analizador xico. Incluso el nuevo nodo para la asignación no puede ser completaniente construido que la exp sea reconocida. De aquí surge la necesidad de saveüName y saveLin (El uso del procedimiento utilitario cogystring asegura que no se comparta la me entre estas cadenas. Advierta que también el valor exp es referido como $4. Esto se a que Yacc cuenta las acciones incrustadas como ubicaciones adicionales en los lados chos de las reglas gramaticales: véase el análisis de la sección anterior.)

En el caso de stmt-seq (líneas 403 1-4039). el problema es que las sentencias se en un árbol sintáctico de TlNY utilizando apuntadores hennanos en lugar (le apuutador jos. Como la regla para secuencias de sentencias se escribe de manera recursiva por quierda, esto requiere que el código rastree la lista de hermanos ya construida en busca la sublista izquierda a fin de unir la sentencia actual al final, Esto no es eficiente, y puede tarse volviendo a escribir la regla de manera recursiva por la derecha, pero esta solución ti su propio problema, que consiste en que la pila de análisis sintáctico se acrecentará a medi que se procesen secuencias de sentencias más largas.

Finalmente, la sección de procedimientos auxiliares de la especificación Yacc (1 414-4162) contiene la definición de los tres procedimientos, yyerror, yylex y pa El pmcediiniento parse, el cual se invoca desde el programa principal, llama al p to de análisis sintáctico definido en Yacc yyparse y luego devuelve el árbol sint6ctieo do. El procedimiento yylex es necesario debido a que Yacc supone que éste es et no procediniiento del andizador léxico, y éste Fue definido de mmem externa como Escribimos esta definición demanera que el analizador sintáctico generado por Y cionara con el compilador TINY con un mínimo de cambios para otros archivos del c puede desear hacer los cambios apropiados al analizador Iáxico y eliminar esta de ticularmente si se está utilizando la versidn de Lex del analizador léxico. El p yyerror es llamado por Yacc cuando se presenta un error; iniprime cierta infoimació tal como el número de Iínea, al archivo de listado. Utiliza una viiriable yychar intem que contiene e1 número de token correspondieute al token que ocasionó el error.

Resta describir los crinibios a los otros archivos en el analizador sintáctico de TINY que se hacen necesarios por el uso de Yacc para producir el :inalizador sintáctico. Como obser- vamos. pretendimos mantener estos cainbios en un mínimo, y confinamos trxdos los minbios :11 archivo globals . h. El ;irchivo revisado he muestra en el :rp6ndice B. líueas 1200--1.320.

Page 249: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

.. :, .. ~

.; , ., ..' Recuperacion de errores en analizadores sintácticos ascendentes >.;

245 . . ... . .

El prohlerna básico es que el archivo de cabecera generado por Yacc, que contiene las dc- finiciones de token, debe ser incluido en la mayoría de los otros archivos de código, pero no puede ser incluido directamente en el analizador sinráctico generado por Yacc, puesto que repetiría las definiciones internas. La solución es comenzar la sección de declaraciones de Yacc con la definición de tina marc;; YYPARSER (línea 4007), la cual se incluirá en el ana- lizador sinráctico de Yacc e indicará cuando el compilador de C a t é dentro ctcl analizador sintáctico. Utilizantos esa marca (líneas 4226-4236) para incluir de manera selectiva el ar- chivo de cabecera generado por Yacc y. tab. h en globals . h.

IJn segundo problema se presenta con el tokcn ENDFILE. que genera el analizador 16- xico para indicar el fin del arclrivo de entrada. Yacc supone que este token siempre tiene el valor O. Suministramos una definición directa de este token (líneas 4234) y la incluimos en la sección selectivamente compilada controlada por YYPARSER, puesto que Yacc'tio la ne- cesita internamente.

El cambio final al archivo globals . h es volver a definir TokenType corno un sinó- nimo para int (línea 4252), ya que todos los tokens de Yacc tienen valores enteros. Esto evita reemplazos iiinecesarios del tipo enumerado TokenType anterior en otros archivos.

5.7 RECUPERACIÓN DE ERRORES EN ANALIZADORES SINTÁCTICOS ASCENDENTES 5.7.1 Detección de errores en el análisis sintáctico ascendente Un analizador sintáctico ascendente encontrará un error cuando se detecte una entrada en blanco (o de e m r ) en la tabla de análisis sintáctico. Obviamente, tiene sentido que loserro- res deberían detectarse tan pronto corno fuera posible, ya que los mensajes de enor pueden ser más específicos y significativc.is, De este modo, una tabla de análisis sintáctico deberia tener tantas entradasen blanco conlo fuera posible.

Por desgracia esta meta está en conflicto con otra de la misma importancia: la reducción del tamaño de la tablü de análisis sintáctico. Ya vin~os (tabla 5.11) que Yacc rellena tantas entradas de La tabla como puede con reducciones predeterminadas, de manera que se pueden producir un gran número de reducciones en la pila de anáiisis sintáctico antes que se declare un error. Esto oculta la fuente precisa del error y puede conducir a mensajes de error poco informativos.

Una característica adicional del análisis sintáctico ascendente es que la potencia del al- goritmo particular utilizado puede afectar la capacidad del analizador sintáctico para detec- tar los errores de inmediato. Por ejemplo, un analizador sintáctico LR(1) puede detectar errores en una etapa más temprana que un analizador LALR(1) o SL,R(I), y estos últinios pueden detectar errores más pronto que un analizador LR(0). Como un ejemplo simple de esto compare la tabla de análisis sintáctico LR(O) de la tabla 5.4 (página 209) con la tabla de análisis sintáctico LR(I) de la tabla 5.10 (página 222) para la misma gramática. Dada la cadena de entrada incorrecta (a $ , la tabla de análisis sintáctico LR(1) de la tabla 5. 10 des- plazará ( y a hacia la pila y se moverá al estado 6. En el estado 6 no hay entrada bajo $, de modo que se informará de un error. En contraste, el algoritmo LR(0) (así como el atgoritmo SLR(1)) reducirán mediante A -+ a antes de descubrir la ausencia de un paréntesis derecho para completar la expresión. De manera similar, dada la cadena incorrecta a) (5, el ;maliza- dor sintáctico LR(1) general desplazará a y despu6s declarará un error del estado 3 en el pa- réntesis derecho, mientras que el analizador LR(0) reducirá otra vez medianteA -+ a antes de declarar un error. Naturalmente, cualquier analizador sintáctico ascendente siempre terminará por informar del error, quizás después de un número de reducciones "erróneas". Ninguno de estos analizadores desplazará jamás u n token en estado de error.

5.7.2 Recuperación de errores en modo de alarma Como cn 21 :tniílisis sintijctico clcsceiidente, \e puede conseguir una iwxper~ciiiii de errores ixonablcmente buena en :nialimdorcs asccritlenies i.ii~ninando sínibolos de in;iricr.rr ptuilcrirc,

Page 250: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. s 1 ANALISIS SINTACTICO ASCENDENTE

ya sea de la pila de, análisis sintáctico o de l;i entrada, o bien de ambas. Como en los a dores sintácticos LL(I), existen tres posibles acciones alternativas que se pueden contem

1. Extraer un estado de Ia pila. 2. Extraer sucesivamente tokens desde la entrada hasta que se vea uuo de ellos p

cual se pueda reiniciar el análisis sintáctico. 3. Insertar un nuevo estado en la pila.

Un niétodo particulrtrmente efectivo para elegir cuál de estas acciones realizar cuando presenta un error es el siguiente:

1. Extraer estados de la pila de análisis sintáctico hasta encontrar un estado con ent das Ir a no vacías.

2. Si existe una accitín legal sobre el token de entrada actual desde uno de los est Ir a, insertar ese estado en la pila y reiniciar el análisis sintáctico. Si existen v de tales estados, preferir un desplazamiento a una reducción. Entre las accione reducción preferir una cuyo no terminal asociado sea menos general.

3. Si no existe acción legal en el token de entrada actual desde uno de los estados avanzar la entrada hasta que haya una acción legal o se alcance el final de la entr

Estas reglas tienen el efecto de obligar al reconocimiento de una construcción que viera en el proceso de ser reconocida cuando ocurre el error, y reiniciar el análisis sintáct' inmediatamente a partir de entonces. La rec~tperación de errores que utiliza éstas o unas glas semejantes se podría denominar recuperación de errores en modo de alarma, pue que es similar al modo de alarma descendente descrito en la sección 4.5.

Desgraciadamente, como el paso 2 inserta nuevos estados en la pila, estas reglas pue dar como resultado un ciclo infinito. Si éste es el caso, existen varias posibles solucio Una es insistir en una acción de desplazamiento desde un estado Ir a en el paso 2. Sin bargo, esto puede ser demasiado restrictivo. Otra solución es, si el siguiente movimient gdl es una reducción, establecer una marca que provoque que el analizador sintáctico sig pista de la secuencia de estados durante las siguientes reducciones, y si el mismo estado repite, extraiga estados de la pila basta que se elimine el estado original en el que ociirri error, y comience de nuevo con el paso l . Si en cualquier momento se presenta una ac de desplazamiento, el analizador vuelve a establecer la marca y continúa con un análisi táctico normal.

Ejemplo 5.19 considere la gramática de expresión aritmética simple cuya tabla de análisis sintáct' Yacc se da en la tabla 5.1 1 (página 235). Considere ahora la entrada errónea ( 2 + análisis sintáctico continúa normalmente hasta que se detecta el asterisco *. En ese pun modo de alarma podría ocasiotiar que se produjeran las siguientes acciones en la pil análisis sintáctico:

Pila de análisis s~ntactico Entrada Acción

. . . . . . . . . $ 0 ( 6 E 10+7 * ) $ error:

inscrtar T, ir a I 1 S O ( 6 E 10+7 7 ' 1 1 *)S desplazar 9 $0(6E10+77'11 ' 9 ) $ error:

insertar F, ir a 13 '30(61:'10+7T1l * 9 F l i ) $ reducir T -* T * F

Page 251: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]
Page 252: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

248

Ejemplo 5.20

CAP. S 1 ANALISIS SINTAC~ICO ASCENDENTE

Considere e1 siguiente reemplazo a la regla para commmd de la figura S. 10:

c o m n d : exp { grintf (%d\nw. $1) ; 1 1 error { yyerror ("expresión incorrecta") ; f

;

Considere también la entrada errónea 2 + + 3 . El análisis de esta cadena continúa norma mente hasta que se alcanza el segundo + erróneo. En este punto el análisis da la sigui te pila de análisis sintáctico y entrada (utilizamos la tabla 5.1 1 para describir esto, aun la adición de la producción de error resultará en realidad en una tabla de análisis ligera te diferente):

PILA DE ANÁLISIS SINTÁCTICO ENTRADA

Ahora el analizador sintáctico introduce el "estado" de error (generando un mensaje de er tal como "syntax error" o "error de sintaxis") y comienza a extraer estados desde la pila, ha ta que se descubre el estado 0. En este punto la producción de errores para commund esti la que error es una búsqueda hacia delante legal, y se desplazará a la pila de anál sintáctico, e inmediataniente reducirá a command, causando que se ejecute la acción asoci (lo que imprime el mensaje "incorrect expression" o "expresión incorrecta"). La situaci resultante es ahora como se muestra a continuación:

PILA DE ANÁLISIS SINTÁCTICO ENTRADA

$0 cornrnund 1 +3$

En este punto la única búsqueda hacia delante legal está al final de la entrada (indicado aq por $, correspondiente a la devolución de O por yylex), y el analizador sintáctico climin rá los tokens de entrada restantes +3 antes de salir (aunque todavía en el "estado de error" De este modo, la adición de la producción de error tiene esencialmente el mismo efecto la versión de la figura 5.10, sólo que ahora podemos proporcionar nuestro propio men de error.

Un mecanismo de error aún mejor que éste permitiría al usuario reintroducir la Iínea de pués de la entruda errónea. En este caso se necesita un token de sincronización, y el fin d marcador de Iínea es el único sensible. Así, el analizador léxico se debe modificar para devo ver el carácter de retorno de línea (en vez de O), y con esta moditic~ción podemos escribi (pero véase el ejercicio 5.32):

comand : exp *\nl { printf("%d\nlv,$l); exit(0);)

1 error '\n' ( yyerrok;

printf("reintroducir expresión: "); }

c o m n d i

Este código tiene el efecto de que, cuando ocurre un error, el analizador sintáctico saltará todos los tokens hasta uno de retorno de Iínea, cuando ejecutará la acción representada por yyerrok y la sentencia printf. Entonces intentará reconocer otro corrrtntrnd Aquí es necesaria la llamada a yyerrok para cancelar el "estado de mor" después de que se tie- tccta el retorno (le Iínea, ya que de otro modo, se presenta un nuevo error justo después

Page 253: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Recuperación de errores en analiradores rinta<tiros ascendentes 249

del retorno de línea, Yacc eliminará silenciosamente los tokens hasta que encuentre una se- cuencia de tres tokens correctos. §

Imaginemos lo que pasaría si se agregara una producción de error a la definicih Yatc de la Figura 5.10 de la iiianera siguiente:

factor : N[MBER ( $$ = $1;)

I ' í ' e x p ' ) ' ( $ S = $2;) I error ( S $ = 0;)

Considerenios en primer lugar la entrada errónea 2++3 conio en e1 ejemplo anterior. (Con- tinuamos usando la tabla 5. 1 1 , ;)unque la producción de error adicional da como resultado una tabla ligeranlente diferente.) Corno antes, el analizador sintáctico alcanzará el punto siguiente:

PILA DE ANÁLISIS SINTÁCTICO ENTRADA

$0 erp 2 + 7 +3S

Ahora la producción de error para,fizctor estipillará que e r ro r es una húsqueda hacia ~Ielante legal en el estado 7, y e r ror será inmediatamente desplazado a la pila y retli~cido afiíiicror, provocando que el valor O sea devuelto. Ahora el analizador ha Ilegatlo al punto siguiente:

PILA DE ANÁLISIS SINTÁCTICO ENTRADA

Ésta es una situación normal, y el analizador continuará su ejecución normalmente hasta el final. El efecto es interpretar la entrada como 2+0+3 (el O entre los dos sínibolos de + está allí porque allí lire donde se insertó el pseudotoken error , y por la acción para la produc- ción (le error, e r ror se visualiza como equivalente a un factor con valor O).

Ahora consideremos la entrada errónea 2 3 (es,decir. dos númerus con un operador per- dido). Aquí el analizador sintáctico alcanra la posición

En este punto (si la regla para comrnund no se ha cambiado), el analizador reducirá (erró- neamente) mediante la regla ccirnnrand -t exp (e imprimirá el valor 2), aun cuaricio un número no es un símbolo siguiente legal para (mmrund. E1 analizador alcanza entonces la posición

Ahora se detecta u n error. y e1 e\tatlo I se cxirae tle la pila de análisis siniiíciico, descuhrien- tlo el estado 0. En este punto la producción de error pamfirctor permite que e r ror sea una hílsqneda hacia tlelante legal desde el estado 0. y e r ror se dcsplam a la pila tle ;tiilílisis ~ i~ i t i t t i co produciendo otra cascado de reducciones y la iriipresi~iri cic1 valor O (el v;ilor de- \rielro por la procliiccih de en-or). i Allora el ;rnnliiador está de regrcso i.11 1;) ~n isn~n posi- ciiin en el :inilisis sintictic». con el niiiricro 3 todnví;~ cri la cnlrada! .\f1)rtci1iatlarl1e1itc, cl

Page 254: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. S I ANALISIS SINT~CTICO ASCENDENTE

analizador sintáctico esti ya en el "estado de error" y no desplazará de nueva cuen error, pero desechará el número 3, descubriendo la búsqueda hacia delante correcta el estado 1 , con lo cual el analizador sintáctico sale de escena."' El resultado es que el lizüdor imprime lo siguiente (repetimos la entr;ida del usuario en la primera línea):

> 2 3

2

error de sintaxie

expresión incorrecta

o

Este comportamiento nos da una visión fugar de las tlificult&s de una buena recuperaci de errores en Yacc. (Véanse los ejercicios para más ejemplos.)

5.7.4 Recuperación de errores en TINY El archivo de especificación de Yacc t i n y . y en el apéndice B contiene dos producci de error, una para stmt (línea 4047) y otra para factor (línea 4139), cuyas acciones ciadas son para devolver el árbol sintáctico nulo. Estas producciones de error proporc un nivel de manejo de errores semejante al del analizador sintáctico descendente recur de TINY expuesto en el capítulo anterior, s6lo que no se hace ningún intento de constr un árbol sintáctico significativo en la presencia de errores. Estas producciones de error ta poco proporcionan sincronización especial para el reinicio del análisis sintictico, de mo que en la presencia de errores múltiples se pueden omitir muchos tokens.

EJERCICIOS 5.1 Considere la gramática siguiente:

E-. ( L ) ! a L + L , E I E

a. Construya el DFA de elcnientos LR(0) para esta gramática. b. Cotistruya la tabla de anrílisis sintáctico SLR(1). c. Muestre la pila de análisis siiit,?ctico y las acciones de un aiializatlor SLR( 1 ) para la cadena

deentrada ((a),a,(a,a)). d. ¿Es esta gramitica una gramática LR(O)? Si no es así, describa el conflicto LR(0). Si lo es,

construya la tabla de análisis sintáctico LR(O) y describa cómo puede diferir un análisis sin- táctico de un análisis SLR(1).

5.2 Considere la gramática del ejercicio anterior. a. Construya el DFA de elementos LR(1) para esta gramática.. b. Construya lü tabla de análisis sintáctico LK(I) general. c. Construya el UFA de elementos LALR(I) pira esta graniática. d. Construya la tabla de aniílisis sintActico IL.ALK(I).

Page 255: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

e. De~criba cualquier diferencia que se pueda presentar entre 1 s acciones de un analizador sin- táctico LR(1) general y un analiz

5.3 Considere la gramática siguiente:

a. Construya el DFA de elementos LR(0) para cstt gramática. b. Construya la tabla de análisis ~intáctico SLR(1). c. Muestre la pila de análisis stntáctico y las acciones de un analizador SLR(1f para la cadena

ba el conflicto LR(0). Si Lo es, ferir un análisis sin-

e un analizador ~ i n -

a. Construya el DFA de elementos LR(0) para esta gnmáiica. b. Construya la tabla de análisis sintáctico SLR(1). c. Muestre la pila de análisis sintáctico y las acciones de un analizador SLR(1) para la cadena

de entrada s i s : s .

d. LES esta gramática una gramática LR(O)? Si no es así, describa el conflicto LR(0). Si lo es, consiruya la tabla de análisis sintitctico LR(0) y describa cómo puede diferir un análisis m- táctico de un análisis SLR(1).

5.6 Considere la gramática del ejercicio anterior. a. Construya el DFA de elementos LR(I) para esta gramática. b. Construya la tabla de análisis sintactico LR(I) general. c. Construya el DFA de elementos LALR(1) para esta gramática. d. Conshlya la tabla de análisis ~intictico LALR(1). e. Describa cualquier diferencia que se pueda presentar entre las acciones de un analizador sin-

táctico LR(1) general y un analizador sintáctico LALR(1). 5.7 Considere la gramática siguiente:

a. Construya el DFA de elenleotos LR(0) para esta grrtrnitica. b. Construya la tabla de análisis sinticrico SLR(1).

Page 256: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. ! 1 I ANALISIS IINTACTICO ASCENDENTE

c. Muestre la pila de análisis sintáctico y las acciones de un analizador SLR(1) para 13 cad

deentrada ( (alata a) ) .

d. Construya el DFA de elementos LALR( 1) inediante la propagación de búsquedas ha lante a través del DFA de elementos LR(0).

e. Construya la tabla de análisis sintáctico LALR(1 j. Considere la gramática siguiente:

~ieclarución -t tipo var-list tipo -+ int / f loat vur-list -+ identificador, vur-list / identificador

a. Vuelva a escribirla en una forma más adecuada para el análisis sintáctico ascendente. b. Construya el DFA de elementos L.R(O) para la gramática reescrita. c. Construya la tabla de análisis sintáctico SLRf1) para la gramática reescrita. d. Muestre la pila de análisis sintáctico y las acciones de un analizador SLR(1) para la caáe

de entrada int x, y, z utilizando la tabla del inciso c. e. Construya d DFA de elementos LALR(1) propagando las búsquedas hacia delante a tra

del DFA de elementos LR(0) del inciso h. f. Constntya la tabla de análisis sintáctico LALR(1) para la gramática reescrita.

Describa una descripción formal del algoritmo para constniir el DFA de elementos LALR(1 propagando búsquedas hacia delante a través del DFA de elementos LR(0). (Este algoritmo s describió de manera informal en la sección 5.4.3.) Todas las pilas de análisis sintáctico mostradas en este capítulo incluyeron tanto núnieros de estado como símbolos gramaticales (por claridad). Sin embargo, la pila de análisis sintáctic necesita sólo almacenar los números de estado: los tokens y no terminales no necesitan ser a macenados en la pila. Describa el algoritmo de análisis sintáctico SLR(I) si sólo los números de estado se mantienen en la pila. a. Muestre que la gramática siguiente no es LR(I):

A - + U A U ~ E

b. (,Esta gramática ei ambigua? Justifique su respuesta. Muestre que la \tguiente gramática es LR(1) pero no LALR(1):

Muestre que una gramática LK(1) que no es LALR(1) debe tener sólo conflictos de reducción- reducción. Muestre que un prefi~o de una forma sentencia1 derecha es un prefiJo viable si y sólo si no se extiende más allá del controlador. ¿,Puede haber una gramática SLR(1) que no sea LALR(I)? Justifique su respuesta. Muestre que un analizador sintáctico I.R(I) general no hará reducción antes que se declare un error, si el siguiente token de entrada no piiede ser desplazado con el tiempo. ;,Puede un analizador sintictico SLR(li hacer rn6s o menos redacciones que un analizador I.:\L.R( 1 ) antes qiie tieclarar u n error'! Justifique su respucstn.

Page 257: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]
Page 258: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. S I ANALISIS I I N T ~ T I C O ASCENDENTE

5.25 Suponga que la producción de error Yace del ejemplo 5.21, páiina 249, se reeinplara por la regla siguiente:

factor : NWMBER ( $ S = SI ; ] . I ' ( ' exg ' ) ' ( $ S = $2; )

I error {yyerrok; $S = 0;)

i

a. Explique el cornporiümiento del anali~ador Yacc dada la entrüda errónea 2++3.

b. Explique el comportamiento del anali~ador Yacc dada la enuada crrónea 2 3.

5.26 Compare la recuperación de errores del analizador sintáctico TlNY generado por Yacc con del analizador sintáctico descendente rectirsivo del capítulo anterior utilizando los programas de prueba de la sección 3.5.3. Explique las diferencias eii comporiamieiito.

EJERCICIOS DE 5.27 Viselva a escribir la especificaiión Yaec de ia figura 5.10, página 235, para u t i ~ i ~ a r ias sigui

PROGRAMACI~N tes reglas gramaticales (en lugar de scanf) en el cálculo del valor de un número (y, por lo tanto, suprimir el token NiJKñER):

número -t número dígiro / dígito d í g i t o + O / l l 2 i 3 i 4 / 5 / 6 1 7 / 8 / 9

5.28 Agregue lo siguiente a la especificación de la calculadora de enteros Yacc de la figura 5.10 (asegúrese que tengan la asociatividad y precedencia correctas): a. División entera con el símbolo /. b. Módulo entero con el símbolo %.

c. Exponenciación entera con el símbolo ". (Adverlrncia: este operador tiene una precedenci mayor que la inultiplicación y es asociativo por la derecha.)

d. Menos unitario con el símbolo -. (Véase el ejercicio 3.12.) 5.29 Vuelva a hacer la especificación dc la calculadora Yacc de la figura 5.10 de manera que acept

números de punto flotatite (y realice ciilculos en modo de punto flotante). 5.30 Vuelva a escribir la espécificación de la calculadora Yacc de la figura 5.10 de manerq que

disringa entre valores de punto flotante y valores enteros en vez de simplemente calcular lodo como enteros o números de punto flotante. (Sirgerencia: un "valor" es ahora un registro con tina marca que indica si es entero 0 de punto flotante.)

5.31 a. Vuelva a escribir la especificrición de la calculadora Yacc de la figura 5. LO de iiianera que devuelva un árbol sintáctico de acuerdo con las declaraciones de la sección 3.3.2.

b. Escriba una fuiición que tome como parátnetro el &bol sintáctico producido por su código del inciso a y deviielva el valor calculado a1 recorrer el árbol.

5.32 La técnica de recuperación de errores simple sugerida para el programa de la calculadora en la página 248 era errónea porque podía causar desbordamiento de la pila dzspués de muchm errores. Vuelva a escribirla para eliminar este problema.

5.33 Vuelva a escribir la especificación del calculador Yacc de la figura 5.10 para agregar los si- guientes nlensajes de error útiles:

"paréntesis derecho olvidado", generado por la cadena ( 2+3

"paréntesis izquierdo oIvid:id«", generado por la cadena 2 + 3 )

"operador olviddo", generado por la cadeden:~ 2 3

"operando olvidado". grner:rdo por la c;i<leiw ( 2+ )

Page 259: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejercicios de programación

5.34 La granxÍiicii siguiente I-eprcsenta enpresi~mcs aritriiétic;rs siniplcs eri una not;icitin de pre-j,)

.tipo LISI':

Por qjeriiplo. la expresih ( * ( - 2 ) 3 4 ) tiene el valor - 24. Escriba una especificación

Yacc para iiri programa que calcril;ir:í e irriprimirá el valor de las expresiones en esta sintaxis. (Su,qrrt.nciir: esto reqi~criri volvcr a escribir la gramítica y usar un rnec;inisni« para pasar el

operador it una .sci,-irip.)

5.35 1.3 sigtiiente gr;i~nática (mhigiia) representa las expresicines tegdares sirnples ;inaliLadas en el capítulo 2:

a. Escriba una cspec i f i~ ic ih escpcleto cn Yacc (cs decir. sin acciirnes) que exprese la asocia- tividad y precedencia correctas de las operaciones.

b. Extienda sil especific:ición del inciso a para incluir todas las acciones y procedimientos auxiliares neces;iri»s para prmti~cir un "coiiipilndor de cxpresiories regulares", es decir, un

progranla que totnará una expresión regular coirio eritnida y corno salida un programa en C que, cuando se compile, buscará tina cadena de entrada para la primera ocurrencia de una

subcadena que iguale la expresión regular. (Sugerencia: puede utili~arse 1rna tabla, n arre- glo hidiinensional. para rcpresent;ir los estados y transiciones tlcl NFA asociado. El NFA

se puede entonces simular utilizando una cola para al~nacenar estadcis. Sólo se ~iecesita ge- ncnir la tabla mediante las acciones Yacc; el resiti del código siempre ser2 el roisiiro. V6ast: el c:ipítul« 2.)

5.36 Vuelva a escribir la especificación Yacc parir 'TINY (apéndice B. lii~eas 4000-4162) cii tina forma rnBs compacta en cudquiera de las siguientes dos maneras:

a. haciendo tiso de una gr:iniátice nrnhigua para expresiones (ain reglas de eliirii~irición de xnbigüedüdes Yacc para la precctlencia y la asociatividad). y

b. colapsando el reconocimiento de operadores en una sola regla, como en

y iirilizan<lo la declaración Yacc %union (dejando que op deviielva el ~tper;idor. mientras que erp y otros no terminales devuelven :ipuni;idores hacia los nodos de 6rbol). Asegárese de que

su ;iiiali~ador sintictic« produrca 11)s ririsrnos árboles sintácticos que antes. 5.37 Agregue 10s oper:idores de comparación <= (menor 0 igual que), > (mayor que). >= (niayor o

igwl que) y < z (distinto de) 21 la especificaci6n Y:icc para el ariali~ador sititlictico TINY. (Esto rcqtieriri agrcgiir c s t o ~ tukens y rtiiidilicar el itnali~;ailr>r Iéuiw tamhién, pero no dehería !seque- rir i ~ r i c;iirihiii :il irht11 .;intGctico.~

5.38 .\gregue 111s c~pcl-:riiores ho~ile:iiios and, or y no t ir la c?pei'ific;iciím Y;icc [ x ~ i el ;in:ili/ailirr \iot,ictico de TINY. Asígiit.le\ l:i\ pi-i~picildes tlescrii;is cn cl cjcrcicio 1.5. :ulcni.i\ <le iIri:i

Page 260: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]
Page 261: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Capítulo 6

Análisis semántico

6. I Atributos y gramáticas con 6.4 Tipos de datos y verificación atributos de tipos

6.2 Algoritmos para cálculo de 6.5 Un analizador sernántico para atributos el lenguaje TlNY

6.3 La tabla de símbolos

En este capítulo analizamos la fase del compilador que calcula la información ad~cional necesaria para la compilación una vez que se conoce la estructura sintáctica de un programa. Esta fase se conoce como análisis semántico debido a que involucra el cálculo de información que rebasa las capacidades de las gramáticas libres de contexto y los algoritmos de análisis sintáctico estándar, por lo que no se considera como sintaxis.' La información calculada tambikn está estrechamente relacionada con el significado final, o semántica, del programa que se traduce. Como el análisis que realiza un compilador es estático por definición (tiene lugar antes de la ejecución), dicho análisis semántico tambien se conoce como análisis semántico estático. En un lenguaje típico estáticamente tipi- ficado como C. el análisis semántico involucra la construcción de una tabla de símbolos para mantenerse al tanto de los significados de nombres establecidos en declaraciones e inferir tipos y verificarlos en expresiones y sentencias con el fin de determinar su exactitud dentro de las reglas de tipos del lenguaje.

El análisis semántico se puede dividir en dos categorías. La primera es el análisis de un programa que requiere las reglas del lenguaje de programación para establecer su exactitud y garantizar una ejecución adecuada. La complejidad de un análisis de esta clase requerido por una definición del lenguaje varía enormemente de lenguaje a lenguaje. En lenguajes orientados en forma dinámica, tales como LISP y Smalltalk, puede no haber análisis semántico estático en absoluto, mientras que en un lenguaje como Ada existen fuertes requerimientos que debe cumplir un programa para ser ejecutable. Otros lenguajes se encuentran entre estos extremos (Pascal, por ejemplo. no es tan estricto en sus requerimientos estáticos como Ada y C, pero no es tan condescendiente como LISP).

l. Este punto fuc cmneniadci con algún detalle en la secci<h 3.6.3 del c;ipititlo 3

Page 262: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 ANALISIS SEHANTICO

La segunda categoría de análisis semántico es el análisis realizado por un compilado para mejorar la eficiencia de ejecución del programa traducido. Esta clase de análisis por lo regular se incluye en análisis de "optimización", o técnicas de mejoramiento de código. Investigaremos algunos de estos métodos en el capitulo sobre generación de c digo, mientras que en este capitulo nos enfocaremos en los análisis comunes que por exactitud son requeridos para una definición del lenguaje. Conviene advertir que las té nicas estudiadas aqui se aplican a ambas situaciones. También que las dos categorías no son mutuamente excluyentes, ya que los requerimientos de exactitud, tales como la rificación de tipos estáticos, también permiten que un compilador genere código más e ciente que para un lenguaje sin estos requerimientos. Además, vale la pena hacer no que los requerimientos de exactitud que aqui se comentan nunca pueden establecer I exactitud completa de un programa, sino sólo una clase de exactitud parcial. Tales req rimientos todavía son útiles debido a que proporcionan al programador información p mejorar la seguridad y fortaleza del programa.

El análisis semántico estático involucra tanto la descripción de los análisis a reali como la implementación de los análisis utilizando algoritmos apropiados. En este se do, es semejante al análisis léxico y sintáctico. En el análisis sintáctico, por ejemplo, uti zamos gramáticas libres de contexto en la Forma Backus-Naus (BNF, por sus siglas en inglés) para describir la sintaxis y diversos algoritmos de análisis sintáctico descendente ascendente para implementar la sintaxis. En el análisis semántico la situación no es t clara, en parte porque no hay un método estándar (como el BNF) que permita espe ficar la semántica estática de un lenguaje, y en parte porque la cantidad y categoría del análisis semántico estático varía demasiado de un lenguaje a otro. Un método para descri el análisis semántico que los escritores de compiladores usan muy a menudo con buen efectos es la identificación de atributos, o propiedades, de entidades del lenguaje q deben calcularse y escribir ecuaciones de atributos o reglas semánticas, que expr san cómo el cálculo de tales atributos está relacionado con las reglas gramaticales del le guaje. Un conjunto así de atributos y ecuaciones se denomina gramática con atributos. Las gramáticas con atributos son más útiles para los lenguajes que obedecen el principio de la semántica dirigida por sintaxis, la cual asegura que el contenido semántico de un programa se encuentra estrechamente relacionado con su sintaxis. Todos los lenguajes modernos tienen esta propiedad. Por desgracia, el escritor de compiladores casi siempre debe construir una gramática con atributos a mano a partir del manual del lenguaje, ya que rara vez la da el diseñador del lenguaje. Aún peor, la construcción de una gramática con atributos puede complicarse innecesariamente debido a su estrecha adhesión con la estructura sintáctica explícita del lenguaje. Un fundamento mucho mejor para la expresi de los cálculos semánticos es la sintaxis abstracta, como se representa mediante un ár sintáctico abstracto. Incluso. el diseñador del lenguaje, también suele dejar al escritor d compilador la especificacicln de la sintaxis abstracta.

Los algoritmos para la implementación del análisis semántico tampoco sqn tan clara- mente expresables como los algoritmos de análisis sintáctico. De nuevo, esto se debe en parte a los mismos problemas que se acaban de mencionar respecto a la especificación del análisis semántico. N o obstante, existe un problema adicional causado por la tempo- rización del análisis durante el proceso de compilación. Si el análisis semántico se puede suspender hasta que todo el análisis sintáctico (y la construcción de un árbol sintáctico abstracto) esté completo, entonces la tarea de implementar el análisis semántico se vuelve considerablemente más fácil y consiste en esencia en la especificación de orden para un recorrido del árbol sintáctico. junto con los cálculos a realizar cada vez que se encuentra un nodo en el recorrido. Sin embargo, esto implica que el compilador debe ser de paso

Page 263: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Atributos y gramátttas con atributor 259

múltiple. Si. por otra parte, el compilador necesita realizar todas sus operaciones en un solo paso (incluyendo la generación del código), entonces la implementación del análisis semán- tic0 se convierte en mucho más que un proceso a propósito para encontrar un orden correcto y un método para calcular la información semántica (suporiiendo que un orden co- rrecto así exista en realidad). Afortunadamente, la práctica moderna cada vez más permite al escritor de compiladores utilizar pasos múltiples para simplificar los procesos de análisis semántico y generación de código.

A pesar de este estado algo desordenado del análisis semántico, es muy útil para estu- diar gramáticas con atributos y cuestiones de especificación. ya que esto redundará en la capacidad de escribir un código más claro, más conciso y menos proclive a errores para análisis semántico, además de permitir una comprensión más fácil de ese código.

Por lo tanto, el capítulo comienza con un estudio de atributos y gramáticas con atri- butos. Continúa con técnicas para implementar los cálculos especificados mediante una gramática con atributos, incluyendo la inferencia de un orden para los cálculos y los re- corridos del árbol que los acompañan. Dos secciones posteriores se concentran en las áreas principales del análisis semántico: tablas de símbolos y verificación de tipos. La última sección describe un analizador semdntico para el lenguaje de programación TlNY presenta- do en capítulos anteriores.

A diferencia de capítulos previos, este capitulo no contiene ninguna descripción de un generador d e analizador semántico ni alguna herramienta general para construir analizadores semánticos. Aunque se han construido varias de tales herramientas, ninguna ha logrado el amplio uso y disponibilidad de Lex o Yacc. En la sección de notas y referencias al final del capítulo mencionamos algunas de estas herramientas y proporcionamos refe- rencias a la literatura para el lector interesado.

6.1 ATRIBUTOS Y GRAMATICAS CON ATRIBUTOS Un atributo es cualquier propiedad de una construcción del lenguaje de programación. Los atributos pueden variar ampliamente en cuanto a la inf«rmación que contienen, su compleji- dad y en particular el tiempo que les torna realizar el proceso de traducci<ín/ejec~ición cuando pueden ser determinados. Ejemplos típicos de atributos son

El tipo de <latos de una variable E l valor de una expresión La ubicaciún de una variable en la memoria E l cbdigo objeto de un procedimiento El número de dígitos significativos en un número

Los atributos se pueden csrablecer antes del proceso de compilaciún (o incluso la construc- ción de un con~pilador). Por ejemplo, el número de dígitos significativos eri un número se puede establecer (o por lo menos dar un valor mínimo) mediante la definición de un Iengua- je. Ademis, los atributos s6lo se pueden determiriar durante la ejecuciúti tiel progrini;~, ti11

como el valor de una expresicín (no consiante). o la ubicaci6n de una estructura {le datos di- niniicaniente asignada. E l proceso (te calcular un atributo y asociar su valor calculado con la construcciún del lenguaje en cuestión se define corno fijación del atrihuio. El tiempo que toma el proceso de conipilación/ejcc~~ci<íri cn;tnd« se presenta la í'ijacitin de un atributo se de-

iiominii tiempo de f¡,jación. Los iictnpos ilc fijacifin de :itrihutos iliferentes viiríon, c iir- cluso c l niis~no ~iirihuio puetlc teticl. iieinpos ilc: qnci(íri h;istantc tlií'ucntcs (le un !enguii~c

Page 264: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 ANALISIS SEMANTICO

a otro. Los atributos que pueden fijarse antes de la ejecución se denominan estáticos. tras que los atributos que sólo se pueden fijar durante la ejecución son dinámicos. Natur mente, un escritor de compiladores está interesado en aquellos atributos estáticos que s jan durante el tiempo de traducción.

Considere la lista de muestra de atributos dada con anterioridad. Analizamos el ti po de fijación y la significancia durante la compilación de cada uno de los atributos la lista.

En un lenguaje estáticamente tipificado como C o Pascal, el tipo de datos de una vari o expresión es un importante atributo en tiempo de compilación. Un verificador de es un analizador semántico que calcula el atributo de tipo de datos de todas las en des del lenguaje para las cuales están definidos los tipos de datos y verifica que es tipos cumplan con las reglas de tipo del lenguaje. En un lenguaje corno C o Pascal, un v <;

, . ,.,*gj rificador de tipo es una parte importante del análisis seinántico. Sin embargo, en un - lenguaje como LISP, los tipos de datos son dinimicos, y un compilador LISP debe nerar código para calcular tipos y realizar la verificación de tipos durante la ejecu del programa. Los valores de las expresiones por lo regular son dinámicos, y un compilador gener código para calcular sus valores durante la ejecución. Sin embargo, algunas expresi pueden, de hecho, ser constantes (por ejemplo 3 + 4 * S), y un analizador semántico de elegir evaluarlas durante la compilación (este proceso se conoce como incorp ción de constantes). La asignación de una variable puede ser estática o dinámica, dependiendo del le guaje y las propiedades de la variable misma. Por ejemplo, en FORTRAN77 toda variables se asignan de manera estática, mientras que en LlSP todas las variable asignan dinámicamente. C y Pascal tienen una mezcla de asignación de variables e tica y dinámica. Un compilador por lo regular pospondrá los cálculos asociados la asignación de variables hasta la generacibn de código, porque tales cálculos depe den del ambiente de ejecución y. ocasionalmente, de los detalles de la máquina obje- tivo: (El capítulo siguiente trata esto con más detalle.) El código objeto de un procedimiento es evidentemente un atributo estático. El gene- rador de código de un compilador está comprometido por completo con el cálculo de este atributo. El número de dígitos significativos en un número es un atributo que con frecuencia no se trata explícitamente durante la compilación. Está implícito en el sentido de que el escritor de compiladores implementa la9 representaciones para los valores, lo que general se considera como parte del ambiente de ejecución que se analiza en e1 pítnlo siguiente. Sin embargo, incluso el analizador léxico puede necesitar conoce número de dígitos signitlcativos permitidos si va a producir constantes de manera correcta.

Como vimos a partir de estos ejemplos, los cálculos de atributos son muy variados. Cuando aparecen de manera explícita en un conipilador pueden presentarse en cualyuier momento durante la compilación: aun cuando asociarnos el cálculo de atributos más estre- chamente con la fase de ariilisis semántico, tanto el analimdor léxico como el analizador sintáctico pueden necesitar disponer de información de atributo, y puede ser necesarici rea- lizat rilgún anhlisis semántico al mismo tiempo que cl anilisis sintáctico. En este capítulo nos enfocamos en cálculos típicos que se presentan antes de la generacibn de c6digo y tles- puCs del ;inálisis sintáctico (pero vdase la secciiín 6.2.5 para inforriiaci6n sobre el ;inálisis

Page 265: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Atrtbutos y gramáticas con atributos 26 1

semántica durante el análisis sintáctico). El análisis de atributos que se puede aplica; direc- tamente a la generaciiín de código se analizará en el capítulo 8.

6.1.1 Gramáticas con atributos

En la semántica dirigida por sintaxis los atributos están directamente as«ciados con los símbolos gramaticales del lenguaje (los ternlinales y no terminales).' Si X es un símbolo gramatical, y u es un atributo asociado con X , entonces escribirnos X.u para el valor de u asociado con X. Esta notación nos recuerda el indicador de campo de registro de Pascal o (de manera equivalente) una operación de miembro de estructura en C. En realidad, una ma- nera típica de implementar cálculos de atributo es colocar valores de atributo en los nodos de un árbol sintáctico utilizando campos de registro (o miembros de estructura). Veremos esto con más detalle en ta siguiente sección.

Dada una colección de atributos a , , . . . . nk, el principio de la semántica dirigida por sin- taxis implica que para cada regla gratnatical XO -, ,Yl X2 . . . X , (donde Xo es un no terminal, mientras que las otras X i son símbolos arbitrarios), los valores de los atributos X,.a, de cada símbolo gramatical X, están relacionados con los valores de los atributos de los otros síni- bolos en la regla. Si el mismo símbolo X, apareciera más de una vez en la regla gramatical, entonces cada ocurrencia tendría que distinguirse de las otras mediante una subindización adecuada, de manera que se puedan diferenciar los valores de atributo de diferentes ocurren- cias. Cada relación está especificada por uria ecuación de atributo o regla semántica" de la forma

donde& es una función matemática de sus argumentos. Una gramática con atributos pa- ra los atributos u l , . . . , uk es la coleccicín de todas esas ecuaciones para todas las reglas gra- maticales del lenguaje.

En esta generalidad las gramáticas con atributos pueden parecer muy complejas. En la práctica Ias funciones A, por lo regular son bastante simples. Por otro lado, es raro que los atributos dependan de un gran número de otros atributos, así que los atributos pueden sepa- rarse en pequeños conjuntos independientes de atributos interdependientes, y las gramáticas con atributos pueden escribirse de manera separada para cada conjunto.

Por lo general, las gramáticas con atributos se escriben en forma tabular, con cada regla gramatical enumerada con el conjunto de ecuaciones de atributo, o reglas semánticas aso- ciadas con esa regla de la manera siguiente:'

2. La semántica dirigida por sintaxis podría fácilmente ser llamada sintaxis dirigida por scrnáii- tira, puesto que en la mayoria de los lenguajes la sintaxis se diseña con la seniántica (incidental) cIc la construcción ya en mente.

3. En nn irahajo posterior veremos las reglas semánticas como algo mis general que ccoacioms . de :itrihutri. Por ahora, el lector (~rictle consider:irl:is idéoricas.

J. Siempre usarenios el cnc;ibez:ido "reglas sernánticas" en estas tablas en ver de "ecnaciones de :~trihtito" para tener cn ciientti tina intcrpretnción rnds general de las regla scm;intic;rs iiris ;~delanle.

Page 266: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 ANALISIS IEMANTICO

Regla gramatical Reglas sernánticas

Regla I Eciiaciones de atributo asociadas

Kegla n Ecuaciories de iitrihttto asociadas

Continuamos directamente con varios ejemplos.

~jemplo 6.1 Consideremos la siguiente grainática simple para núnreros sin signo:

rtrinzero + riiímero &giro / &ito d i g i t o + O / 1 / 2 / 3 / 4 / 5 / 6 / 7 / 8 / 9

El atributo más importante de un número es su valor, al cual le asignamos el nombre v Ccrrla dígito tiene un valor que es directamente calculable del dígito real que representa. A por ejemplo, la regla gramatical dígiro -+ O implica que dígito tiene valor O en este cas Esto se puede expresar mediante la ecuación de atributo c l~~ i to .va l = O, y asociamos e ecnación con la regla disito -+ O. Adeniás, cada número tiene un valor basado en el díg qne contiene. Si tin número se deriva utilizando la regla,

número + dígito

entonces el número contiene precisamente un dígito, y su valor es el valor de este dígito. L ecuación de atributo que expresa este hecho es

4 mítnero. val = digiro. vid 5

Si un número contiene más de un dígito, entonces se deriva utilizando la regla gramatical

ndmerci 4 nrímero dígito

y debernos expresar la relación entre el valor del sínibolo en el lado izqnierdo de la regla gramatical y los valores de los símbol«s en el lado derecho. Advierta que las dos ocu- rrencias de rtiírnrro en la regla gramatical deben distinguirse. pitesto que el 11N17iero a la derecha tendrá un valor diferente del correspondiente al rirííwro en la izquierda. Los disiin- miremos titilizmdo subíndices. y la regla gramatical la escribiremos con10 se muestra a -,

c«ritinnacicín:

Page 267: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Atributos y gramaticas ron atributos 263

Ahora considere un número como 34. Una derivación (por la izquierda) ile este núinero es de la manera siguiente: niírnem * núnwro iiígito * clígito dígiro * 3 ilíqito 34. Considere el uso de la regla gramatical numerol + rnímero2 di@ en el primer paso de esa deriva- ción. El no terminal ntírnero2 corresponde a1 dígito 3, mientras que díqiro corresponde 211 dígito 4. Los valores de cada tino son 3 y 4: respwtivarnente. Para obtener el valor de n~írnefo~ (es decir, 34), debemos ntultiplicar el valor de n~irnero~ por 10 y agregar cl valor de dígilo: 34 = 3 * 10 + 4. En otras pirlabras, desplü~amos el 3 un lugar decimal hacia la izquierda y sumamos 4. Esto corresponde a la ecuación de atributos

Una gramática con atributos completa para el atributo r d se da en la tabla 6.1. El significado de las ecuaciones de atributo para una cadena particular pueden visua-

lizarse utilizando el árbol de análisis gramatical para la cadena. Por ejemplo, el árbol de análisis gramatical para el número 345 se da en la figura 6.1. En esta figura el cálculo co- rrespondiente a la ecuación de atributo apropiada se muestra debajo de cada nodo iiite- rior. Visualizar las ecuaciones de atributo como cálculos en e1 árbol de análisis grarrtatical es importante para los algoritmos que calculan valores de atrihoto, como vereinos en la sec- ción s ig~ ien te .~

Tanto en la tabla 6.1 como en la figura 6. l pusimos énfasis en la diferencia entre la rc- prcsentacih sintictica de un dígito y el valor, o contenido setnántico, del dígito uti1ir;mclo diferentes fuentes tipográficas. Por ejemplo, en la regln gramatical &ito -+ O , el dígito O es un token 0 carácter, mientras que clígito.viz1 = O significa que el dígito tiene el valor itu- ménco 0.

:. Tdbla 6.1

i: Gramatira con atributos Regla gramatical Reglas semánticas

para el ejemplo 6.1 nrirnerot -+ rnímerot . vril =

Page 268: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 6.1 Árbol stntactico que muestra ~álculos de atr~buto pan el ejemplo 6.1

Ejemplo 6.2

UP. 6 1 ANALBIS SEMANTICO

ruímero (val= 34 * 10 + 5 = 345)

/ \ iiiímem d(&~

(w1= 3 * 1 0 + 4 = 3 4 ) ( v d = 5 )

// \ niimrro dígifo

l 5

( i d = 3) (val = 4 )

1 4 1 dtgito

(val = 3 )

1 3

Considere la gramática siguiente para expresiones aritméticas enteras simples:

esp 4 exp + tenn / exp - term / temz term 4 term *factor 1 factor fucror -+ ( exp ) 1 número

Esta gramática es una versión un poco modificada de la gramática de expresión simpl estudiada extensamente en capítulos anteriores. El atributo principal de una e.rp (o te factor) es su valor numérico, el cual escribimos como val. Las ecuaciones de atributo el atributo val se proporcionan en la tabla 6.2.

Tabla 6.2 Gramática ton atributos Regla gramatical Reglas sern&nticas pan el ejemplo 6.2

expl -t expz + tenn exp, .val = exp2 .sal 4- lerm.val exp, -+ expz - t e m e.xp, .val = expz .val - tem.val ex/? -+ term r.rp.va1 = tem.vul terin, .+ termz "factor reml .vd = t r m , .val * factor.va1 term -t fuct(~r trm.va1 = factor. val factor -r ( e . y ) factor.va1 = exp. val factor -+ número factor. val = número.va1

Estas ecuaciones expresan la relación entre la sintaxis de las expresiones y la semánti- :;! * ca de los cálculos aritméticos que se realizarán. Observe, por ejemplo, la diferencia entre el '4 símbolo sintáctico + (un token) en la regla gramatical

$

e.rp, + exp, + t e m . .

y la operación de adición o suma aritmética + que se realiza en la ecuación

e.rpip, . ~ u l = rxp2 . V L Z I + trrnz. vul

Page 269: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Atr~butos y gramáticas tan atributos 265

Advierta también que no hay ecuación con número.va1 en el lado izquierdo. Coino veremos en la sección siguiente, esto implica que nÚmero.va1 debe calcularse antes de cualquier análisis semjritico que utilice esta gmniática con atributos (por ejemplo, mediante el análisis Ié- xico). De manera alternativa, si queremos que este valor esté explícito en la gramática con atri- butos, debemos agregar reglas gramaticales y ecuaciones de atributo ri la gramática coi1 atributos (por ejemplo, las ecuaciones del ejemplo 6.1).

También podemos expresar los cálculos implicados mediante esta gramática con atribu- tos agregando ecuaciones a los nodos en un árbol de análisis gramatical, como en el ejem- plo 6.1. Por ejemplo, dada la expresión ( 3 4 - 3 i *42, podemos expresar la semántica de su valor en su irbol de análisis gramatical como en la figura 6.2. S,

f ~ g u r a 6 2 A&ol de anillrts gramatical para ( 3 4 - 3 ) * 4 2 que term

muestra cálculor del atributo (val = 31 * 4 2 = 1302)

val para la gramática con term / l \ /¿actor atributos del ejemplo 6.2 (vul= 31) (vul = 42)

1 factor

1 número

(val = 3 1 ) (val = 42)

A\' ( e . v )

(vol = 34 - 3 = 31)

11 e.tp term

(val = 34) (VUI = 3) I I

term faclor (vol = 34) (val = 3 )

I I factor número

(val = 34) ( w l = 3)

I número

(val = 34)

Ejemplo 6.3 Considere la siguiente gramática simple para declaraciones de variable en una sintaxis tipo C:

decl- , type var-list type 4 int / f loat var-list -+ id, var-list / id

Queremos definir un atributo de tipo de datos para las variables dadas por los iden- tificadores en una declaración y escribir ecuaciones que expresen cómo está relacionado el atributo de tipo de datos con el tipo de la declaración. Hacemos esto construyendo una gramática con atributos para un atributo rftype (utilizamos e1 nombre dtype para distin- guir el atributo del no terminal typr).-La gramática con atributos para dtype se da en la tabla 6.3. Harenios las observaciones siguientes respecto a las ecuaciones de atributo de esa figura.

Page 270: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tabla 6.3 Gramática ton atributos Regla gramatical Reglas sernánticas para el ejemplo 6.3

decl -+ rype var-lisr var-listdtype = gpe.<ftype

f igura 6.3 hrbol de analisis gramatical para la cadena f loat

x , y que muestra el atributo dtype como re especifica mediante la gramática ton

atributos de la tabla 6.1

Ejemplo 6.4

CAP. 6 1 ANALISIS SEMAUTICO

En primer lugar, los valores de dtype son del conjunto [integer, r e d ) que correspon a los tokens i n t y f loat. El no terminal type tiene un cltype dado por el token que re senta. Este iltype corresponde al d t ~ p e de la vur-list entera, por la ecuación asociada co regla gramatical para <lecl. Cada i d en la lista tiene este mismo tlope, por las ecuaci asociadas con vur-list. Advierta que no hay ecuación que involucre el & p e del no ter r l d . En realidad, una decl no necesita tener un dtype: no es necesario especificar el v de un atributo para todos los símbolos gramaticales.

Como antes, las ecuaciones de atributo se pueden exhibir en un árbol de análisis gra tical. Se proporciona.un ejemplo en la figura 6.3.

decl

--'- ' \

r ? p var-li.st (~ltypr = real) (dlype = rml )

l ] \ f loat id vur-Iist

( X ) (dtype = r e d ) (dqpe = r e d ) 1

i d ( Y )

(iitype = real)

En los ejemplos que hemos visto hasta ahora, sólo había un atributo. Las gramática atributos pueden involucrar varios atributos interdependientes. El ejemplo siguiente e "situación simple donde existen atributos interdependientes.

Considere una modificaciRn de la gramática numérica del ejemplo 6.1, donde los números pueden ser octales o decimales. Imaginemos que esto se indica mediante un sufijo de un carácter o (para octal) o d (para decimal). Entonces tenemos la gramática signiente:

Page 271: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Atrtbutos y gramaticas con atributos 267

En este caso rmm y dígito requieren un nuevo atributo buse, que se utiliza para calcular el atributo val. La gramática con atributos para hase y vril se proporciona en la tabla 6.4.

Tabla 64 Gramática con atributos Regla gramatical Reglas sernanticas

el ejemplo 6.4 iiurn-hose -t iiiitn-hure i u1 = nirni 1111

nu~n riirhnse nuni.hme = curbase.buse curbase -+ o carhnse.buse = 8 <.~rrbcrse -+ d c«rbuse.hu.re = 1 O nurn, - riurnz digiro riiiin l . w l =

if dígitavul = error or riuin2 . v d = error then error else mimz . vul * num, .hase + rlígiio. vrrl

nurni .hase = nwnl .huse dígito.buse = ririrni .bu.se

riiim -t dígito num. val = ú(qito. val dígito.base = r111m.l~ase

dígifo -t O dígito. vul = O digiro -+ 1 dígiro. vul = I

if dígif<t.bu.se = 8 then error else 8 rlí& + 9 dígiro.vul =

if d(qito.base = 8 then error eise 9

Deberían observarse dos nuevas características en esta gramática con atributos. En pri- mer lugar, la gramática BNF no elimina por sí misma la conibinacicín errónea de los dígitos (no octales) 8 y 9 con el sufijo o. Por ejeniplo, la cadena 1890 es sintácticamente correcta de acuerdo con la BNF anterior, pero no se le puede asignar ningún valor. De este modo, es necesario un nuevo valor error pxa tales casos. Además, la gramática con atributos debe expresar el hecho de que la introducción de 8 o 9 en un número con un sufijo o produce un valor de error. La manera más fácil de hacer esto es emplear una expresión if-then-else en las funciones de las ecuaciones y atributo apropiadas. Por ejemplo, la ecuación

num .val = i f dígitaval = error nr numz .val = error then error else num2 .val * numl .buve + dígiro.vu1

correspondiente a la regla gramatical numI -t num2 dígito expresa e1 hecho de que si cual- quiera de nctin2.vcil o rli~ito.vu1 es error entonces numi.vul también debe ser error, y scílo si no es ése el caso rinrnl.~~irl está dado por la fórmula n~irn~.vnl * nurni.b~ise + cli,ito.vnl.

Para coticluir con este ejemplo mostramos de nueva cuenta los cálculos de atributo en un irbol de ;inálisis gramatical. La figura 6.4 ofrece un árbol de análisis gramatical para el ~iúinero 3450,junto con 10s valores de atributo calculados de acuerdo con la gramática con atributos de la tabla 6.4.

Page 272: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 6.1 ~ r b o l de analisis gramatical que muestra cálculos de atributo para el ejemplo 6.4

6.1.2 Simplificaciones y extensiones a gramática con atributos

El uso de una expresión if-then-else extiende las clases de expresiones que pueden cer en una ecuación de atributo de una manera útil. La colección de expresiones perm en una ecuación de atributo se conoce como metalenguaje para la gramática con atributos Por lo regular queremos un nietalenguaje cuyo significado sea suficientemente claro p que no surja confusión sobre su propia seniántica. ?'anibién deseamos un metalenguaje q sea cercano a un 1enguaje.de programación real, ya que, como veremos en breve, quere convertir las ecuaciones de atributo en código de trabajo en un analizador semántico. este libro utilizamos un metalenguaje que está limitado a expresiones aritméticas, lógicas algunas otras clases de expresiones, junto con una expresión if-then-else, y ocasionalmen te una expresión case o switch.

Una característica adicional útil a1 especificar ecuaciones de atributo es agregar al me- talenguaje el uso de funciones cuyas definiciones se puedan dar en otra parte. Por ejemp en las gramáticas para números hemos estado escribiendo ecuaciones de atributo para ca una de las opciones de rlígito. En cambio, podríamos adoptar una convención más con escribiendo la regla gramatical para dígito como dígito -+ D (donde se entiende que D es u de los dígitos) y entonces escribir la ecuación de atributo correspondiente como

digito. vul = tzurnvul(r))

Aquí nurnvctl es una función cuya definición debe ser especificada en otra parte como un su- plemento para la graniiítica con atributos. Por ejemplo, podemos d:ir la siguiente dctlnición de nunriul corito c6digo en C:

int numval(char D)

{ return (int)D - íint)'O';)

Page 273: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Auibuter y gramáticar con atributos 269

Una shptificación adicional que puede ser útil al especificar gramática con atributos es utilizar una.fomaambigua, pero más simple, de 1a.gramática ariginal. De hecho, puesto que se supone queel analizador sintáctico ya fue construido, toda ambigüedad se habrá aborda- do en esta e,tapa, y la gramática con atributos puede basarse libremente en construcciones ambiguas, sin implicar . , ninguna ambigüedad en los atributos resultantes: Por ejemplo, la gra- mática'de expresión aritmdtica del ejemplo 6.2 tiene la siguiente forma más sencilla, pero ambigua:

exp -+ exp + e x p / exp - e x p 1 e x p e a p / ( e x p ) / número

Cuando se utiliza esta gramática el atributo vdl se puede definir mediante la tabla, que se muestra en la tabla 6.3 (compare esta con la tabla 6.2). , .

Regla gramatical ~ e ~ f & semánticas

exppf -+ exp2 + expl expt .va¡, = e x p ~ . val t exp3 .val e.xp, -9 e.ip2 - exp3 exp, .val = expz .val - e.rp3 .val exp, -+ expz * exp3 exppc .val = exp, .val * exp3 .val ex& -+ ( e% exp, .val = exp, ,val exp -+ aúmaro exp .~aaf número .val

Tambien se puede hacer una simplificación exhibiendo valores de atributo mediante el uso de un árbol sintáctico abstracto en lugar de un árbol de análisis gramatical. Un árbol sin- táctico abstracto debe tener siempre la estructura suficiente, de manera que se pueda expre- sar la semintica definida mediante una gramática con atributos. Por ejemplo, Ia expresión

isis gramatical y atributos val se mostraron en la figura 6.2, pueden tener su sem pletarnente expresada mediante el árbol sintáctico abstracto

el árbol sintáctico mismo se pueda especificar mediante una ve en el ejemplo siguiente.

Figura 6.5 Arbol sintactico abstracto para (34-3 ) *42 que muestra los cálculor de atributo de val para la gramitica con atributos de la tabla 6.2 o la tabla 6.5

(val =

/ 34

(val = 34)

(val = 31 * 42 = 1302)

- (sal = 3)

Ejemplo 6.5 Dada la gramática para expresiones aritméticas enteras simples del ejemplo 6.2 (página 264), podemos definir un árbol sintáctico abstracto para expresiones mediante la gramática con atributos dada en la tabla 6.6. En esta gramática con atributos utilizamos dos funciones auxiliares rnkOpNode y mkillumNotle. La función mkOpNode toma tres parámetros (un token de operador y dos árboles sintácticos) y construye un nuevo nodo de árbol cuya eti- queta de operador es e1 primer pi~rámetro. y cuyos hijos son el segundo y tercer parámctros. La función tnkiVurnNocfe toma u n parámetro (un valor numérico) y construye u n nodo hoja

Page 274: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 ANALISIS SEHANTICO

representando un número con ese valor. En la tabla 6.6 escribimos el valor numérico c número.le-xvd para indicar que es construido por el analizador léxico. De hecho, éste dria ser el valor numérico real del número, o su representación de cadena, dependiendo la implementación. (Compare las ecuaciones de la tabla 6.6 con la construcciún descen te recursiva del irbol sintáctico de TlNY en el apéndice B.)

Tabla 6.6 Gramática con atributos para Regla gramatical Reglas semanticas

árboles sintácticos abstractos expi -r expz + term erp E . tree =

o expresiones aritméticas mkOph%>rle (+. erp2 .tree, term. rrre)

enteras simples erp --, e.cp2 - trrm a p i .Irre =

mkOpN<ide(-, expz . tree, rerm.tree) etp -+ term exptree = termmre ler1nl -+ f e m 2 *fili;ror ferm, .&e =

mkOpNoiie(*. term2 . tree, firctor. tree) terrn + fuctor term.rree = fuctor.tree factor -+ ( exp ) factor.tree = exp.tree ,f'ocror -+ número factor.tree =

mkNumNodefnúmero.lrxva1)

Una cuestión que es fundamental para la especificación de atributos utilizando gr inática con atributos es: jeómo podemos estar seguros de que una gramática con atrib tos en particular es consistente y completa, es decir, que define de manera única los a butos dados? La respuesta simple es que hasta ahora no podemos. Ésta es una cuestió similar a la de determinar si una gramática es ambigua. En la práctica, son los algoritm de análisis sintáctico que utilizamos lo que determina la aceptabilidad de una gramáti y ocurre una situación semejante en el caso de las gramáticas con atributos. De este ni do, los métodos algorítmicos para calcular atributos que estudiaremos en la siguie sección determinarán si una gramática con atributos es adecuada para definir los valore de atributos.

62 ALGORITMOS PARA CALCULO DE ATRIBUTOS En esta sección estudiaremos las maneras en que se puede utilizar una gramática con a butos como base para nn'compilador a fin de calcular y utilizar los atributos definidos p las ecuaciones de la gramática con atributos. Fundamentalmente, esto equivale a convertir las ecuaciones de atributo en reglas de cálculo. De este modo, la ecuación de attibiito

se visualiza como una asignación del valor de la expresión funcional en el lado derecho para el atributo X,.ai. Para que esto tenga éxito, los valores de todos los atributos que aparecen en el lado derecho ya deben existir. Este requerimiento se puede pasar por alto en la espe- cificaciitn de las gramáticas con ;itributos, donde las ecuaciones se pueden escribir en orden arbitrario sin afectar su validez. El problema de iniplementar un :ilgoritnio corre.;pr)ndicnte

Page 275: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Algoritmos para calculo de atributos 27 1

n una gramática con atributos consiste ante todo en hallar un orden para la evaluación y asig- n:ición de atributos que aseguran que todos los valores.de atributo titiliz;id«s en cada cálciilo estén disponibles cuando éste se realice. Las mismas ecuaciones de atrihuto indican las limitan- tes de orden en el cálculo de los atributos, y La primera tarea es hacer explícitas las liinit;intes de orden utilizando gráficas tlirigitlas para representarlas. Estas gráficas dirigidas se conocen como gráficas de deperidcncia.

6.2.1 Gráficas de dependencia y orden de evaluación Dada una gramática con atributos, cada regla gramatical tiene una gráfica de dependencia asociada. Esta gráfica tiene un nodo etiquetado por cada atributo Xl.ci, de cada símbolo en la regla gramatical, y para cada ecuación de atributo

asociada con la regla graniatical hay un borde desde cada nodo X,,.u, en el lado derecho para el nodo Xi.czj (q~ie expresa la dependencia de Xi.q respecto a X,,.ci,). Dada una cadena legal en el lenguaje generado por la gramática libre de contexto, la gráfica de dependencia de la cadena es la unión de las gráficas de dependencia de las reglas gramaticales que repre- sentan cada nodo (no hoja) del árbol de análisis gramatical de la cadena.

Al dibujar la gráfica de dependencia para cada regla gramatical o cadena, los nodos aso- ciados con cada síinbolo X son trzados en u11 grupo, de manera que las dependencias se puedan visualizar como estructuras alrededor de un árbol de análisis gramatical.

Ejemplo 6.6 Considere la gramática del ejemplo 6.1. con la gramática con atributos como se proporcio- na en la tabla 6. l . Hay sólo ~ i n atrihuto, wl , de modo que para cada sí~nbolo existe sólo un . . nodo en cada gráfica de dependencia, correspondiente a su atributo val. La regla gramatical número, -+ rihnero2 digito tiene la ecuació~i de atributo asociada simple

izlhzero, .red = número2 .val * 10 + clígito.vn1

La gráfica de dependencia para esta regla gramatical es

(En futuras gráficas de dependencia omitiremos los subíndices para símbolos repetidos, ya que la representación gráfica distingue claramente las diferentes ocurrencias como nodos distintos.)

De manera semejante, la gráfica de dependencia para la regla gratnatical rtcímero -+ dígito con la ecuacih de atributo r~rítnero.vcil = clígito.vc11 es

Page 276: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

WP. 6 1 ANALISIS SEN~NTICO

Para las reglas gramaticales restantes de la brma dígiro -+ D las gráficas de depend son triviales (no hay bordes), ya que dígito.vu1 se calcula directamente del lado derec la regla.

Finalmente, la cadena 345 tiene la siguiente gráfica de dependencia correspondien su árbol de análisis gramatical (véase la figura 6.1):

Ejemplo 6.7 Considere la gramática del ejemplo 6.3 con la gramática con atributos para el atrib tltype que se da en la tabla 6.3. En este ejemplo la regla gramatical vur-lisr, + i d , vur-11 tiene las dos ecuaciones de atributo asociadas

y la gráfica de dependencia

De manera similar, la regla gramatical vcir-list + i d tiene la gráfica de dependencia

var-1ist.dtype

1 i d . &pe

Las dos reglas cype -+ i n t y type -+ íloat tienen gráficas de dependencia trivides. Finalmente, la regla decl i type wr-l ist con la ecuación asociada vnr-1ist.dtype =

tvpe.ilt)ye tiene la gráfica de dependencia

En este caso, como &el no está involncrada de manera directa en la gráfica de dependen- cia, no está conipletamente claro cuál regla gramatical tiene su gráfica asociada a ella. Por esta razón (y algunas otras razones que comentaremos posteriormente). a menudo dibuja- rnos la griifica de dependencia sribrepucsta sobre un segrncnto de &bol de análisis gramati- cal correspondiente a la regla jrarnatical. De este modo. la gráfica (fe &pendencia anterior se p c d e dibujar como

Page 277: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Algoritmos para dl tulo de atributos

y esto hace más clara la regla gramatical a la que está asociada la , denendencia. Advierta tariibién que cuando dibujamos los nodos del árbol de análisis gramatical stiprimirrios la no- tación punto para los atributos, y representamos los atribiitos de cada nodo escribi6ndolos a continuaci6n de su nodo asociado. Así, la primera gráfica de dependencia en esle ejemplo tamhih se puede escribir como

Finalmente, la grifica de dependencia para La cüdenü f loat x, y es

float dype id r ~Irype vnr - l i ~ t

(X) 1 1 1 úfype id

(Y)

Considere la gramática de números de base del ejerriplo 6.4 con la gramática con atributos para los atributos buse y vol como se dan en la tabla 6.4. Dibujaremos las gráficas de depen- dencia para las cuatro reglas graniaticales

num-base + num carbuse num -3 nurn dígito nurn 4 digito dígito 4 9

y para la cadena 3450, cuyo árbol de aiiálisis gramatical se muestra en la figura 6.4. Comenzamos con la gráfica de dependencia para la regla gramaticül rrutrr-btrse -+ rrirm

L Y ~ ~ / x J . s ~ :

Page 278: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 6 6 Gráfica de dependencia para la cadena 3450

(e'jemplo 6.8)

tical trwn -. num dígito:

E ~ t a gráfica erpre\a las dependencias de las tres ecuaciones de atributo

nurn I . vctl = if dígito.vu1 = error or mm2 . v d = error then error else num2 .val * nrtml .base + dígito. vul

n m z .base = nuin, .buse ~lí,qito.hase = numl .buse

La gráfica de dependencia para la regla gramatical num -+ dígito es similar:

huse num vul

Finalmente, dibujarnos la gráfica de dependencia para la regla gramatical tlígito -i 9:

CAP. 6 1 ANÁLISIS IEMANTICO

Esta gráfica expresa las dependencias de las dos ecuaciones asociadas rturn-huse.vul = num. y iiutri.hase = carbuse.base.

A continuaci6n dibu&imos la gráfica de dependencia correspondiente a la regla gram

Esta gráfica expresa la dependencia creada por la ecuación digito.vul = if ciígito.base = then error else 9, a saber, que dígito.ial depende de dígito.buse (es parte de la prueba en expresión if). S610 resta dibujar la gráfica de dependencia para la cadena 3450. Esto se h ce en la figura 6.6.

nrirn-base val . . . - , . . / S : C c~rbuse2se I I

, 1

base dígito val O 1 1 1

hose ririm i.iil - hase díi'ito r . d 5 l t l

1 1

4

Page 279: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Algoritmos para cálculo de atributos 275

Supongamos &ora que deseamos determinar un algoritn~o que calcula los citrihutos de una gramática con atributos utilizando las ecuaciones de atributo como reglas para el cálcu- lo. Dada una cadena particular de tokens para traducirse, la gráfica de dependencia del árbol de anilisis gramatical de la cadena da un conjunto de liniitantes de orden bajo el cual el algoritmo debe operar para calcular los atributos de esa cadena. En realidad, cualquier al- goritmo debe calcular el atributo en cada nodo de la gráfica de dependencia cirires tie inten- tar catcular cualquier atrihuto sucesor. Un orden del recorrido de la gráfica de dependencia que obedece esta restricción se denomina cIasificaciFn topológica. y una muy conocida condición necesaria y suficiente para que exista una clasificación topológica es que la grá- fica debe ser acíclica. Tales gráficas se conocen como gráficas acíclieas dirigidas, o DAG, por sus siglas en inglés.

La gráfica de dependencia de la figura 6.6 es una DAG. En la figura 6.7 numeramos los nodos de la gráfica (y eliminamos el árbol de análisis gramatical subyacente para facilitrir la visualización). Una clasificación topológica está dada por el orden de nodo en el que los nodos están numerados. Otra clasificación topológica está dada por el orden

Una cuestión que wrge en el uso de una clasificación topológica de la gráfica de depen- dencia para calcular valores de atributo es cómo se encuentran los valores de atributo en las raíces de la gráfica (una raíz de una gráfica es un nodo que no tiene predecesores). En la fi- gura 6.7, los nodos 1,.6, 9 y 12 son todos raíces de la g r á f i ~ a . ~ Los valores de atributo en estos nodos no dependen de ningún otro atributo, entonces se deben calcular utilizrindo la información que está directamente disponible. Esta información se encuentra a rnenudo en forma de tokens que son hijos de los correspondientes nodos de %bol de artálisis gramati- cal. En la figura 6.7, por ejemplo, el val del nodo 6 depende del token 3 que es el hijo del nodo digito a1 que val corresponde (véase la figura 6.6). De este modo. e1 valor de atributo en el nodo 6 es 3. Todos esos valores raíz necesitan ser calciilahlei antes del cálculo de conl- quier otro valor de atributo. 'Tales cálculos se realizan con frecuencia mediante los :inriliza- dores léxico 0 sintáctico.

Page 280: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Es posible basar un algoritmo para análisis de atributos en una construcción de la gr ca de dependencia durante la compilación y una clasifimción topológica subsiguiente d gráfica de dependencia con el fin de determinar un orden para la ev:iluacióu de los a tos. Como la construcción de la gráfica de dependencia está basada en el irbol de an gramatical específico en tiempo de compilación, este método se conoce en ocasiones c método de &bol de análisis yametical. Dicho método es capaz de evaluar los atri en cualquier gramttica con atributos que sea no circular, es decir, una gramática con butos para la cual toda gráfica de dependencia posible es acíciica.

Existen varios problema con este método. En primer lugar, tenemos la complejidad gada requerida por La constmcción en tiempo de compilación de la gráfica de depende En segundo lugar, aunque este método puede determinar si una gráfica de dependenc acíclica en tiempo de compilación, por lo general es inadecuado esperar hasta el mom de compilaci6n para descuhrir un círculo vicioso, ya que es casi seguro que este represen un error en la gramática con atributos original.En rralidad, para evitarlo debería pro por adelantado una gramática con ahbutos. Existe un algoritmo para hacer esto (véa sección de notas y referencias), pero es un algoritmo de tiempo exponencial. Natural este algoritmo sólo necesita ejecutarse una vez. en el tiempo de construcción del cornpil de modo que éste no es un argumento aplastante en su contra (al menos para los pro tos de la constrncción del compilador). La complejidad de este método es un argumento convincente.

La alternativa al método anterior para evaluación de atributos, por la que opta prác mente todo compilador, es que el escritor de compiladores analice la gramática con atri tos y fije un orden para La evaluación de atributos en tiempo de constmcción de compil Aunque este método todavía utiliza el &bol de análisis gramatical como guía para evalu de atributos, el método se conoce como método basado en reglas, ya que depende de un a lisis de las ecuaciones de atributo, o reglas semánticas. Las gramáticas con atributos para cuales un orden de evaluación de atributos se puede fijar en tiempo de constmcción del co pilador forman una clase que es menos general que ta clase de todas las gramáticas con a hutos no circulares, pero en la práctica todas las gramáticas con atributos razonables tien esta propiedad. A menudo se les denomina gramáticas con atributos completamente circulares. Continuaremos con una exposición de los algoritmos basados en reglas p esta clase de gramáticas con atributos después del ejemplo siguiente.

Ejemplo 6.10 Considere nuevamente las gráficas de dependencia del ejemplo 6.8 y las clasificaciones pológicas de la gráfica de dependencia analizada en el ejemplo 6.9 (vkase la figura 6.7). A cuando los nodos 6,9 y 12 de la figura 6.7 son raíces de la DAG, y, por consiguiente, t podría ocurrir al principio de una clasificaci<ín topológica, en un método basado en r esto no es posible. La razón es que cualquier val puede depender de la base de su nod gito asociado, si e1 token correspondiente es 8 o 9. Por ejemplo, la gráfica de dependenci para digito -+ 9 es

Así, en la figura 6.7 el nodo 6 puede haber dependido del nodo S, el nodo 9 puede haber de- pendido del nodo 8, y el nodo 12 puede haber dependido del nodo I l . En un método basa- d« en reglas estos nodos serían forzados a ser evaluados después que cualquier n d o del que ellos pudier:rn dcpznder. Por lo tanto. kin orden de evaluaci6n que. itigatnos, evaluara prinie-

Page 281: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Algoñtmos para calculo de atubutos 277

ro el nodo 12 (véase el ejemplo 6.91, aunque es correcto para el árbol particular de la ligu- ra 6.7, no es un orden vilido para un algoritmo basado en reglas, puesto que violaría el or- den pira otros ;írbolcs de análisis grinnatical. (S

6.2.2 Atributos sintetizados y heredados La evaluacicín de atributos bas:tdos en reglas depende de un recorrido cxplícito o iniplícito del irbol de análisis gramatical o del árbol sintáctico. Diferentes clases de recorridos varian en potencia en términos de las clases de dependencias de atributo que se pueden manejar. Para estudiar las diferencias primero debemos clasificar los atributos mediante las clases de dependencias que exhiben. La clase más simple de dependencia a manejar es la de los atri- butos sintetizados, que se define a continuación.

. .. *.. g: Definición 9. t.

Un atributo es sintetizado si todas sus dependencias apuntan de hijo a padre en el árbol de an a '1' t s~s .' gramatical. De manerd equivalente, un atributo u es sintetizado si, dada una regla gramatical A + XIX2 . . . X,,. la única ecuación de atributo asociada con una a en el lado izquierdo es de la forma

Una gramática con atributos en la que todos los atributos están sintetizados se conoce como gramática con atributos S.

Ejemplo 6.1 1

Ya vimos varios ejemplos de atributos sintetizados y gramáticas con atributos S. El atri- buto val de números en el ejemplo 6.1 está sintetizado (véanse las gráficas de dependencia en el ejemplo 6.6, página 271), como el atributo vtil de las expresiones aritméticas enteras simples en el ejemplo 6.2, página 264.

Dado que un árbol de análisis gramatical o árbol sintactico se ha construido median- te un analizador sintictico, los valores de atributo de una gramática con atributos S se pueden calcular median. un recorrido ascendente, o postorden, del árbol. Esto se puede expresar mediante el siguiente pseudocódigo para un evalnador de atributo postorden re- cursivo:

procedure PastEva~ ( T: treeiiocle ); hegin

for cada hijo C de T do Po.stEvul( C );

calcule todos los atributos sintetizados de T; end;

<:onsiileremos la gratnática con atributos del ejcriiplo 6.2 para expresiones aritméticas sirn- pies, con el atributo sintetizado vid. Dada la estructura siguiente para un árbol siniiclico 4 tal corno cl de la i'igt~ra 6 5 . página 261))

Page 282: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 6.8 Código C para el evaluador de atributo postorden para el ejemplo 6.1 1

Definición

typedef enum (Pius,Minus,Times) OpKind;

typedef enum {OpKind,ConstKind} ExpKind;

typedef struct streenode

f ExpKind kind;

OgKind og;

struct streenode *lchild,*rchild;

int val;

> STreeNode; typedef STreeNode *SyntaxTree;

el pseudocódigo PostEval se traduciría en el ccídigo C de la figura 6.8 para un recomd izquierda a derecha.

void postEval(SyntaxTree t)

{ int tenp;

if (t->kind = OpKind) ( postEval(t-7lchild);

postEval(t-zrchild);

switch (t->op)

{ case Plus:

t->val = t->lchild->val + t->rchild->val; break;

case Minus:

t->val = t->lchild->val - t->rchild->val; break;

case Minus:

t->val = t->lchild->val * t->rchild->val; break;

1 / * end switch * / 1 / * end if * / / * end postEva1 * /

Por supuesto, no todos los atributos son sintetizados.

Un atributo no sintetilado \e denomina heredado.

Ejemplos de atributos heredados que ya hemos visto incluyen al atributo dtwe del ejernplo 6.3 (página 265) y cl atributo base del ejemplo 6.4 (página 266). Los atributos he- redados tienen dependencias que fiuyen ya sea de padre a hijos en el árbol de análisis gra- matical (a lo que deben su nombre) o de hermano a hermano. Las figuras 6.9(a) y ib) ilus- tran las dos categorías hásicas de dependencia de atributos heredados. Cada una de estas c ~ . 'ises . . de dependencia se preserttrtn en el ejemplo 6.7 para el atributo cbpe L.a r a h cle que

;unhas se clasifiquen corno herellatlas es que en algorilmos para calcular atribut«s hereda- dos, la hereucia entre hermanos a itietiudo se implcmenta de tal tnaiiera que los valores de atributo se pasen de herniano a herntano u t r c d s del padre. En efec&, esto es necesario \ i los bordes del irbol siiirácticcr d o apuntan ile padre a hijo (de este tnoclo un hijo no puede

Page 283: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Algoritmos para calculo de atributos 279

Ejemplo 6.12

tener acceso a su padre 0 hermanos directamente). Por otra p;rrte, s i algunas estructuras en un irbol sintáctico se implementan a través de apuntadores hermanos, entonces la herencia entre hermanos puede coniinuar directarnerite a lo largo de una cadena de herr~iario, corno se representa en la figura h.%c).

(a) Herencia de padres a hermanos (b) Herencia de hermano a hermano

(c) Herencia hermana vía apuntadores hermanos

Volvemos ahora a Los métodos algorítmicos püra La evaluación de atrihutos heredados. Los atributos here&ados se pueden calcular nlediante un recorrido preorden, o combinado preordenlenorden del árbol de análisis gramatical o del árbol sintáctica. De manera esque- mática. esto se puede representar mediante el siguiente pseudocódigo:

procedure PreEv<d ( T: tremode ); begin

for cudu hijo C de T do cczlcule todos los cztrihutos heredados úe C; PrcEvrzl( C );

end;

A diferencia de los atributos sintetizados, el orden en e l que se calculan los atrihutos here- dados de los hijos es importante, porque los atributos heredados pueden tener dependencias entre los atributos de los hijos. El orden en el que se visita cada hijo C de Ten e l pseudocó- digo precedente debe, por lo tanto, adherirse a cualquier requerimiento de estas dependen- cias. En los dos ejemplos siguientes demostrareinos esto utilizando los atrihutos heredados clt)~pe y base de 10s ejemplos anteriores.

Considere la gramática del ejemplo 6.3 que iiene e l atributo heredado cliy)e y cuyas gráfi- cas de dependencia se proporcionan en e l ejemplo 6.7, página 272 (véase la gramática con atributos en la tabla 6.3, pigina 266). Sup«ng;irnos primero que un árbol de anilisis gra- matical se ha construi<lo explicit:imente a partir de la gramática, la que repetimos aquí para poder hacer referencia a ella con facilidxl:

tlccl -, tvpe tur-li.st ypr - int / f loat i:cii.-Iist 4 id, ~<ri.-li.st 1 id

Page 284: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 ANALISIS SEIIANTICO

Entonces el pseudocódigo para un procedimiento recursivo que calcula el atributo cl para todos los nodos requeridos se muestra a continuación:

prncedure EvalType ( T: treenode ); begin

case clasenodo de T of decl:

EvalType ( tipo hijo de T ); Asignar dvpe de tipo hijo de T a vur-list hija de T; EvnlType ( var-list hija de T );

m'<: if hijo (le T = int then T.drype := intcger else T.dtype : = real;

var-list: asignar T.diype at-primer hijo de T; if tercer hijo de T no es ni1 then

asignar Tdtype u1 tercer hijo; EvalType ( tercer hijo de T );

end case; end EvalType;

Advierta cómo operaciones preorden y en orden están mezcladas, dependiendo de la cl diferente de nodo que se está procesando. Por ejemplo, un nodo decl requiere que el d de su primer hijo se calcule primero y después se asigne a su segundo hijo antes de una mada recursiva de EvalType a ese hijo; éste es un proceso en orden. Por otra parte, un no wrr-list asigna drype a sus hijos antes de hacer cualquier llamada recursiva; éste es un p ceso preorden.

En la figura 6.10 mostramos el árbol de analisis gramatical para la cadena f loat y junto con las gráficas de dependencia para el atributo drype, y numeramos los nodos e orden en e1 cual se calcula drype de acuerdo con el pseudocódigo anterior.

Figura 6.10 _ decl -. . ,C -. Arbol de anilisis gramatical . . . -. -. que muestra orden drvpe - d e uar-list @ de recorrido p a n el I I /'/y..-..

I 8'

ejemplo 6.12 I '--. float &pe i d @ , &pe uar-list @

ix) 1 1 i dope i d @

(Y f

Para proporcionar una forma completamente concreta a este ejemplo, convertimos el pseudocódigo anterior a código C real. También, en vez de utilizar un 6rbol de anrili- sis gramatical explícito. suponemos que se ha construido un árbol sintáctico, en el que ~ur-list se representa mediante una lista de hermanos de nodos i d . Entonces una cade- na de declaración tal como f loat x, y tendría el árbol sintáctico (compare esto con la figura 6.10)

Page 285: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Algoritmos para calculo de atributos

y el orden de evaluación de los hijos del nodo decl sería de izquierda a derecha (primero el nodo type, luego el nodo x y finalmente el nodo y). Observe que en este árbol ya incluimos el dtype del nodo type; suponemos que esto se ha calculado previamente durante el análisis sintáctico.

La estructura del jrbol sintáctico la proporcionan las siguientes declaraciones en C:

typedef enum {decl,type,id) nodekind;

typedef enum (integer,realf typekind;

typedef struct treeNode

( nodekind kind;

struct treeNode

lchild, * rchild, * aibling; typekind dtype;

/ * para nodos id y type * /

char * name; / * 8610 para nodos id */ * SyntaxTree;

El procedimiento EvulType tiene ahora el código correspondiente en C:

void evalrype (SyntaxTree t)

{ switch (t->kind)

( case decl:

t->rchild->dtype = t-plchild->dtype;

evalType(t->rchild);

break;

case id:

if (t->sibling l = NULL)

{ t->aibling->dtype = t->dtype;

evalType(t->sibling);

1 break;

) / * end switch * /

> / * end evalType * /

Este ccítiigo se puede simplificar al siguiente procedimiento no recursivo, que funciona en- teramente al nivel del nodo raíz (decl):

Page 286: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

282 CAP. 6 1 ANALISIS SEMANTICO

void evalTne ( SyntaxTree t )

( if (t->kind -= decl) f SyntaxTree p = t->rchild;

P->dtme = t->lchild->dtype; while (0->sibling ! = MILL)

( p->sibling->dtype = p->dtype; p = p->sibling;

1 1 / * end if * /

). / * end evalType * /

Ejemplo 6.13 Considere la gramática del ejemplo 6.4 (página ?66), que tiene el atributo heredado base gráficas de dependencia están dadas en el ejemplo 6.8, pígina 273). Repetiremos la gr tica de ese ejemplo aquí:

nirm-base 1; num curbuse carbase 4 o / d tium 1; num digito / cligito d í g i t o - . 0 / 1 / 2 / 3 ~ 4 / 5 / 6 / 7 / 8 / 9

Esta gramfitica tiene dos nuevas características. En primer lugar, existen dos atributos, atributo sintetizado val y cl atributo heredado base, del cual depende val. En segundo Lu el atributo base es heredado del hijo derecho al hijo izquierdo de un num-base (es de desde carbase hasta num). Así, en este caso debemos evaluar los hijos de un nztm-base derecha a izquierda en lugar de izquierda a derecha. Procederemos a dar el pseudocód para un procedimiento EvulWithBase que calcula tanto base como val. En este caso buse calcula en preorden y vul en postorden durante un paso simple (en breve comentaremos cuestitín de pasos y atributos múltiples). El pseudocódigo se presenta a continuación (vd la gramática con atributos en la tabla 6.4, página 267).

procedure EvalWithBuse ( T : treenode ); hegin

case clusenodo de T of num-base:

EvalWithBase ( hijo derecho de T ); asignur base del hijo derecho de T a la base del hijo izquierdo; EvulWi?hBase ( hijo izquierdo de T j ; asignar valor de hijo izquierdo de T a T.vul;

tz11m: asignar T.base a la buse del hijo izquieráo de T; EvulWibhBase ( hijo derecho de T ); if hijo derecho de T no es ni1 then

nsignur 7 b m e a 1 1 huse del hijo derecho (le T; EvairlCVithBase ( hijo derecho de T ) ; if wlores de hijos izquierdo y derecho # error then

T.vd : = T.bnse*(vulor de hijo izquier(1o) + wlo r de hijo rlrrt.clzo else % 1x11 : - error;

Page 287: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Algontmor para cálculo de atributos

c.trrhose: if hijo de T := o then l h a s e :-. 8 else T. h<ise : = 10;

t lQ i t0 : if T.hase = S and &jo de T = 8 or 9 then T.wd := error else %val : = rirtntvrd ( hijo de 7' );

end case; end Evc~lWithB<i.se;

De,iamos para 10s ejercicios la construcción de las declaraciones en C en el caso de un árbol sintáctico apropiado y tina traducción del pseudocódigo para E~~ulWithRnse a código en C.

§

En las gramjticas con atributos con combinaciones de atnbtítos sintetizados y heredados, si los atributos sintetizados dependen de los atributos heredados (además de otros ütrihtitos sintetimios). pero los atributos heredados no dependen de ningún atributo sintetiza&, en- tonces es posible calcular todvs los atributos en un solo paso sobre el árbol de análisis gra- matical o el árbol sint2ictico. El ejemplo anterior es un buen ejemplo de cómo se hace esto, y el orden de evaluación puede resurnirse combinando los procedimientos en pseudoc6digo PostEvtrl y PreEscil:

hegin for radu hijo C de T do

c<rlcultr todos los uirihui»s Irereclridos de C; Comhiizc4Eial ( C );

rnlccdr roilor los ntnbutos rrnrt,tr:~rdoc de T; end;

Las situiiciones en las cuales los atributos hcredatlos dependen de atributos sintetizados son más complejas y requieren de mis de un pííso sobre los árboles de análisis gran1:itical « sin- kíctico, como lo rnuestra el siguiente ejemplo.

Ejemplo 6.14 Considere la siguiente versión simple de una gramática de exprcsión:

Esta gramática tiene una sola operación, de división, indica& por el toke~i /. Tarribién tiene (los ver,iortes de núrneros: números enteros cvmpvestos de secuencias de dígitos. las c~mies se indican iiietiiürite el token num y números de punto flotante, que se indican por medio del token num.num. La idea de esla gramática ei que las operaciones se pueden iorcr- pretar de manera diferente, dependiendo de si son operaciones de pu~ito flotante o esiric- t:imcnte entenis. La división, en pxticulx, es bastante difcrentc. clepcrldiciido (le j i se permiten las í'rxxiones. Si no. la división con frecuencia se llama opci-acih div. y cl valor i!e 5 / 4 cs 5 tfiv 4 = 1 . S i se !efierc a I U divisifin de punto Slotaiite. cntonccs 5 14 time u n b:lIor tic l.?.

Siipong;imvs :hora qiic nrl Ienpi jc de progrnii;;icii'w requiere eupri.sii~iics ií~i.~cI;:d;is

Page 288: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 AHALISIS SEHANTICO

5 / 2 / 2 . O (suponiendo la asociatividad por la izquierda de la división) es 1.25, rnientr el significado de 5/2/2 es Para describir estas semánticas se requieren tres a tos: un atributo booleano sintetizado isFloat, que indica si alguna parte de una exp tiene un valor de punto tlotante; un atributo heredado efype, con dos valores int y que da el tipo de cada subexpresión y que depende de isFlour; y finalmente, el val c lado de cada subexpresión, que depende del efype heredado. Esta situación también re re que se identifique la expresión de nivel superior (de nianera que sepamos que no ha subexpresiones por considerar). Hacemos esto aumentando la gramática con un sím inicial:

S 4 exp

Las ecuaciones de atributo se dan en la tabla 6.7. En las ecuaciones para la regla gra e.xp -+ un utilizamos Float (num.vul) para indicar una función que convierte el val tero num.vul en un valor de punto flotante. También empleamos la 1 para la divisi6n to flotante y div para la división entera.

Los atributos isFloat, eiype y val en este ejemplo se pueden calcular mediante dos sos sobre el árbol de análisis gramatical o el árbol sintáctico. El primer paso calcula el a

..:. 3 % buto sintetizado isFloat mediante un recorrido postorden. El segundo paso calcula tanto cly;d atributo heredado etype como el atributo sintetizado val en un recorrido combinado pre den/postorden. Dejamos la descripción de estos pasos y los correspondientes cálculos atributo para la expresión 5/2/2.0 para los ejercicios, además de la construcción del pseu cúdigo o código en C para efectuar los pasos sucesivos en el árbol sintáctico.

Tabla 6 1 Gramatica con atributos Regla gramatical Reglas sernánticas

para el ejemplo 6.14 S -i exp erp.e@pe =

if exp.isFloat thenfloat else int S.val = exp val

exp, -, rxpz / erp3 exp, .isFlout = exp2 .isFlout or exp, .rsFloat

rxp2 .e@pe = expl espe erp, rtype = expl etype exp, .val =

ii'exp, .e@pe = rnt then exp2 .val div exp, .val else expz .val /exp3 .val

exp -+ num e.xp.isFloat = false exp.val =

i f eqetype = tnt then num .val else Floarfnum .val )

exp -t num.num exp.isFloat = true exp.val = num. num .val

I

3 7. Esta regla no es la niisnia regla que la empleada en C. Por ejemplo, cl valor de 5 / 2 / 2 . O es 1.0 1

en C. no 1.25. 1 9

Page 289: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejemplo 6.15

Algoritmos para calculo de atr~butos 285

6.2.3 Atributos como parametros y valores devueltos Con frecuencia al calcular atributos tiene sentido utilizar paránictros y valores de iiinción de- vueltos para comunicar los valores de atributo en vez de alinacenarlos corno cnnipos en una estructura de registro de {íbol sintáctico. Esto es p~uticulmente cierto si niuchos de los valo- res de atributo son los misinos o se utilizan scílo de manera temporal para calcular otros va- lores de atributo. En este caso no tiene niucho sentido eniplear el espacio en el árbol sintácti- co para almacenar valores de atributo en cada nodo. [Jn procedimiento de recorrido recursivo simple que calcule atributos heredados en preorden y atributos sintetizados en postorden pue- de, de hecho, pmar los valores de atributos heredados como parámetms a llamadas recursivas de hijos y recibir valores de atributos sintetizados como valores devueltos de esas mismas llama- das. En capítulos anteriores ya se presrntiuon varios ejemplos de esto. En particular, el cilculo del valor sintetizado de una expresión aritmética se puede hacer mediante un procedimiento de análisis sintáctico recursivo que devuelve el valor de la expresión actual. De manera simi- lar, el árbol sintáctico como un atributo sintetizado debe él mismo ser calculado mediante un valor devuelto durante el análisis sintáctico. puesto que hasta que se haya c«nstrnid», no exis- te todavía estructura de datos en la cual pueda registrarse a sí mismo como un atributo.

En situaciones más complejas, como cuando debe ser devnelto más deun atributo sintetizado, puede ser necesario emplear una estrnctura de registro o unión como un valor devuelto, o puede dividirse el procedimiento recursivo en varios procedimientos para cubrir los diferentes casos. Ilustramos esto con un ejemplo.

Considere el procedimiento recursivo EvulWithBu,se del ejernplo 6.13. En este procedimiento el atributo base de un número se calcula sólo una vez y entonces se utiliza p m todos los cálcu- los subsiguientes del atributo vul. De manera semejante, el atributo vtrl de una parte del número se emplea sólo como algo tempord en el cálculo del valor del número completo. 'Tiene sentido convertir a base en un parámetro (como un atributo heredado) y a r'ul en un valor devuelto. El procedimiento modificado EvulWitftBuse queda conlo se presenta a continuación:

function EvdWithBuse ( T: treenode; buse: integer ): integer; var ternI), temp2: integer; begin

case clusenodo de T of num-buse:

rerrzp : = EvulWithBase ( hijo derecho de T ) ; return EvalCl/itlzBuse ( hijo izquierdo de T, temp );

num: ternp := EvcrlWithBase ( hijo irqctierdu de T, huse ); if hijo derecho de T no es ni1 then

tentp2 : = Evul WithBuse ( hijo derecho de T, huse ); if temp r f error and te1np2 J. error then

return ba.se*teinp + temp2 else return error;

else return temp; curlmse:

if hijo de T = o then return 8 else return 10;

digiro: if huse = 8 and hijo de T = 8 or 9 then return error else return riurnvcrl ( hijo de T );

end ease; end f~vtrlWithHtrse;

Page 290: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

EnP. 6 1 a n A u a nnhwrico

Naturalmente, esto funciona sólo porque el aiributo base y el atributo vul tienen el mi tipo de datos integer o "entero", ya que en un caso EvulWithBase devuelve el atributo (cuando el nodo del árbol de análisis gramatical es un nodo cczrhuse) y en los otros c EvdWithBase devuelve el atributo val. También es algo irregular que la primera Ilam a EvalWithBase (en el nodo del árbol de análisis gramatical num-hme raiz) deba tener valor base facilitado, aun cuando todavía no exista, y lo cual es ignorado posteriorme Por ejemplo, para iniciar el cálculo se tendría que hacer una llamada como

EvulWithBase(nodorraiz,O);

con un valor de base de prueba O. Por lo tanto, sena más racional distinguir tres casos: e so num-base, el caso carbuse y el caso num y dígito, así como escribir tres procedimie separados para estos tres casos. El pseudoc6digo aparecería eotonces como se muestr enseguida:

function EvalBusetlNutn ( R treenode ): integer; (* sólo llanzuilo en nodo rciiz *) begin

return EvalNum ( hijo izquierdo de T, EvalBuse(hijo derecho de 7') ); end EvalBasedNuin;

function EvalBase ( R treenode ): integer; (* sólo llanziido en nodo cahase *) hegin

if hijo de T = o then return 8 else return 10;

end EvalBase;

function EvalNum ( T: rreenode; base: integer ): integer; var tetnp, remp2: integer; hegin

case clu.senodo de T of nunz:

ternp := EvalCVithBuse ( hijo izquierdo de T, base ); if hijo derecho (le T no es ni1 (nulo) then

temp2 := EvcilI.VithBuse ('hijo derecho de T, base ); if temp i: error and femp2 J: error then

return base*temp + remp2 else return error;

else return renzp; rfígito:

if base = 8 and hijo de T = 8 or 9 then retnrn error else return nnrnval( hijo de T );

end ease; end EvdNum;

6.2.4 E l uso de estructuras de datos externas para almacenar valores de atributo

En aquellos casos en que los \alores de atributo no se prestan I'icilniente para el método de los pxhnctros y vitlorcs <levueltos (lo que es cierto cn particular cucindo los v;ilores (le airihuto tienen un;r eslrnctiira <ignilic;itiva y pticde ser necesaria e11 p~iiitos itrhiit-arios iiur:~i~te

Page 291: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Algoritmos para calculo de atributos 287

la txailucci<ín), totlavía puede no ser razonable almacenar valores de atributo en los nodos dcl árbol sintáctico. En tales casos las estructuras de datos tales como las tablas de búsque- da, gráficas y otras estructuras pueden ser útiles para obtener el componamieiito correcto y accesibilidad de los valores de atributo. La misina gramiitica con atributos se puede riiu- dificar para reflejar esta necesiclad reemplarartdo ecuaciones de :ttributo (que representan asignaciones de valores de atributo) mediante llamadas a procedimientos que representan ope- rxiones en las estructuras de (latos apropiadas que se emplea11 para Iniinterier los valores de atributo. Las reglas semánticas resultantes ya no representan entonces una gramática con :itributus, pero todavía son útiles en la descripción de la seniáiitica de los atributos. rnieiitras que la operación de los procedimieiitos se ve con claridad.

Considere el ejemplo anterior con el proceditnicnto Ev<iIWlhRttse empleantio parámetros y valores devueltos. Corno el atributo h~r.se, una vez que se establece, se mantiene fijo el tiern- po que dure el cálculo del valor, podemos utilizar una variable no local para alniacenar su valor, cn ver. de pasarlo como un parámetro en cada ocasión. (Si htrse no estuviera fijo, en u n proceso recursivo así esto sería riesgoso o incluso incorrecto.) De este modo, podemos alterar el pseud«cí>digo para EvcilWilhBose de La niünera siguiente:

function EiwlWithB<i.se ( T: treenode 1: iizteger; var tenzp, tetnp2: Nfteger; begin

case clasenoiio de T uf num-bme:

,SetBrise ( hijo derecho de T ); return EvdWithBme ( hijo iryi~ierilo de T 1;

11 llm: r m p : = E d WithBnse ( htjo izquierdo de T ); if hijo clerecho de Tizo e.s ni) (rudo) then

temp2 := EvdWithBase ( hijo derecho de T ); if ternp i, error and terrzpZ f error then

return bn,se*tetttp + temp2 else return error;

else return temp; dígito:

if base = 8 and hijo (le T = 8 or 9 then return error else return I ~ U I I ~ V L I I ( hijo de T ) ;

end case; end EvtdWitizBuse;

procedure SetB~ise í T: rr twoik ); begin

if hqo (le T = o ihei1 hir te : = 8 else hare : - 10;

end S e t R t ~ ~ r ;

tlquí sepafiimos el proceso de ;isignaci<ín a la variable hri.re no local e11 cl procedimiento SvrBcm, el cual sólo es llamado en u n nudo cvri>osc,. El resto del c6digo de I<vcilWiiltBtr.~i. ci1toiiccs se refiere simple~ric~ite ;t hase de manera directa, sin pasarlo como u n p;irirnetr«.

'TainhiCn podenios cümhi:~r las I-cgl;is sernrinticni pitrn reflejar el uso ,le I;I wrii~ble n o Ii1c:il Í v w . 131 esre c;tso las reg1:rs tcntlríxi un aspecto :ilgo parecido ;I lo \igi~icntc. ilonile uti1i~;iinos la ;isignxiríri p:tra iriiiic;ir de iiintlera explícita el cst;ihlccirriic~iio de la v:rri;lhlc ¡ l o 1,)caI h!i,W

Page 292: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

EAP. 6 1 ANALISIS SENANTICO

Regla gramatical Reglas semCInticas

iwm-buse -+ nurn-basc.i.111 = riirm.vu1 mtm curhuse

curhase i o huse := 8 curbase -r d base := 10 nurn~ .-, numi dígiro num, .val =

iP dfgitu. val = errur or numz. val = error then error else ntrmz. v d * buse + digiro. vid

etc. etc.

Ahora base ya no es un atributo en e1 sentido en que lo henios utilizado hasta ahora, reglas semánticas ya no fonnan una gramática con atributos. No obstante, si se sab base es una variable con Las propiedades adecuadas, entonces estas reglas todavía de de manera adecuada la semántica de un num-base para el escritor del compilador.

Uno de los ejemplos principales de una estructura de datos externas al árbol sintá es la tabla de símbolos, que almacena atribntos asociados con procedimientos, variab constantes declaradas en un programa. tJna tabla de símbolos es una estructura de dat diccionario con operaciones tales como inserción, búsqueda y eliminación. En la sig sección analizaremos cuestiones en torno a la tabla de símbolos en lenguajes de progr ci6n típicos. Nos contentaremos en esta sección con el siguiente ejemplo simple.

Ejemplo 6.17 Considere la gramática con atributos de las declaraciones simples de la tabla 6.3 (pá 266) y el procedimiento de evaluación de atributo para esta gramática con atributos dad el ejemplo 6.12 (página 279). Por lo general, la informacidn en las declaraciones se inse en una tabla de símbolos utilizando los identificadores declarados como claves y almac dos allí para una búsqueda posterior durante la traducción de otras partes del programa. consiguiente, suponemos para esta gramática con atributos que existe una tabla de símb que almacenará el nombre del identificador junto con su tipo de datos declarado, y insertarán pares de nombre-tipo de datos en la tabla de símbolos mediante un proced~ to de inserción denominado insert, que se declara como se muestra a continuación:

procedure insert ( name: string; dtype: typekind);

De este modo, en vez de almacenar el tipo de datos de cada variable en el árbol sintácti lo insertamos en la tabla de símbolos utilizando este procedimiento. También, puesto cada declaración tiene sólo un tipo asociado, pdemos utilizar una variable no local par macenar el dtype constante de cada declaración durante el procesamiento. Las reglas semá ticas resultantes son las siguientes:

Regla gramatical Reglas sernánticas

(lec1 - jype vur-lis1 lypa -. i n t dlype = Niteger t y )e + f loat útype = reul wir-lirll * i d , vttr-lisb insert(id .rrunre, d@[)ef vur-list-1 i d inserff id .riumr, rltype)

Page 293: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Algoritmos para dlculo de atributos 289

En las llamadas a irrsert hemos utilizado i d . riurne para hacer referencia a la cadena de iden- tificador, que suponemos es calculada por el analizador léxico o analizador sintáctico. Estas reglas semánticas son m u y diferentes de la gramática con atributos correspondiente; la [-e- gla gramatical para una úecl no tiene, de hecho, ninguna regla semántica. Las dependencias no están expresadas tan claramente, aunque salta a la vista que las reglas de type deben procesarse antes que las reglas asociadas de vur-lisl, debido a que las llamadas a inserr de- penden de dtype, lo que está establecido en las reglas de type.

El pseudocódigo coirespondiente a un procedimiento de evaluación de atributo EvuETvpe es como se presenta a continuación (compare esto con el c6digo de la página 280):

procedure EvalType ( T: treenode 1; begin

case clusenodo de T of decl:

EvalType ( tipo hijo de T ); EvalTjpe ( var-list hijo de T );

type: if hijo de T = i n t then dtype := uiteger else dtype : = real;

var-list: insert(nombre del primer hljo de T, dtype) if tercer hiJo de T no es nrl then

EvalType ( tercer hijo de T ); end case;

end EvalTvpe;

6.2.5 El cálculo de atributos durante el análisis sintáctico Una cuestión que se presenta de manera natural es hasta qué punto los atributos se pueden calcular al mismo tiempo que la etapa de análisis sintáctico, sin esperar a realizar pasos adi- cionales sobre el código fuente por medio de recorridos recursivos del árbol sintáctico. Es- to es particularmente importante respecto al árbol sintáctico mismo, el cual es un atributo sintetizado que debe ser construido durante el análisis sintáctico, si se va a emplear para análisis semántica adicional. Históricamente, la posibilidad de calcular todos los atributos durante el análisis sintáctico era incluso de mayor interés, ya que se había puesto mucho én- fasis en la capacidad de un compilador para realizar la traducción en un paso. En la actuali- dad esto es menos importante, asi que no proporcionaremos un análisis exhaustivo de todas las técnicas especiales que se han desarrollado. Sin embargo, vale la pena ofrecer una pers- pectiva básica de sus ideas y requerimientos.

Saber cuáles atributos se pueden calcular con éxito durante un análisis sintáctico depen- de en gran medida de la potencia y propiedades del método de análisis sintáctico empleado. Una restricción importante es determinada por el hecho de que todos los métodos de análi- sis sintáctico principales procesan el programa de entrada de izquierda a derecha (esto es lo que significa la primera L en las técnicas de análisis sintáctico LL y LR que se estudiaron en los dos capítulos anteriores). Esto es equivalente al requerimiento de que los atributos puedan evaluarse mediante un recorrido del árbol de análisis gramatical de izquierda a de- recha. Para los atributos sintetizados esto no es una restricción, porque los hijos de un nodo pueden procesarse en orden arbitrario. en particular de izquierda a derecha. Pero, para los atributos heredados, esto significa que puede no haber dependencias "hacia atrás" cn la grá- 6ca de dependencia (las dependencias apuntan de derecha a izquierda en el árbol de análi- sis gramatical). Por cjemplo, la gramática con atributos del ejemplo 6.3 (pigina 266) viola

Page 294: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 ANALISIS S E M ~ T I C O

esta propiedad, porque la base de un nurn-base tiene su base dada por el sufijo o o d, atributo vcd no se puede calcular hasta que el sufijo al final de la cadena se ve y se proc Las gra~náticas con atributos que satisfacen esta propiedad se llaman de atrihuto L (por 1 del término izquierda a derecha en inglés) y se detine de la manera siguiente.

Una gramática con atrihutos para los atributos a , , . . . , uk es de atrihuto 1, si, para ca atributo heredado u,, y para cada regla gramatical

x,-+ x , x , . . . x,,

1 as _ txuaciones .. asociadas para a, son todas de la forma

Xi.oj =,fl i iXO.al, . . . , Xt>ak, X,.u,, . . . , Xl.uk,. . . , Xi-,.al, . . . , X,-I .LI~)

Es decir, el valor de (9 para X, sólo puede depender de los atrihutos de los símbolos X,,, . . X, , que se presentan a la izquierda de Xi en la regla gramatical.

Como un caso especial, ya advertimos que tina gramática con atributos S es de at buto L.

Dada una gramática con atributos L en la que los atrihutos heredados no dependa de los sintetizados. un analizador sintáctico descendente recursivo puede evaluar todos 1 atributos convirtiendo los heredados en parámetros y los sintetizados en valores devuelt de la manera antes descrita. Por desgracia, los analizadores LR, ctnno un analizador sint rico LALR(1) generado por Yacc, son adecuados para controlar principalmente atribut sintetizados. La razón reside, irónicamente, en la mayor potencia de los analizadores L respecto a los analizadores LL. Los atributos sóIo se pueden calcular cuando la regla gr rnatical a utilizarse en una derivación se vuelve conocida, porque sólo entonces las ecu ciones son determinadas por el cálculo de atributo. Sin embargo, los analizadores posponen la decisiítn de qué regla gramatical utilizar en una derivación hasta que el lado recho de una regla gramatical está completamente formado. Esto hace difícil que los atrib tos iteredados estén disponibles, a menos que sus propiedades permanezcan fijas para toda las posibles selecciones del lado derecho. Expondremos brevemente el uso de la pila de an lisis sintáctico para calcular los atributos en los casos más comunes, con aplicaciones p Yacc. Las fuentes para técnicas más complejas se mencionan en la sección de notas y re rencias.

CBfcuIo de atributos rintetizados durante el análisis sintáctico LR Éste es el caso más sencillo para tin analizador LR. Un analizador LR generalmente tendrá una pila de valores en la que se al- macenan los atributos sintetizados (quizá como tiniones o esuucturas? si hay más de un atri-

' buto para cada símbolo gramatical). La pila de valores será manipulada en paralelo con la pila de análisis sintáctico, con los nuevos valores que se están calctilando de acuerdo con las ecuaciones de atributo cada vez que se presenta un desplazamiento « una reduccilín en la pi- la de anilisis sintáctico. Ilustrarnos esto en la tabla 6.8 para la gramática con :itrib~~tos de la tabla 6.5 (página 269)). que es la versión ambigua de la grainatica de eupresicín aritrriética 4inpls. Por simplicidad utilizamos notaci6n abreviada para la graitiitica e ignor;imvs al- gun<~s tle l o s tietalles de u11 algoritmo de :rn:ílisis si~itictico 1.R en la i:ibla. Eri pxiicular. no

Page 295: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Algorrtn~os para cálculo de atributos 29 1

se indican los núnieros de csiado. i io se r i~i icstra e l sínibolti i i i icinl aiirnentado y no se ex- presan las reglas de elirrriiiiiciii~i de airihigüedrtd iiiiplícitas. L a t;ihla coniiciie dos nuevas co- I~irnrias adeiriás de las acciones habituales de análisis sir~táctico: la pi la tic valores y ac-

ciones scniánticas. Las accictnes semáiiticiis indican cdrrio oclirren los cálculos en la p i h de valores a niedida cpe se presentan las rctliicciones en la pila de awílisis sintáctico. ([..os dcsplaminient»s se ven como la inserción de valores de token tanto en la pila de an5lisis sintáctico coino en la pila de valores, aunque esto puede diferir en a r ia l i~~dores sintácticos individuales.)

Conio ejernplo de una accidiiseinántica cotisidere el paso 10 de la tabla 6.8. L a pila de valores contiene los valores enteros 12 y S. separados por e l token t. con el 5 en la pasie superior de la pila. 1.a ;icci6ii del análisis sintáctico es reducir rnediante E .--+ E t E, y lii accicín seináiitica cimeqx)ndiente de I;i cuhla 6.5 es calcular de acuerdo con l a ecuaci6n El .~.<i/ = & . c d + 5, . v d . Las ;~cciones correspoiidientes en la pila de valores que realiza cl analizador sin(6ctico son las que se muestran a coriiinuacidn (en psetidocódigo):

pop 13 ( obtieite E , .id de l a pila de valores ) [~op ( descarta e l toketi + ] pop 12 ( obtiene E, .vid de la p i la de valores ) t l = rZ -t 13 ( suma ] push rl ( inserta e l resultado de iiuevo eii la pila de valores /

El i Yacc la situacií,n representada por la recluccidn e n e l paso 10 se escrihiríü como la regla

Aquí las pseudov;iriables Si representan posiciones en el lado derecho de la regla qiie se rstri retlociendo y se convierten a posiciones en la pila de valores contando hacia atris a par- tir de la derecha. De este rriodo. $3. que corresporide al E más a Iri derecha, se encontrar5 en la parte superior o tope de la pila, inientrtis cpe $1 se hallará dos posiciones por dehi jo del tope.

Tdbla 6.8 Aaioner remanticas y de análisis sintáctico para la expresión 3 *4+5 durante un análisis rintktico LR I

S 3 4 5 6

7 S '1

10

I l

Pila de Acción análisis de analisis Pila de Acción sintáctico Entrada sintactico valores sernantica

3*4+5 'S dcylaz:i~nienro $

$ n *4+5 $ rectuccih de E --r n $ n E . vol = n . ~:d $ E *4t5 $ dzsplazntnie~ito 'S 3 <SE* 4 t5 $ desplar~rniento s i * S E * n +5 $ reducción <le E -- n $ 3 * n 1:' .I.<II -= n . ~ r i i % E * / < t5 $ redtrcci6n de $ 3 * 4 f I =

E .-- IJ * E E 2 . 1 , d * E i .l.d RE +5 'S dcs~iln~aniieiito S 17 $ E + 5 % ~le,spl~~:i t~i ieni i~ ', I Z + S E + n S rzdi~cciiín Jc E -a n '; 17 + n I<.i~,i/ -: n r i i l

C I + l i '3 r.cclirccii>n de S I ? + ; i<, . ~ , i i l

1; --+ f',' + IC- l ' , d + l..'< . l , d

7 F i 5' 17

Page 296: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

U?. 6 1 A N ~ L I S I S S E M ~ N T I C O

Herencia de un atributo sintetizado previamente cakculado durante el análisis rintáctfco IR Debido a l trategia de evaluación de izquierda a derecha del anrüisis sintáctico LR, una acción as da a un no terminal en el lado derecho de una regla puede utilizar atributos sintetizados los símbolos a la izquierda del mismo en la regla, puesto que estos valores ya se insert en la pila de valores. Para ilustrar esto brevemente considere la opción de producción A. B C, y suponga que C tiene un atributo heredado i que depende de alguna manera del a buto sintetizado s de B.: C.i = f (5,s). El valor de C.i puede almacenarse en una vari antes del reconocimiento de C introduciendo una producción E entre B y C que program almacenamiento de la parte superior de la pila de valores:

Regla gramatical Regias semánticas

A - B D C B + . . . calcule B.s ) D - r c : saved-i = f (vulstack[top]) C - 3 . . . [ ahora está disponible ,yaved-i )

En Yacc este proceso se vuelve aún más fácil, ya que la producción E no necesita ser in ducida explícitamente. En su lugar, la acción de almacenamiento del atributo ca~cuiado escribe simplemente en el lugar de la regla donde se va a registrar:

A : B { saved-i = f($l); > C ;

(Aqui la pseuciovariable $1 se refiere al valor de B, que está en la parte superior de la p cuando se ejecuta la acción.) Tales acciones incrustadas en Yacc se estudiaron en la secci 5.5.6 del capítulo anterior.

Una alternativa para esta estrategia está disponible cuando siempre puede predecirse posición de un atributo sintetizado previamente calculado en la pila de'valores. En es caso, el valor no necesita ser copiado en una variable, sino que se puede acceder a él dire tamente en la pila de valores. Considere, por ejemplo, la siguiente gramática con atributo L con un atributo dtype heredado:

Regla gramatical Reglas semhnticas

decl -t type vur-list var-list.dtype = .ipe.&pe type -+ int type.dtype = integer type -3 f loat type.drype = real var-listl -+ var-lista , i d insert(Ld .narne, var-listl.&ype)

var-list2.dtype = var-list&pe iw-lisr + i d insert(id .name, var-list.dr/pe)

En este caso el atributo clrype, que es un atributo sintetizado para el no terminal rype, se pue- de calcular en la pila de valores justo antes de que se reconozca la primera vw-l is t . Enton- ces, cuando se reduce cada regla para vnr-lis?, &pe se puede encontrar en una posición fi- ja en la pila de valores contando hacia atrás desde la parte superior o tope: cuando se reduce vw-iirt -t id. dt-ye está justo debajo de la parte superior de la pila. mientras que cuando se reduce iar-lixt, -3 v( i r - l i .~ t~ , id, dvpe está tres posiciones debajo del tope de la pila. Po- demos implementiw esto en un rinalizador siiitictico LR mediante la eliminación de las dos

Page 297: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Teorema

Algoritmos para cálculo de atributos 293

reglas de copia para +pr en la gramática con atributos anterior y entrando a la pila de valores de manera directa:

Regla gramatical Reglas semánticas

drc1-r type vur-1i.s~ npe -. i n t type.&pe = integer Qpe -) f loat tvpt..ilIype = rtul iur-iist, + var-list2 , i d insert(id .narnr, ~ ~ ~ i ~ r l l l c k [ l o p - i ~ ) w~r-li,~C + i d insert(id .nome. r~ul.rtack~op- 1 1 )

(Advierta que, como vur-lisr no tiene atributo sintetizado dtvpe, el analizador sintáctico debe insertar un valor de prueba en la pila para mantener la ubicación apropiada en ésta.)

Existen varios problemas con este métdo. En primer.lugar, requiere que el programador tenga acceso de manera directa a la pila de valores durante un análisis sintáctico, y esto pue- de ser peligroso en analizadores sintácticos generados de manera automática. Por ejemplo, Yacc no tiene una convención de pseudovariable para tener acceso a la pila de valores bajo la regla actual que se está reconociendo, como se requeriría con el método anterior. De este modo, para implementar un esquema de esta clase en Yacc, se tendría que escribir código especial para hacerlo. El segundo problema es que esta técnica sólo funciona si la posición del atributo previamente calculado es predecible a partir de la gramática. Por ejemplo, si hu- biéramos escrito la gramática de la declaración anterior de manera que vur-lkr fuera recur- siva por la derecha (como hicimos en el ejemplo 6.17), entonces un número arbitrario de i d podría estar en la pila, y no se conocería la posición de dype en la pila.

Con mucho, la mejor técnica para tratar con atributos heredados en el análisis sintácti- co LR es utilizar estructuras de datos externas, tal como una tabla de símbolos o variables no locales, con el fin de mantener los valores de atributos heredados, y de agregar produc- ciones E (o acciones incrustadss como en Yacc) para permitir que se presenten cambios a estas estructuras de d.dtos en ios momentos apropiados. Por ejemplo, una solución para el problema de dtype que se acaba de comentar puede encontrarse en el anjlisis de las accio- nes incrustadas en Yacc (sección 5.5.6).

Sin embargo, deberíamos estar conscientes de que incluso este último métocio no carece de riesgos: la adición de producciones E a una gramática puede agregar conflictos de análi- sis sintáctico, de manera que una gramática LALR(1) se puede convertir en una gramática no LR(k) para cualquier k (véase el ejercicio 6.15 y la sección de notas y referencias). En si- tuaciones prácticas, no obstante, esto rara vez sucede.

6.2.6 l a dependencia del cálculo de atributos respecto a la sintaxis Como el tema final en esta sección, vale la pena advertir que las propiedades de los atri- butos dependen en gran parte de la estructura de la gramática. Puede pasar que las modi- ficaciones a la gramática que no cambian las cadenas legales del lenguaje provoquen yue el cálculo de los atributos se vuelva más simple o más complejo. En realidad, tenemos el siguiente:

(De Knuth [l9681). Dada una grüniáticü con atributos, todos los atributos heredados se pueden cambiar en atributos sintetiz;id«s mediante la modificación adecuada de ILI pramáti- c;i. sin cambiar el lenguaje de la misma.

<**:m-

Page 298: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejemplo 6.18

F ~ g u r a 6 11 Arbol de análiws gramatical para la cadena fioat x, y que muestra el atributo dtype como se eípecifico med~ante la gramát~ca con atr~butor del ejemplo 6.18

UP. 6 1 ANALISIS SEMANTICO

Daremos un ejemplo de cómo un mibuto heredado se pude convertir en un atributo tizado mediante modificación de la gramática.

Vuelva a considerar La gramática de declaraciones simples de los ejemplos anteriores

decl-+ type iur-list type -+ i n t 1 float vur-list -+ id , vnr-list / id

El atributo clvpe de la gramática con atributos de la tabla 6.3 es heredado. Sin embarg volvenios a escribir la gramática de la manera siguiente,

iiecl -+ vur-list id var-list -+ vqr-list i d , 1 type ype -+ i n t / f loat

entonces se aceptan las mismas cadenas, pero el atributo dtype ahora se vuelve sintetiza de acuerdo con la siguiente gramática con atributos:

Regla gramatical Reglas semanticas

<lec1 + viir-lis! id id .&pe = iw-listdtype iur-listi + var-list2 id , id .dtype = vur-li.stt .dtype

vur-¡¡.S!, .dtype = vur-listz .dtype sur-lis1 -. rype vur-li.st.iltyp = 1 p e . d ~ p e type -+ int typedope = integer tjpe -+ f l oa t f)pe.dtype = real

Ilustraremos cómo este cambio en la gramática afecta al árbol de análisis gramatical y el c lo del atributo dtype en la tigura 6.11, que exhibe el &bol de análisis gramatical para la c f loat x, y, junto con las dependencias y valores de atributo. Las dependencias de los valores i d .d@pe respecto a los valores del padre o hermano se trazan como líneas d' tinuas en la figura. Aunque estas dependencias parecen ser violaciones a la afirmaci que no hay ztributos heredados en esta gramática con atributos, de hecho estas depe cias son siempre hacia hojas en el árbol de análisis sintáctico (es decir, no recursivo) y den conseguirse mediante operaciones en los nodos padre apropiados. De este modo, dependencias no se ven como herencias.

decl _--a .

/-

-. . . /a-

. . . .. r - i r dtype = rrnl id dtype = red

(Y) , , , , /

, 4

! , , . - , - - - _ _ _ _ - - , ... , ..

f loat

Page 299: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

De hecho, d teorema establecido es menos útil de lo que puede parecer. Modificar la gra- mática con el fin de cambiar los atributos heredados a atnbutas sintetizados con frecuencia hace a la granática y las reglas semántieas mucho m& complejas y difinles de comprender. Por lo tanto, no es una manera recomendable pata tratar con los problemas de cálculo de atributos heredados. Por otra parte, si un cálculo de atributo parece anormalmente difícil, puede ser porque Ia gramática está definida de ada para su c&ulo, y un cambio en la gramática puede valer la pena,

organizaciones diferentes: así como la investigación de buenas estrategias de orgmización, es uno de los temas principales de un curso de estructura de datos. Por lo tanto, en este texto

8. Antes que destruir esta iiiforrnación, es más prohabk que una operación de elimin<rciún (deirte) la retire de la vista. ya sea alinacenándolü en otra p:irte: o marcándola ct>ino inactiva.

Page 300: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 / ANALISIS SEMANTICO

no trataremos este terna de manera detallada, sino que remitiremos al lector que desee inhrmación a las fuentes que se mencionan en la sección de notas y referencias al fin este capítulo. Sin embargo, proporcionaremos aquí una perspectiva general de las estn ras de datos más útiles para estas tablas en la construcción de compiladores.

Las implementaciones típicas de estructuras de diccion'uio incluyen listas lineales, sas estructuras de &bol de búsqueda (árboles de búsqueda binarios, árboles AVL y irbo así como tablas de dispersión (bUsqueda de dirección). Las listas lineales son una bu tructura de datos básica que puede proporcionar implententaciones directas y fáciles de 1 operaciones hiisicas, con una t)perdción de inserri(jn en tiempo constante (insertando sie al frente o la parte posterior) y operaciones de b~ísrprrla y eliminc~cicín que son de tie lineal en las dimensiones de la lista. Esto puede ser suficientemente bueno para una itnple ración de compilador en la que el interés principal no sea la velocidad de compilacicín, ta mo un compilxior prototipo o experimental, o un intérprete para progranlas muy peclu. Las estructuras de árbol de búsqueda son un poco menos útiles para la tabla de sínibola parte porque no proporcionan la mejor eficiencia del caso, pero también debido jidad de la operación de elirninlrci<ín. La tabla de dispersión proporciona a menudo la selección pan implementar la tabla de símbolos, ya que las tres operaciones se pueden zar en un tiempo casi constante, y se utiliza niuy frecuentemente en la práctica. Por lo t comentaremos e1 caso de la tabla de dispersión con algo más de detalle.

Una tabla de dispersión es un arreglo de entradas. denominadas cubetas, indiz mediante un-intervalo entero, generalmente desde el O hasta el tamaño de la tabla m uno. Una función de dispersión convierte la clave de búsqueda (en este caso, el nombre identificadoi; compuesto de una cadena de caracteres) en un valor entero de dispersión e intervalo del índice, y el elcntento correspondiente a la clave de búsqueda se almacena en cubeta en este índice. Se debe tener cuidado p:ira garantizar que la función de dispersic distribuya los índices clave tan uniformemente como sea posible sobre todo el intervalo índices, porque las colisiones de dispersión (donde se mapean dos claves hacia el mismo I

dice mediante la función de dispersión) provocan una degradación del rendimiento en I operaciones de bií.squedu y eliminación. La función de dispersión también necesita funcio nar en un tiempo constante, o por lo menos en un tiempo que sea lineal en Las dimension de la clave (esto equivale a tiempo constante si existe una cota superior en las dimensiones la clave), Examinaremos funciones de dispersión en breve.

Una cuesti6n importante es cómo una tabla de dispersión se ocupa de las colisiones (e se conoce como resoluci6n de cofisión). Un método asigna sólo el espacio suficiente p un solo elemento en cada cubeta y resuelve las colisiones mediante la inserción de nuev elementos en cubetas sucesivas (esto en ocasiones se denomina direccionarniento abiert En este caso el contenido de la tabla de dispersión está limitado por el taniaño del arre utilizado para la tabla y, a medida que el arreglo se va llenando, las colisiones se vuelv más y mis frecuentes; esto causa una degradación importante del rendimiento. Un' pro ma adicional con este método, a1 menos para la construcción del compilacior, es que es fícil iinplementar una operación de eliminacitín, y estas eliminaciones no mejoran el r dimierito oosterior de la tabla.

Quizás el mcjor esquema para la construcciitn de compiladores es la alternativa al dire cioitaniiento abierto, denorninacla encadenamiento por separado. En este método cada cub ta es en realidad una lista lineal, y las colisiones se resuelven insertando el nuevo elemento en la lista de la cubeta. La figura 6.13 muestra un ejemplo simple de este esquema, con una ta- bla de dispersión de tamaño 5 (irrealmente pequeño, para propósitos de dernostración). En esa tabla partimos del supuesto de que se han insertado cuatro identificadores (i, j, size y temp) y que size ("tamaño") y j tienen el rnisrno valor de dispersión !a saber, 1). En la ilustración mostranios size antes que j en el número 1 de la lista de la cubeta; el ordcn de loi elemcritos en la lista depende del orden de insercih y de cómo se mantenga la lista. Un ril6totlo común es insertür siempre al principio de la lista, de marizra que ~isando cste mito- <lo size se Iiahría insci-tado después de j.

Page 301: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

La tabla de rimbolos

La Figura 6.12 también muestra las listas en cada cubeta implementadas conio listas li- gadas (se utilizan puntos sólidos para indicar apuntadores nulos). Éstos se pueden asignar utilizando la asignacicín dinámica de apuntadores del lenguaje de implementación del com- piiador. o se pueden asignar a mano a partir de un arreglo de espacio dentro del compilador niismo.

Una cuestión que debe contestar el escritor de compiladores es qué tan grande hacer el arreglo inicial de cubetas. Por lo regular, este tamaño se fijará durante el tiempo de cons- trucción del compilador.9 Los tamaños típicos abarcan desde algunos cientos hasta más de un millar. Si se utiliza la asignación dinámica para las entradas reales, incluso arreglos pe- queños permitirán la compilación de programas muy grandes, con el costo de un aumento en el tiempo de compilación. En cualquier caso el tan~año real del arreglo de cubetas debe- ría elegirse como un número primo, ya que esto hace que las funciones de dispersión típi- cas se comporten mejor. Por ejemplo, si se desea iin arreglo de cubetas de tamaño 200, se debería seleccionar 21 1 como el tamaño desde el 200 mismo (21 1 es el número primo más pequeño mayor que 200).

Volvamos ahora a una descripción de las funciones de dispersión comunes. Una funcirín de dispersión, para su uso en una implementdción de tabla de sirnbolos, convierte una cade- na de caracteres (el nombre del identifieador) en un entero en el intervalo O...size - l. Por lo común, esto se hace mediante un proceso de tres pasos. En primer lugar, cada carácter en la cadena de caracteres se convierte a un entero no negativo. Luego, estos enteros se combi- nan de alguna manera para formar un entero simple. Al final, el entero restiltante se escala al intervalo O..size - 1.

La conversión de cada carácter a un entero no negativo generalmente se hace utilizan- do el mecanismo de conversión integrado del lenguaje de implementación del compilador. Por ejemplo, la función ord de Pascal convierte un carácter en un entero, por lo general su valor ASCII. De manera similar, el lenguaje C convertirá automáticamente un carácter a un entero si se utiliza en una expresión aritmética o se asigna a una variable entera.

Escalar un entero no negativo para que caiga en el intervalo O..size - 1 también se hace fácilmente utilizando la función módulo de matemáticas, la cual devuelve el residuo divi- diendo size entre el número. Esa función se llama mod en Pascal y % en C. Cuando se utili- za este método, es importante que size sea un número primo. De otro modo, un conjunto de enteros distribuido al azar puede no tener sus valores escalados distribuidos aieatoriarnente en el intervalo O...~ize - 1.

Resta al implementador de la tabla de dispersión elegir un método para combinar los di- ferentes valores enteros de los caracteres en un solo entero no negativo. Un método simple es ignorar muchos de los caracteres y sumar sólo los valores de unos cuantos caracteres prí- meros, o el primero, el medio y el último. Esto es inadecuado para un cornpilador, porque los programadores tienden a asignar nombres de variable en grupos, tal como templ,

hdices Cubetas Listas de elementos

temp2 0 mltmp, m2tmg, y así sucesivamente, así que este niétodo ocasionará frecuentes

O I

muestra la resolución

3 d

9. Existen métodos para incremenrar e l t;ini;iño "siie" del xrcg lo (y tnodificar la iuncitin dt: dis- prs ió i i ) :il viielo s i la tabla dc i lkpcri i6n crcce iIciiiasi;ido. pero s11n coinplejos y mr;i i .ei i e i i t i l i ~ 3 1 i .

---.-. - - - e -

-1 / o + aize 1 + m - temg / O -

o

Page 302: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 6.13 funtlón de dispersión para una tabla de simbolos

CAP. 6 1 ANÁLISIS SEKÁNTICO

colisiones entre tales nombres. Por consiguiente, el método elegido debería involucrar los caracteres en cada nombre. Otro método popular pero inadecuado es sumar simpl te los valores de todos los caracteres. Si se usa este método, todas las pennutaciones mismos caracteres, tal como temgx y xtemg, provocwín colisiones.

Una buena solución a estos problemas es emplear repetidamente un número cofist como factor multiplicativo cnando se sume al valor del siguiente carácter. De modo c, es el valor numérico del i-ésimo carkter, y tii es el valor de dispersión parcial cal en el i-ésinio paso, entonces las h, se calculan de acuerdo con Las fórmulas recursivas y h,¡ = alzi + ci, con el valor de dispersión Final h calculado como h = h, mod .vize, de ri es el número de caracteres en el nombre donde se está aplicando la dispersión. E equivale a la fórmula

La selección de a en esa fórmula tiene, por supuesto, un efecto importante sobre 1 lida. Una selección razonable para a es una potencia de dos, tal como 16 O 128, de man que la multiplicación se pueda realizar como un desplazamiento. Efectivamente, selec nando u = 128 se tiene el efecto de visualizar la cadena de caracteres como un número base 128, suponiendo que todos los valores de carácter ci sean menores que 128 (verda para los caracteres ASCII). Otras posibilidades que se mencionan en la literatura son va números primos (véase la sección de notas y referencias).

En ocasiones el desbordantiento puede ser un problema en la fórmula para h, espe mente para valores grandes de a en máquinas con enteros de dos bytes. Si se utilizan val res enteros para los cálculos, el desbordamiento puede dar como resultado valores ne (en representación de complemento a dos), lo que causará errores de ejecución. En ese caso posible obtener el mismo efecto realizando la operación mod en el bucle de sumatoria. la figura 6.13 se proporciona una muestra de código C para una función de dispersión h qu efectúa esto utilizando la fórmula anterior y un valor de 16 para u (un desplazamiento de de 3, puesto que 16 = 24).

63.2 Declaraciones El comportamiento de una tabla de símbolos depende inuchb de las propiedades de las decl:iraciones del lenguaje que se está traducietido. Por ejemplo, la manera en que las ope- raciones de ir?.wrcitjn y elinrintrciún nctitan sobre la tabla de símbolos. cuando se necesita

Page 303: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

l a tabla de rimbolos 299

llarniir a esas operaciones, y cuáles atributos se insertan en la tabla, varía :iiripliatnente de lenguaje a lenguaje. lncluso el tiempo que toma el proceso de tratlucci(>nie,jecuci<ín, cuando sc puede construir la rab\:i de síniholos, y la extcnsilín que necesita tener la tahla sírnholos para existir. pueden diferir hastante de un Ieiiguaje a otro. En esta seccii,n indicarenios al- g u ~ s ú e las cuestiones de lenguaje involucradrts en tieclaraciones que afectan el coinporta- iniento y la implemcnt:~cii>ii de la tabla de símbolos.

Existen cuatro cl:ises básicas de declaraciones que se presentan con ('rccucncia en los lcngrmjes de programacih: declaraciones de constantes, tleclaraciones de tipo, declwacio- ries de variable y declaraciones de proceditiiiento/funcicín.

l a s declaraciones de constantes incluyen las declaraciones cons t de C. tales como

const int SIZE = 199;

(C también tiene u11 mecanismo #def ine para la creaci6n de constantes, pero k t e , tnás que por et propio coinpilador, es iiianqjado meniante un preprocesador.)

1.2s declaraciones de tipo incluyen las declaraciones de tipo de Pascal, tales coii io

tYPe Table = array [l. .SIZEI o£ Entry;

y declaraciorie~ de C s t r u c t y union tales como

Struct Entry

( char * name; int count;

struct Entry * next; };

las cuales definen un tipo de estructura con nombre Entry. C también tiene un niecrinisrrio typedef pdra declamci6n de norrihres que son alias para tipos

typede£ struct Entry * EntryPtr;

Las declaraciones de variables son la h r m a más coinún de dec larac ih e inclrtyen declaraciones en FORTRAN tales como

integer a,b(lOO)

y declaraciones de C corno

int a,b[1001;

Finaltnente, existen declaraciones de proeedimientolfunción, tales coii io la t'uncióti de C definida en la figtira 6.13. Éstas en realidad son s6lo una declaraci6n constante de tipo promdimie~iio/f~incií,n, pero por lo regtilar se destaciin en definiciones del lenguaje debido a sti iiaturaleza especial. Estas dec1ar;iciones son explícitas, ya que se util iza tina construc- c i h del lenguaje especial para declaraciones. También es posible tener declaraciones implícitas, en las cu;ilcs las declaraciones se unen a instrucciones ejecutables sin rrienci6ii explícita. FORTRAN y BRSIC, por ejemplo, perrriiten que las v:iriahles se ut i l icen sin i l e c l n r ~ i t i n explícita. En tales declaraciones implícitas se u t i l i ~ a r i corivenciones para proporcionar la inl'orniíiciúii que de otro rnodo sería diida por una dcclarltcicín esplícito. F1)RTRAN. por ejemplo. tieiic la conveiicicí~i de tipo de que las vsriahlcs que cornicticcii con el iritervalit tic Ietr;rs <Irle va de la l 3 la N sean :iutorniticarneritc enteros s i sc ut i l i~: in $irr una d c c l ; ~ r x i h i c~p1ícit:i. iriieritras que tocI:th las otras son ~ii11o1ri5iiciit11~rlte r . cdc~ . 1-:E

Page 304: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 ANALISIS SEHANTICO

declaraciones implícitas también se pueden denominar declaraciones por el uso, po primer uso de una variable que no esté declarada de manera explícita se puede const como si contuviera implícitamente su declaración.

Con frecuencia es más fácil utilizar una tabla de símbolos para mantener los no todas las diferentes clases de declaraciones, particularmente cuando el lenguaje proh el mismo nombre en distintos tipos de declaraciones. De cuando en cuando es más plear una tabla de símbolos diferente pm cada clase de declaración, de manera ejemplo, todas las declaraciones de tipo estén contenidas en una tabla de símbolos, que todas las declaraciones de variable estén contenidas en otra. Con ciertos lengu particular los derivados de Algol, tales como C. Pascal y Ada, podemos desear aso blas de símbolos separadas con diferentes regiones de un programa (tales como los dimientos) y vincularlas de acuerdo con las reglas semánticas del lenguaje (coment esto con más detalle en breve),

Los atributos ligados a un nombre por medio de una declaración varían con la c declaración. Las declaraciones de constante asocian valores a nombres; por esta raz ocasiones las declaraciones de constante se conocen como fuaeiones o especifkaci valor. Los valores que pueden ser fijados determinan cómo los trata el compilador. P plo, Pascal y Modula-2 requieren que los valores en una declaración constante sean y, por lo tanto, calculables por el compilador. El compilador puede entonces utilizar I de símbolos para reemplazar los nombres de constante con sus valores durante la compil Otros lenguajes, tales como C y Ada, permiten que las constantes sean dinámicas; es dec lo calculables durante la ejecución. Tales constantes se deben tratar más bien como vari en el sentido que se debe generar el código para calcular sus valores durante la ejecució embargo, tales constantes son de asignación única: una vez que sus valores se han minado no pueden cambiar. Las declaraciones de constante también pueden ligar de implícita o explícita tipos de datos con nombres. En Pascal, por ejemplo, los tipos de dato constantes se determinan implícitamente de sus valores (estáticos), mientras que en C los de datos se proporcionan de manera explícita, como en las declaraciones de variable.

Las declaraciones de tipo vinculan nombres a tipos recién construidos y también den crear alias para tipos nombrados que ya existen. Los nombres de tipo por lo regul utilizan en conjunto con un algoritmo de equivalencia de tipo para realizar la verifica de tipos de un programa de acuerdo con las reglas del lenguaje. Dedicamos una sección tenor en este capítulo a la verificación de tipos, así que por el momento no hablaremos de las declaraciones de tipo.

Las declaraciones de variable vinculan más frecuentemente nombres a tipos de d como en el caso de C

Table aynrtab;

que vincula el tipo de datos representado por el nombre Table a la variable con el no symtab. Las declaraciones de variable también pueden fijar otros atributos impli Uno de tales atributos que tiene un efecto importante sobre la tabla de símbolos es de una declaración o la región del programa donde la declaración se aplica (es decir, las variables definidas por la declaración son accesibles). El ámbito suele indicarse me te la posición de la declaración dentro del programa, pero también puede ser afectado notaciones sintácticas explícitas e interacciones con otras declaraciones. Puede, además, s una propiedad de las declaraciones de constante, tipo y procedimiento. Abordaremos las re- glas del ámbito con más detalle dentro de poco.

Un atributo de las variables relacionado con el ámbito que también está ligado de manera implícita o explícita por una declaración es la asignación de memoria para la variable decla- rada, así como el tiempo que dura la ejecución de la asignación (que a veces se denomina tiempo de vida o extensión de la declaración). Por ejemplo, en C, todas las variables cuyas declaraciones son externas a funciones se asignan estáticamente (es decir, antes del principio

Page 305: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

la tabla de simbolos 30 1

de la ejecución), asf.que tienen una extensión igual a todo el tiempo que tome la ejecución del programa, mientras las variables que se declaran dentro de las funciones se asignan sólo me- diante la duración de cada llamada de función (lo que se denomina asignación automática). C también tiene en cuenta el cambio de la extensión de una declaración dentro de una función, a ser cambiada de automática a estática mediante el uso de la palabra reservada static en la declaración, como en

int count (void)

( static int counter a 0;

return ++counter;

1

La función count tiene una variable local estática counter que retiene su valor de Ila- mada en Ilamada, de manera que count devuelve como su valor el número actual de veces que ha sido llamada.

C también clistingue entre declaraciones que se utilizan para controlar la asignación de memoria y aquellas que se emplean para verificar tipos. En C, no se emplea cualquier de- claración de var~able que comience con la palabra reservada extern para efectuar asigna- ción. De este modo, si la función anterior se escribiera como

int count (void)

f extern int counter;

return ++counter;

k

la variable counter se tendda que asignar (e inicializar) en otro sitio del programa. El len- guaje C se refiere a las declaraciones que asignan memoria como definiciones, mientras que reserva la palabra "declaración" para las declaraciones que no asignan memoria. De mane- ra que, una declaración que comience con la palabra reservada extern no es una defini- ción, pero una declaración estiuidar de una variable, tal como

int x;

es una definición. En C, puede haber muchas declaraciones de la misma variable, pero sólo una definición.

Las estrategias de asignación de memoria, como la verificación de tipo, forman una importante y compleja área en el diseño de compiladores que es parte de la estructura del ambiente de ejecución. Corno dedicaremos todo el capítulo siguiente al estudio de los am- bientes, aquí no hablaremos más de la asignación. En vez de eso regresaremos a un análisis de ámbito y estrategias para el mantenimiento del ámbito en una tabla de símbolos.

6.33 Reglas de imbito y estructura de bloques Las reglas de ámbito en los lenguajes de programación varían mucho, pero existen varias reglas que son comunes a muchos lenguajes. En esta sección analizaremos todas estas reglas, la declaración antes del uso y la regla de anidación más próxima para estmctura de bloques.

La declaración antes del uso es una regla común, utilizada en C y Pascal, que requiere que se declare un nombre en el texto de un programa antes de cualquier referencia al nombre. La declaración antes del uso permite construir la tabla de símbolos a medida que el análisis sintáctico continúa y que las búsquedas se realicen tan pronto como se encuentra una refe- rencia de nombre en el código; si la búsqueda falla, es que ha ocurrido una violación de la declaración antes del uso, y el compilador emitirá un mensaje de error apropiado. De este modo, la tieclaración antes del uso fomenta la compilación de un paso. Algunos lenguajes

Page 306: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 A N ~ L ~ S ~ S SEM~NTICO

no requieren de la declaración antes del uso (Modula-2 es un ejemplo), y en ellos se r re de un paso por separado para la construcción de la tabla de símbolos; la compilaci un paso no es posible. :

La estructura de bloques es una propiedad común de los lenguajes moderno bloque en un lenguaje de programación es cualquier construcción que pueda co declaraciones. Por ejemplo, en Pascal, los bloques son el programa principal y las raciones de proccdimiento/función. En C, los bloques son las unidades de compila decir, los archivos de código), las declaraciones de procedimientoifunción y las sent compuestas (secuencias de sentencias encerradas entre llaves ( . . . 1). Las estnict uniones en C (registros en Pascal). también se pueden visualizar como bloques, ya qu tienen declaraciones de campo. De manera similar, las declaraciones de clase de los le jes de programación orientados a objetos son bloques. Un lenguaje está estructura bloques si permite la anidación de bloques dentro de otros bloques, y si el ámbito de d raciones en un bloque está limitado a ése y otros bloques contenidos en el mismo, s la regla de anidación más próxima: dadas varias declaraciones diferentes para el nombre, la declaración que se aplica a una referencia es la única en el bloque anidad próximo a la referencia.

Para ver cómo la estructura de bloques y la regla de anidación más próxima afe tabla de símbolos, consideremos el fragmento de código en C de la figura 6.14. código existen cinco bloques. En primer lugar, existe el bloque del código complet contiene las declaraciones de las variables enteras i y j y la función f . En segundo existe la declaración de f misma, que contiene la declaración del parámetro siz tercer lugar, existe la sentencia completa del cuerpo de f , que contiene las declaracio las variables carácter i y temp. (La declaración de función y su cuerpo asociado se p visualizar alternativamente como la representación de un bloque simple.) En cuarto existe la sentencia compuesta que contiene la declaración double j. Finalmente, exist sentencia compuesta que contiene la declaración char * j . Dentro de la función f tene declaraciones simples de variables size y temp en la tabla de símbolos, y todos los uso estos nombres se refieren a estas declaraciones. En el caso del nombre i, hay una decl ción local de i como un char dentro de la sentencia compuesta de f , y por la regla de

. dación más próxima esta declaración reemplaza la declaración no local de i como un en el bloque de archivo de código circundante. (Se dice que la i n t i no local tiene un h de ámbito dentro de f .) De manera similar, las declaraciones de j en las dos sente compuestas postenores dentro de f reemplazan la declaración no local de j como un dentro de sus bloques respectivos. En cada caso las declaraciones originales de i y j se cuperan cuando se sale de los bloques de las declaraciones locales.

Figura 6.14 i n t i , j ;

fragmento de código en C que ilustra los ámbitos i n t f ( i n t s i z e )

anidados { char i, temp; . . . { double j;

. . . )

. . . { char * j ;

Page 307: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 6.15 fragmento de código en parcal que ilustra los ambitos anidados

La tabla de sirnbolor 303

En muchos lengua,jes, tales como Pascal y Ada (pero no C), los procedimientos y funciones también pueden estar anidados. Esto presenta un factor de coinplicación en el ambiente de tiempo de ejecución para tales lenguajes (el cual se estudiará en el capítulo si- guiente), pero no presenta complicaciones particulares para ámbitos anidados. Por ejemplo, el código en Pascal de la figura 6.15 contiene los procedimientos anidados g y h, pero tiene esencialmente la misma estructura de iabla de símbolos que la del código en C de la figura 6.14 (excepto, por supuesto, para los nombres adicionales g y h).

program E x ;

var i , j : integer;

function f ( s i z e : integer): integer;

var i.temp: char;

procedure g;

var j : real;

begin

. . . end;

grocedure h; var j: "char;

begin

. . . end;

begin ( * f *)

. . . end;

begin ( * programa principal * )

Para implementar hmbitos anidados y la regla de anidación más próxima. la operación de inserción de la tabla de símbolos no debe sobrescribir declaraciones anteriores, sino que las debe ocultar temporalmente, de manera que la operación de búsqueda sólo encuentre la declaración para un nombre que se haya insertado más recientemente. De manera similar, la operación de eliminación no debe eliminar todas las declaraciones correspondientes a un nombre, sino sólo la más reciente, revelando cualquier declaración previa. Entonces la cons- trucción de la tabla de símbolos puede continuar realizando operaciones de inserción para todos los nombres declarados en la entrada dentro de cada bloque y efectuando operaciones de eliminación corre?pondientes de los mismos nombres a la salida del bloque. En otras palabras, la tabla de símbolos se comporta de una manera parecida a una pila ilurante el pro- cesamiento de los ámbitos anidados.

Yara ver cómo p k d e mantenerse esa estmctura de una manera práctica, considere la ini- plcmentación de la tabla de dispersión de una tabla de símbolos que se escribió con antcrio- ridad. Si tiacemos suposiciones de siinplificación seniejantes a las de la figura 6.12 (plígina 297). entonces tlespués [le que son procesadas las declaraciones del cucrpo del procedimiento

Page 308: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

comportan como una pila para las diferentes declaraciones de ese nombre.

F ¡ @ ~ f a 6.16 Índices Cubetas Lista de elementos

Contentdo de tabla - o - -+i ( c h a r ) H i (int)jo]

de sirnbolos en vartor 1 4 s i z e (inti-j (inti1.l lueares en el codiao

m

9 " - '. de la figura 6.14 3

4

(a) Después del procesamiento de las declaraciones del cuerpo de f

hdices Cubetas Lista de elementos

CAP. 6 1 A N ~ L I S I S SEMANTICO

f en la figura 6.14, la tabla de símbolos podría parecerse a la de la figura 6.16(a). D procesamiento de la segunda sentencia compuesta dentro del cuerpo de f (la que la declaración char * j), la tabla de símbolos se vería como la de la figura 6.16(b mente, despues de salir del bloque de la función f, la tabla de símbolos podría verse en la figura 6.l6(c). Advierta como, para cada nombre, las listas ligadas en cada cu

(b) Después del procesamiento de la declaración de la segunda sentencia compuesta anidada dentro del cuerpo de f

índices Cubetas Lista de elementos

(c) Después de salir del cuerpo de f (y de eliminar sus de~laraciones)

Existen varias alternativas posibles para esta implementación de ámbitos anidados. solución es construir una nueva tabla de símbolos para cada ámbito y vincular las tablas de de ámbitos internos a ámbitos externos, de manera que la operación de búsquedu contin buscando automáticamente con una tabla encerrada, si falla al encontrar un nombre en la t bla actual. Abandonar un imbito requiere entonces menos esfuerzo, ya que no es necesario volver a procesar las declaraciones utilizando operaciones de elirninacih. En vez de eso, toda la tabla de símbolos correspondiente al ámbito se puede liberar en un paso. Un ejem- plo de esta estructura correspondiente a la figura 6.16(b) se proporciona en la figura 6.17. En esa figura se tienen tres tablas, una para cada ámbito, ligadas desde la más interna hasta la más externa. Para abandonar un ámbito sólo se requiere volver a establecer el apuntador de acceso (indicado a la izquierda) al siguiente hmbito niás externo.

Page 309: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ca tabla de rimbolos 305

gura 6 ií' ,mctura de tabla de mbolo~ correspandtente a la Fa 6.16(b) en la que re Ililaion tablas reparadas ,a cada imblto

temp (char)l a ]

Un procesamiento adicional y cálculo de atributos también pueden ser necesarios duran- te la construcciún de la tabla de símbolos, dependiendo del lenguaje y los detalles de la ope- ración del compilador. Un ejemplo es el requerimiento en Ada de que todavía sea visible un nombre no local dentro de un hueco de ámbito, donde puede ser referenciado utilizando una notación semejante a la selección de campo de registro, por medio del nombre asociado con el ámbito en el cu:il se declara el nombre no local. Por ejemplo, en el código Pascal de la figura 6.15, dentro de la función f la variable entera global i, en Ada, todavía ser6 visible como la variable Ex. i (utilizando el nombre del programa para identificar el ánrbito glob:~l). De cste modo, puede tener sentido, mientras se construye la tabla de símbolos, identificar cada h b i t o mediante un nombre y asignar un prefijo a cada nombre declarado dentro de un Ambito mediante sus nombres de ámbito anidados acumulados. Así, todas las ocurrencias del nombre j en la figura 6.15 se distinguirían como Ex. j , Ex. f . g . j y Ex. f . h. j. De. manera adicional o alternativa, puede necesitarse asignar un nivel de anidaeión o pro- fundidad de anideci6n a cada ámbito y registrar en cada entrada de la tabla de símbolos el nivcl de anidación de cada nombre. De esta forma, en la figura 6.15 el ámbito global del programa tiene nivel de anidación 0, las declaraciones de f (sus parámetros y vr~riables lo- cales) tienen nivel de anidación 1 y las declaraciones locales tanto de g como de h tienen rii- ve1 de anidación 2,

Un mecanismo que es semejante a las características de selección de iírnhito de Ada que se acaba de describir es el operador de resolución de ámhito : : de C f f. Este operador permite que Se pueda tener acceso al ámbito de una declaración de clase desde el exterior de la declaración misma. Esto puede ser empleado para conipletar la definición de funcio- nes niienibro desde el exterior de la declaración de clase:

class A { ... int f 0 ; ... 3 ; / / f es una función miembro

A::fO / / ésta es la definición de f en A

( ... 1

Todas las clases, procedimientos (en Ada) y estructuras de registro pueden visuali- ,zarse como representación de un ámbito nombrado, con el conjunto de declaraciones lo- cales como un atributo. En situaciones donde estos ámbitos se pueden rcferenciar de mane- ra externa, conviene construir una tabla de símbolos por separado para cada ámbito (como en La figura 6.17).

Hasta ahora hemos estado comentando la regla de ámbito estándar que sigue la cstruc- tura textual del programa. En ocasiones esto se llama ámhito léxico o estático (porque I:I tabla de símbolos se construye de manera estática). Una regla de ciinbito cilternativ:1 mie se utiliza en algunos lenguajes orientados dinámicamente (versiones antigtlas de LISP, SNOBO[, y algunos lenguaies de consulta de base de datos) se conoce como ámbito dinámico. Esta - regla requiere que la :iplicaciGn cie los iimbitos anidados siga la trayectoria de ejecrtci&i, cii vcz tiel discíio text~i;il del progr:ima. Un cjonplo simple que muestra diferencias cnise, 1:~';

Page 310: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 AHÁLISIS SEHANTIEO

dos reglas es el cúdigo en C de la figura h. 18. Este cítdigo imprime 1 utilizando la re ámbito estúndar de C debido a que el ántbito de la variable i al nivel de archivo se <le al procedimiento f . Si se utilizar:i ámbito dinámico, entonces el programa impritni porque f es llamado desde main, y main contiene una tleclaración de i (con valor 2 se extiende a f si se utiliza la secuencia de llamadas para resolver referencias no loc inibito dinámico requiere que la tabla de símbolos se construya durante la ejecució lizando operaciones de irrsercicín y rlirnincici(ít~ a rnedida que los ámbitos se introdu extraen en tiempo de cjecución. De este {nodo, el uso del ámbito dinámico requiere q tabla.de sítnbolos se vuelva parte del ambiente y que el código se genere mediante un co pilador para mantenerlo, en vez de tener la tabla de símbolos construida directamente (y táticaniente) por el compilador. El ámbito dinámico compromete, adem6s. la legibilidad d los programas, porque las referencias iio locales no pueden resolverse sin simular la ejec ciún del pg rama . Finalmente, el ámbito dinámico es incompatible con la verificaci6n d tipo estútico, ya que los tipos de datos de las variables deben mantenerse por medio de la tabl de símbolos. (Advierta el problema que se presenta en el c6digo de la figura 6.18 si la dentro de main se declara como double.) De este modo, el ámbito dinámico rara vez utiliza en los lenguajes rnodernos, por ello no comentaremos más a1 respecto aquí.

6.18 #include tstdio.h>

C que ilustra la :ia entre inibito int i = 1;

y dinámico void f(void)

{ printf ("%d\n", i) ; 1

void main(void)

( int i = 2;

f 0 ; return O;

Finalmente, deberíamos ztdvertir que la operación de rli~nincición se ha mostrado en La tigura 6.16 para eliminar del todo cieclaraciones de la tabla de símbolos. De hecho puede ser necesario mantener las tlecl~raciones en la tabla (o por lo menos no dejar de asignar sus espacios en memoria), porque otras partes del compiladov pueden volver a necesitar hxw referencia a ellas posteriormente. Si deben mantenerse en la tabla de sítnbolos, entonces la

6.3.4 lnteracción de declaraciones del mismo nivel Otra cuestión que se relaciona con el &nbito es La interacción entre declaraciones del mismo nivel de anidación (es decir, asociadas a un solo bloque). Ésta pueden variar con la clase de declaración, y con el lenguaje que se está traduciendo. Un requerimiento típico en muchos Icnguajes(C, Pascal, Ada) es que no se puede volver a utilizar el mismo nombre cn decla- rxiories 111 rtiisino nivcl. De este modo, en C. las siguientes declaraciones consecutivas pro- voc~irán un error dc compilación:

typedef int i;

int i;

Page 311: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

La tabla de simbolos 307

Para verificar este requerimiento un cornpilador debe realizar una búsqueda antes de cada inserción y determinar mediante algún inecanisnio (el nivel de anidación, por ejemplo) si cualquier declaración preexistente con el mismo nombre está o no en el mismo nivel.

Algo más difícil es la cuestión de cuánta información tienen disponible entre s í las declaraciones en una secuencia al mismo nivel. Por ejemplo, considere el fragmento de código en C

int i = 1;

void f(void)

( int i = 2, j = i+l;

La cuestión es si j dentro de f es inicializada al valor 2 o 3, es decir, si se utiliza la decla- ración local para i o la declaración no local para F. Puede parecer natural que se utilice la declaración más reciente (la local), mediante la regla de anidación más próxima. Ésta es en realidad la manera en que se comporta C. Pero esto presupone que cada declwación se agre- gue a la tabla de símbolos a medida que se procesa, lo que se conoce como declaración secuencial. Es posible que en vel: de esto todas las declaraciones se procesen "simultánea- mente" y se agreguen a la vez a la tabla de símbolos al final de la sección de declaraciones. Entonces cualquier nombre en las expresiones dentro de las declaraciones haría referencia a las declaraciones anteriores, no a las nuevas declaraciones que se e s t b procesando. Una es- tructura de declaración así se denomina declaración colateral, y algunos lenguajes funcio- nales, como ML y Scheme, tienen una estructura de declaración como ésta. Una regla para declaraciones de esta naturaleza requiere que las declaraciones no se agreguen inmediata- mente a la tabla de símbolos existente, sino que se acumulen en una nueva tabla (o estruc- tura temporal) y después se agreguen a la tabla existente cuando todas las declaraciones se hayan procesado.

Finalmente, existe el caso de la estructura de declaracibn recursiva, en la cual Ias de- claraciones pueden hacer referencia a s í mismas o entre sí. Esto es necesario en particular para declaraciones de procediniientoffunción, donde los grupos de funciones mutuamente recursivas son comunes (por ejemplo, en un analizador sint5ctico descendente recursivo). En su forma más simple, una función recursiva se llama a sí misma, como en el siguiente código en C, para una función que calcula el máximo común divisor de dos enteros:

int gcd(int n, int m)

{ if (m -- 1 0) return n; else return gcd(m,n % m);

1

Para que esto sea compilado correctamente, el compilador debe agregar el nombre de la fun- ción gcd a la tabla de símbolos untes de procesar el cuerpo de ia función. De otro d o , el nombre gcd no se hallará (o no tendrá el significado correcto) cuando se encuentre la Ila- nlada recursiva. En casos niás complejos de un grupo de funciones mutuamente recursivas, como en el siguiente fragmento de código en C

void f (void)

O ... ) void g(void)

. O ... 1

Page 312: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 AMALISIS SEMANTICO

no es suficiente con agregar cada función a la tabla de símbolos antes que su cuerpo cesado. En realidad, el fragmento anterior de código C producirá un error durante la lación para la llamada a g dentro de f . En C, este problema se elimina agregando 1 minada declaración de función prototipo para g antes de la declaración para f :

void g(void); / * declaración de prototipo de- función */

void £ (void)

C... s o ... 1

void gfvoid)

. o ... 1

Tal modificación se puede ver como un modificador de ámbito, extendiendo el á nombre g para incluir f . De este modo, el compilador agrega g a la tabla de símbol do se alcanza la (primera) declaración de prototipo para g íjunto con su propio at ámbito posicional). Naturalmente, el cuerpo de g no existe hasta que se alcanza la ción (o definición) principal de g. Además, todos los prototipos de g deben tener verifi de tipo para asegurar que son idénticos en estructura.

El problema de la recursividad mutua puede ser resuelto de varias maneras. Por plo, en Pascal se proporciona una declaración fonvard como extensor de ámbito de dimiento/función. En Modula-2 la regla de ámbito para procedimientos y funciones (a de variables) extiende sus ámbitos para incluir el bloque completo de sus declaracion manera que son mutuamente recursivos de manera natural y no es necesario un mecan de lenguaje adicional. Esto requiere un paso de procesamiento en el cual todos los p mientos y funciones se agreguen a la tabla de síq~bolos antes de procesar cualquiera cuerpos. Declaraciones mutuamente recursivas similares también se encuentran dispon en varios otros lenguajes.

63.5 Un ejemplo extendido de una gramática con atributos mediante una tabla de símbolos

Ahora queremos considerar un ejemplo que demuestre varias de las de las claraciones que hemos descrito y desarrollar una gramática con atributos que haga exp tas estas propiedades en el comporfamiento de la tabla de símbolos. La gramática que zamos para este ejemplo es la siguiente condensación de la gramática de expresión aritm simple, junto con una extensión que involucra declaraciones:

S -+ exp exp -+ ( exp ) / exp + exp / id 1 num 1 let dec-list in exp dec-list -+ dec-list , decl / decl deel-+ id = exp

Esta gramática incluye un símbolo inicial S en nivel superior, debido a que la gramática con atributos contiene atributos heredados que necesitarán inicialización en las raíces del árbol sintáctico. La gramática contiene sólo una operación fia adición, con token +) para hacerla más simple. También es ambigua, y se parte del supuesto de que un analizador sintácti- co ya construyii un irbol sintáctico o manej6 de :&una manera las ambigiiedades (dejamos para los e,jercicios la construcción de una graniitica no ambigua equivalente). Sin embargo.

I

Page 313: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

La tabla de simbolos 309

incluimos parhtesis, de manera que podamos escribir expresiones de manera no ambigua en esta gramática si así lo deseamos. Como en ejemplos anteriores, utilizando gramáticas similares suponemos que num e id son tokcns cuya estructura está determinada por un analizador léxico (puede wponerse que num es una secuencia de dígitos y que id es una secuencia de caracteres).

La adición a la gramática que involucre las declaraciones es la expresih let:

e.ip -+ l e t dec-list in exp

En una expresión let las declaraciones se componen de una secuencia de declaraciones se- paradas por comas de declaraciones de la forma id = exp. Un ejemplo de esto es

De manera informal, la semántica de una expresión let es como sigue. Las declaraciones después del token let establecen nombres para expresiones que, cuando aparecen en la exp después del token in, qimbolizan los valores de las expresiones que representan. (La exp es el cuerpo de la expresión let.) El valor de la expresión let es el valor de su cuerpo, que se calcula reemplazando cada nombre en las declaraciones por el valor de su exp correspondiente y posteriormente calculando el valor del cuerpo de acuerdo con las reglas de la aritmética. Por ejemplo, en el caso anterior, x representa el valor 3 (el valor de 2+1) y, por otro lado, y representa el valor 7 (el valor de 3+4). De esta manera, la ex- presión let misma tiene el valor 10 (= el valor de x t y cuando x tiene valor 3 e y tiene el valor 7).

A partir de la semántica que se acaba de dar, vemos que las declaraciones dentro de una expresión let representan una clase de declaración constante (o fija), y que las expresiones let representan los bloques de este lenguaje. Para completar la exposición informal de la se- mántica de estas expresiones, necesitamos describir las reglas de ámbito e interacclones de las declaraciones en las expresiones let. Advierta que la gramática permite anidaciones arbitrarias de expresiones let entre sí mismas, por ejemplo, en la expresión

l e t x = 2, y = 3 in ( l e t x = x+l , y s ( l e t 2=3 in x+y+z)

in (x+Y)

)

Estableceremos las siguientes reglas de ámbito para las declaraciones de expresiones let. En primer lugar, no habrá declaración repetida del mismo nombre dentro de la misma expre- sión let. De este modo, una expresión de la forma

l e t x=2,x=3 in x+ l

es ilegal, y da como resultado un error. En segundo lugar, si no es declarado cualquier nombre en alguna expresión let circundante, también se obtiene un error. De esta Forma, la expresión

l e t x =2 in x+y

es errónea. En tercer lugar, el hmbito de cada declaración en una expresión let se extiende so- bre cl cuerpo de lrt de acuerdo con la regla de anidación rnas próxima pard estructura de blo- ques. Así, el valor de la expresión

Page 314: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 ANÁLISIS SEMANTICO

let x=2 in (let x=3 in X )

es 3, no 2 (puesto que la x en el ler interno se refiere a la declaración x=3, no a la cicín x=2).

~inalmente, definimos la interacción de las declaraciones en una lista de decla en el mismo let para que sea secuencial. Es decir, cada declaración utiliza las dec nes anteriores para resolver nombres dentro de SU propia expresión. De este modo, expresión

let x = 2 , y = x + l in (let x=x+y,y=x+y in y )

la primera y tiene valor 3 (utilizando la declaración anterior de x), la segunda x tiene 5 (utilizando las declaraciones de let entre paréntesis) y la segunda y tiene valor 8 (em do la declaración anidada de y además del valor de la x apenas declarada). Así, la sión completa tiene valor 8. Invitamos al lector a calcular de manera parecida el valo expresión let triplemente anidada de la página anterior.

Ahora deseamos desarrollar ecuaciones de atributo que utilicen una tabla de sí para estar al tanto de las declwaciones en expresiones let y que expresen las reglas hito e interacciones que se acaban de describir. Por simplicidad, sólo utilizaremos 1 de símbolos para determinar si una expresión es o no errónea. No escribiremos ecna para calcular el valor de las expresiones, pero dejaremos que el lector lo haga como cio. En vez de eso calculamos el atributo booleano sintetizado err que tiene el valor ("verdadero") si la expresión es errónea y false ("falso") si la expresión es correcta, de a do con las reglas previamente establecidas. Para hacer esto necesitamos tanto un a to heredado symtnh, que represente a la tabla de símbolos ("bol le"), como un atr heredado nestlevel ("nivel anidado") para determinar si dos declaraciones están d del mismo bloque Let. El valor de nestlevel es un entero no negativo que expresa el ni anidación actual de los bloques let. Se inicializa con el valor O en el nivel más extern

El atnbuto syntub necesitará operaciones de tabla de símbolos típica. Como quer escribir ecuaciones de atributo, expresamos las operaciones de tabla de símbolos en una nera libre de efectos colaterales escribiendo la operación de irrserción para tomar una ta de símbolos como un parámetro y devolver una nueva tabla de símbolos con la nueva in mación agregada, pero con la tabla de símbolos original sin sufrir cambios. De esta form inserción Ntsert(s, ti, 1) devolverá una nueva tabla de símbolos que contiene toda la infor ción de la tabla de símbolos s y además asocia el nombre n con el nivel de anidación 1 s cambiar s. (Como estamos determinando sólo la exactitud, no es necesario asociar un v a n, únicamente un nivel de anidación.) Puesto que esta notación garantiza que podamo cuperar la tabla de símbolos s original, no se necesita una operación delete explícita. FI mente, para probar los dos criterios que deben satisfacerse para la exactitud (que tod nombres que aparezcan en expresiones se hayan declarado previamente y que no ocu declaraciones repetidas en el mismo nivel), debemos poder verificar la presencia de un n bre en una tabla de símbolos, y también recuperar el nivel de anidación asociado co nombre que esté presente. Haremos esto con las dos operaciones isin(s, n), que devue un valor booleano dependiendo de si n está o no en la tabla de símbolos S , y lookup que devuelve un valor entero dando el nivel de anidación de la declaración más recie n, si e.xiste, o - 1, si n no está en la tabla de s ímboh s (esto permite expresa eaaciones uii- lizando lookup sin tener que realizar primero una operación isin). Finalmente, debemos tener una notación para una tabla de símbolos inicial que no tenga entradas; escribiremos esto como empfyrnble ("tabla vacía").

Con estas operaciones y convenciones para tabla de símbolos, volvemos ahora a la cscrititra de las ecuxiones de atributo para los tres atributos syrntub, nesrlevel y err de las expresiones. La gramática con atributos completa se resume cn la tabla 6.9.

Page 315: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

La tabla de rimbalor

Regla gramatical Reglas semánticas

S + exp ~~.rpiip..syrntub - rnii,iyuhlt. exp.rie.silevrl = O

En el nivel niás alto, asignainos los valores de los dos atributos heredados y elevamos el valor del atributo sintetizado. De este modo, la regla gramatical S -+ exp tiene Itis ires ecuaciones de atributo asociadas

Page 316: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 ANALISIS SEMANTKO

Para la regla exp, -+ expz + exp3 (como es habitual cuando numeramos los no te les escribiendo ecuaciones de atributo), las reglas siguientes expresan el hecho de expresiones del lado derecho heredan los atributos symtab y nest[evel de la expr lado izquierdo, la cual contiene un error si por lo menos una de las expresiones derecho contiene un error:

exp, symtab = exp, symtab exp3 .symtab = exp, .symtab exp, .nestlevel = exp, mstlevel exp, .nestlevel = exp, .nestlevel exp, .err = exp, .err or exp, .err

La regla exp -+ i d produce un error sólo si el nombre del i d no puede hall tabla de símbolos actual (escribiremos i d .wme para el nombre del identificador dremos que se calcula mediante el analizador léxico o el analizador sintáctico), de la ecuación de atributo asociada es

exp.err = not isin(exp.symtab, id m m e )

Por otra parte, la regla exp -+ num nunca puede producir un error, de modo que la ec de atributo es

exp.err = false

Ahora volveremos a un anilisis de las expresiones let con la regla gramatical

exp, -+ l e t dec-list i n expz S 4

y las reglas asociadas para declaraciones. En la regla para las expresiones let, la decl-1' compone de cierto número de declaraciones que se deben agregar a la tabla de símbol tual. Expresamos esto asociando dos tablas de símbolos por separado con decl-list: la entrante intab, que está heredada de expl, y la tabla saliente outtab, que contiene las n declaraciones (además de las antiguas) y que debe pasarse a exp,. Debido a que las vas declaraciones pueden contener errores (tales como la declaración repetida de los mos nombres), debemos expresar esto teniendo en cuenta una tabla de símbolos esp errtab que se pase desde una dec-list. Finalmente, la expresión let tiene un error s dec-list contiene un error (en cuyo caso su outtab sería errtab) o si el cuerpo expz de 1 presión let contiene un error. Las ecuaciones de atributo son

dec-1ist.intab = exp, .symtab dec-list.nestleve1 = exp, mstlevel + 1 exp, .symtab = dec-1ist.outtab expz .nestlevel = dec-list.nestleve1 exp, .err = (decl-1ist.outtab = errtab) or exp2 .err

Advierta que el nivel de anidación también se incrementa en uno tan pronto como se intro- duce el bloque let.

Resta desarrollar ecuaciones para lktas de declaraciones y para declaraciones individua- les. Una libta de declaraciones debe acumular, mediante la regla secuencia1 para declaracio- nes, declaraciones individuales a medida que el procesamiento de la lista continúa. De este modo, dada la regla

Page 317: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tipos de datos y verificación de tipos

dec-liut, -+ úec-list2 , decl

la outtab de dec-list2 se pasa como la intab a decl, con lo que se da acceso a decl a las de- claraciones que la preceden en la lista. En el caso de una declaración simple ((lec-list 4

decl), se realiza copia y herencia estándar. Las ecuaciones completas se muestran en la ta- bla 6.9.

Finalmente analizamos el caso de una declaración individual

deel-+ i d = exp

En este caso la decl.intub heredada se pasa de inmediato a exp (porque las declaraciones en este lenguaje no son recursivas, de modo que rxp debe encontrar sólo declaraciones anterio- res del nombre de este i d , no del actual). Entonces, si no hay errores, id.nume se inserta en la tabla con el nivel de anidación actual, y esto se pasa hacia atrás como la outtab sinte- t i~ada de la declaración. Pueden presentarse errores de tres maneras. La primera, un error puede haberse presentado ya en una declaracibn anterior, en cuyo caso decl.in#ab es errtub; esta errtab debe ser pasada como outtuh. La segunda, puede presentarse un error dentro de la enp; esto se indica mediante e.vp.err y, si es verdadero, también debe provocar que outtub sea errtab. La tercera, la declaración puede ser una declaración repetida de un nombre en el mismo nivel de anidación. Esto se debe verificar realizando una búsqueda ("lookup"), y este error también se debe informar obligando a que outtab sea errtab. (Advierta que si lookup falla en su intento de encontrar id.name, entonces devuelve - 1, el cual nunca coincide con el nivel de anidación actual, de modo que no se genera ningún error.) La ecuación completa para decl.outtab se proporciona en la tabla 6.9.

Con esto concluye nuestro análisis de las ecuaciones de atributo.

6.4 TIPOS DE DATOS Y VERIFICACION DE TIPOS Una de las tareas principales de un compilador es el cálculo y mantenimiento de la informa- ción en tipos de datos (inferencia de tipos), y el uso de tal información para asegurar que cada parte de un programa tenga sentido bajo las reghs de tipo del lenguaje (verificación de tipo). Estas dos tareas por lo regular están estrechamente relacionadas, se realizan juntas, y se hace referencia a ellas simplemente como verificación de tipos. La información del tipo de datos puede ser estática o dinámica, o una mezda de las dos. En la mayoría de los dialectos de LISP la información de tipo es enteramente dinámica. En un lenguaje así, un compilador debe generar código para realizar inferencia de tipo y verificación de tipos durante la ejecución. En lenguajes más tradicionales, como Pascal, C y Ada, la información de tipo es principalmente estática, y se utiliza como el mecanismo principal para verificar la exactitud de un programa antes de la ejecución. La información de tipo estático también se emplea para determinar el tamaño de memoria necesario para la asignación de cada variable y la manera en que se puede tener acceso a la memoria, y esto puede utilizarse para simpli- ficar el ambiente en tiempo de ejecución. (Comentaremos esto en el capítulo siguiente.) En esta sección sólo nos ocuparemos de la verificación de tipos estática.

La información de tipo de datos puede presentarse en un programa en varias formas di- ferentes. Teóricamente, un tipo de datos es un conjunto de valores, o de manera más preci- sa, un conjunto de valores con ciertas operaciones sobre ellos. Por ejemplo, el tipo de datos integer ("entero") en un lenguaje de programación hace referencia a un subconjunto de los enteros matemáticos, junto con las operaciones aritméticas, tales como + y *, que son proporcionadas por la definición del lenguaje. En el terreno práctico de la construcción de compiladores, estos conjuntos por lo regular se describen mediante una expresión de tipo, que es un nombre de tipo, tal como integer, o una cxpresidn cstructurada, tal como

Page 318: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 AN~LISIS SEMANTICO

array . [l. .lo] of real, cuyas operaciones generalmente se suponen o impl' expresiones de tipo se pueden presentar en diversos lugares en un programa. Éstas i declaraciones de variable tales como

var x: array [l.. 101 o£ real.;

la cual asocia un tipo con un nombre de variable, y declaraciones de tipo, tales co

la cual define un nuevo nombre de tipo para utilizarlo en un tipo posterior o declar de variable. Tal información de tipo es explícita. También es posible que la inform tipo sea implícita, por ejemplo en la declaración de constante en Pascal

const greetfng = "Hell~!~;

donde el tipo de greeting ("aaludos") es implícitaniente array [l. . 6 ] o de acuerdo con las reglas de Pascal.

La información de tipo, ya sea explícita o implícita, que est6 contenida en de nes, se mantiene en la tabla de símbolos y se recupera mediante el verificador de ti pre que se hace referencia a los nombres asociados. Los tipos nuevos se infieren e de estos tipos y se asocian con los nodos apropiados en el árbol sintáctico. Por eje la expresión

los tipos de datos de los nombres a e i se obtienen de la tabla de símbolos. Si a es d array [l. .lo] o£ real, e i tiene tipo integer, entonces se determina que las presión a ti] es correcto en tipo y tiene tipo real. (La cuestión de si i tiene un val esté entre 1 y 10 es una cuestión de verificación de intervalo, que en general no es e ticamente determinable.)

La manera en que se representan los tipos de datos mediante un compilüdor, la form que una tabla de símbolos mantiene la información del tipo y las reglas que utiliza rificador de tipo para inferir los tipos, dependen todas de las clases de expresiones de disponibles en un lenguaje y las reglas de tipo del lenguaje que gobiernen el uso de e expresiones de tipo.

6.41 Expresiones de tipo y constructores de tipos Un lenguaje de programación siempre contiene un número de tipos ya incluidos, con bres tales corno int y double. Estos tipos predeñnidos corresponden, ya sea a tip datos numéricos que son provistos internamente mediante diversas arquitecturas de m na y cuyas operaciones ya existen como instrucciones de máquina, o a tipos elementa mo boolean y char, cuyo comportamiento es fácil de implementar. Tales tipos de son tipos simples, ya que sus valores no exhiben estructura interna explícita. Una repr ración típica para enteros es en la forma de complemento a dos de dos o cuatro bytes. Una representación típica para números reales, o de punto flotante, es de cuatro u ocho bytes, con un bit de signo, un campo de exponente y un campo de fracción (o mantisa). Una repre- sentación coniún para caracteres es la de los códigos ASCII de un byte, y para los valores booleanos es la de un byte, del cual sólo se utiliza el bit menos significativo (1 = "true" o verdadero, O = "false" o falso). En ocasiones un lenguaje impondrá restricciones sobre cómo 5e van a iinplementar e w s tipos predefinido~. Por ejemplo, el lenguaje C eitándar requiere que un tipo double de punto tlotante tenga por lo menos 10 dígitos decimale\ de precisión.

Page 319: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tipos de datos y verificacion de tipos 315

Un tipo predefinido interesante en el lenguaje C es el tipo void. Este tipo no tiene va- lores, así que representa el conjunto vacío. Se le utiliza para representar una función que no devuelva algún valor (es decir, un procedimiento), así como para representar un apuntador que (de momento) apunte a un tipo desconocido.

En algunos lenguajes es posible definir nuevos tipos simples. Ejemplos típicos de éstos son los tipos subrange (3ubintervalo") y los tipos enumerated ("enumerados"). Por ejem- plo, en Pascal el subintervalo de los enteros compuesto de los valores del O hasta el 9 puede declararse como

type Digit = 0. .9;

mientras que un tipo enumerado compuesto de los valores nombrados red, green y blue puede declararse en C como

tygedef enum ired,green,blue) Color;

Las implementaci~nes tanto de subintervalos como enumeraciones podrían ser como enteros, o se podría utilizar una cantidad más pequeña de memoria que fuera suficiente para represen- tar todos los valores.

Dado un conjunto de tipos predefinidos, se pueden crear nuevos tipos de datos utilizan- do constructores de tipo, tales como array y record o struct. Tales constructores se pueden ver como funciones que toman tipos existentes como sus parámetros y devuelven nuevos tipos con una estructura que depende del constructor. Tales tipos con frecuencia son denominados tipos estructurados. En la clasificación de estos tipos es importante comprender la naturaleza del conjunto de valores que un tipo así representa. A menudo, un constructor de tipo equivaldrá estrechamente a una operación de conjuntos en los conjuntos de valores subyacentes de sus parámetros. Ilustraremos esto mencionando varios constructores conlu- nes y comparfíndolos con las operaciones de conjuntos.

Array (Arreglo) El constructor de tipo ( i r r q ("arreglo") o ("matriz") toma dos parámetros de tipo, en donde uno es el tipo-índice y el otro el tipo-componente y produce un nuevo tipo array. En una sintaxis tipo Pascal escribiríamos

array [tipo-índice] o£ tipo-componente

Por ejemplo, la expresión tipo Pascal

array [Color] o£ char;

crea un tipo array cuyo tipo-índice es color y cuyo tipo-componente es char. Con fre- cuencia existen restricciones en los tipos que se pueden presentar como tipos de índice. Por ejemplo, en Pascal un tipo de índice está limitado a los denominados tipos ordinales: tipos pa- ra los cuales todo valor tiene un predecesor inmediato y un sucesor inmediato. Tales tipos incluyen subintervalos enteros y de caricter, así como tipos enumerados. En contraste, en lenguaje C sólo se permiten intervalos enteros comenzando en O, y sólo se especifica el ta- maño en lugar del tipo-índice. En realidad, no hay palabra reservada que corresponda a array en C: los tipos arreglo \e declaran escribiendo simplemente como sufijo del nombre el intervalo entre corchetes. De modo que no hay un equivalente directo en C para la expre- \ión anterior tipo Paical, pero la declaración de tipo en C

typedef char ArI31;

define un tipo Ar que tiene una estructura de tipo equivalente al tipo precedente (suponien- do que Color tenga tres valores).

Page 320: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 ANALISIS SEHANTICO

Un arreglo representa valores que son secuencias de valores del tipo compone zadas mediante valores del tipo índice. Es decir, si tipo-índice tiene conjunto de y tipo-componente tiene conjunto de valores C, entonces el conjunto de valores c diente al tipo array [tipo-índice] of tipo-componente es el conjunto de secuen tas de elementos de C indizados mediante elementos de I , o, en términos matem conjunto de las funciones I -+ C. Las operaciones asociadas sobre los valores d arreglo se componen de la operación única de subindización, la cual se puede para asignar valorés a componentes o buscar valores de componentes: x : = a [r a [bluel : = y.

Por lo regular, a los arreglos se les asigna almacenamiento contiguo desde lo más pequeños a los más grandes, para permitir el uso de cálculos de desplazamient máticos durante la ejecución. La cantidad de memoria necesaria es n * tamaño, don el número de valores en el tipo de índice y tamaño es la cantidad de memoria neces ra un valor del tipo componente. De este modo, una variable del tipo array [O. . . . . , ., . , = ' . , ~ integer necesita 40 bytes de almacenamiento si cada entero ocupa 4 bytes.

-:.,.-a .: .',-">;S .,.,,. "<, 2: .,A<

Una.complicación en la declaración de arreglos es la de los arreglos muftidi nales. Éstos a menudo se pueden declarar utilizando aplicaciones repetidas del con de tipo arreglo, como en

array [O.. 91 o£ array [Color] of integer;

o una versión más breve en la cual los conjuntos de índice se enumeran juntos:

, . ,.; array ~0..9,Color] o£ integer;

.... ,..~: -.;< ,.p . ,.

La subindización en el primer caso aparece como a [l] [red], mientras que en el s do caso escribimos a [ 1, redl . Un problema con los subíndices múltiples es que la se

'S cia de valores se puede organizar de diferentes maneras en la memoria, dependiendo de si : la indización se hace primero en el primer índice y después sobre el segundo índice, o al con- , trario. La indización sobre el primer índice produce una secuencia en la memoria p aa[O,red],a[l,red],a[2,redl, ..., a[9,red],a[O,bluel,a[l,bluel, y así sucesivamente (esto se denomina forma de columna mayor), mientras que la in ción en el segundo índice produce el orden en la memoria' de a [O, redl, a [O, bl a[O,green],a[l,redl,a[l,bluel,a[l,greenl,. . . , y asísucesivamente(est se denomina forma de renglón mayor), Si la versión repetida de la declaración de arre multidimensional va a ser equivalente a la versión abreviada (es decir, a [ 0, red] = [red] ), entonces se debe utilizar la forma de renglón mayor, ya que los índices difer se pueden agregar por separado: a [O] debe tener tipo array [Color] of inte debe hacer referencia a una pomión contigua de memoria El lenguaje FORTRAN, en no hay indización parcial de arreglos multidimensionales, se ha implementado tradicio mente utilizando la forma de columna mayor.

? En ocasiones un lenguaje permitirá el uso de un arreglo cuyo intervalo de índices no es-

té especificado. Un arrelo de indización abierta es especialmente útil en la declaración - de parámetros de arreglo para funciones, de manera que la función pueda manejar arreglos de di- ferentes tamaños. Por ejemplo, se puede usar la declaración en C

void sort (int a[], int first, int last)

para definir un procedimiento de clasificación que funcionará en cualquier arreglo de ta- maño a. (Por supuesto, Fe debe emplear algún método para determinar el tamaño real en el momento de la llamada. En este ejemplo otros parámetros lo hacen.)

Page 321: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tipos de datos y verificacibn de tipos 317

Record (Registro) Un constructor de tipo record o structure ("estructura") toma una lista de nombres y tipos asociados y construye un nuevo tipo, como en el ejemplo en C

S t N C t

í double r; i n t i;

1

Los registros difieren de los arreglos en que se pueden combinar los componentes de diferen- tes tipos (en un arreglo todos los componentes tienen el mismo tipo) y en que se utilizan nom- bres (más que índices) para tener acceso a los diferentes componentes. Los valores de un tipo "registro" corresponden aproximadamente al producto cartesiano de los valores de sus tipos componentes, sólo que se utilizan nombres en vez de posiciones para tener acceso a los com- ponentes. Por ejemplo, el registro previamente dado corresponde aproximadamente al pro- ducto cartesiano R X I, donde R es el conjunto correspondiente al tipo double e I es el conjunto correspondiente a in t . De manera más precisa, el registro dado corresponde al producto cartesiano ( r X R) X ( i X 0, donde los nombres r e i identifican los componen- tes. Estos nombres generalmente se utilizan con una notación punto para seleccionar sus componentes correspondientes en expresiones. De este modo, si x es una variable del tipo de registro dado, entonces x. r se refiere a su primer componente y x. i al segundo.

Algunos lenguajes tienen constructores de tipo de producto cartesiano puros. Un len- guaje de esta clase es ML, donde i n t * r e a l es la notación para el producto cartesiano de los números enteros y los reales. Los valores del tipo i n t * r e a l se escriben como tuplas, tales como (2,3.14 ) , y se tiene acceso a los componentes mediante funciones de proyección f at y snd (de las palabras en inglésfirst y second): f st ( 2 , 3.14) = 2 y snd(2, 3.14) = 3.14.

El método de implementación estándar para un registro o producto cartesiano consiste en asignar memoria de manera secuencial, con un bloque de memoria para cada tipo de componente. Así, si un número real requiere cuatro bytes y un entero dos bytes, entonces la estructura del registro dado anteriormente necesita seis bytes de almacenamiento, que se asignan como se muestra a continuación:

(4 bytes) 1 (2 bytes) 1

Union (Unión) Un tipo unión corresponde a la operación de unión de conjuntos. Está dispo- nible directamente en C por medio de la declaración un ion , por ejemplo la declaración

union

í double r; i n t i;

1

la cual define un tipo que es la unión de los números reales y los números enteros. Estricta- mente hablando, ésta es una unión disjunta, porque cada valor se visualiza como un real o como un entero, pero nunca como ambos. La interpretación que se desea se aclara median- te el nombre del componente utilizado para tener acceso al valor: si x es una variable del ti- po unión dado, entonces x. r significa el valor de x como un número real, mientras que x. i significa el valor de x como un entero. Matemáticamente esta unión se define escri- biendo (r X R) u ( i X 4.

Page 322: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

grintf (''%d",x.i);

CAP. 6 1 ANALISIS SEMANTICO

El método de implementación estkdar para una unión es asignar La memoria e lo para cada componente, de manera que la memoria para cada tipo del componen perponga con todas las demás. Así, si un número real requiere de cuatro bytes, y u de dos bytes, entonces la estructura de unión previamente dada necesita sólo cuatro b almacenamiento (el máximo de los requerimientos de memoria de sus componentes) ta memoria se asigna de la manera siguiente:

r (4 bytes) i ( 2 bytes) 1

Una implementación así requiere, de hecho, que la uni6n se interprete como una junta, puesto que la representación de un entero no concordará con su correspo presentación como uno real. En realidad, sin algún medio para que un programador ga los valores, una unión así conduce a errores en la interpretación de los datos, y tam proporciona una forma para burlar a un verificador.de tipo. Por ejemplo en C, si x e variable que tiene el tipo unión dado, entodcesd escribir

x.r = 2.0;

no se provocará un error de compilación, pero en lugar de que se imprima el valor 2, s pnmirá un valor erróneo.

Esta inseguridad en los tipos unión ha sido abordada por muchos diseños de le jes diferentes. En Pascal, por ejemplo, un tipo unión se declara utilizando el denomi registro variante, en el cual los valores de un tipo ordinal se registran en un compon discriminante, el cual se utiliza para distinguir los valores deseados. De este modo, e tenor tipo unión se puede escribir en Pascal como

record case isñeal: boolean o£

tme:(r: real);

false:(i: integer);

end;

Ahora una variable x de este tipo tiene tres componentes: x. isReal (un valor boolea x. r y x. i, y el campo isReal está asignado en un espacio separado que no se su pone en la memoria. Cuando se asigna un valor real, como x. r : = 2 .0 , también se hería, entonces, hacer la asignación x. isReal : = trua. En realidad, este mecanis es relativamente inútil (al menos para el uso det compilador en verificación de tipo) to que el discriminante se puede asignar por separado de los valores que se están minando. Efectivamente, Pascal permite que el componente discriminante (pero no el de sus valores para distinguir los casos) se omita eliminando su nombre en la decla

. I $

record case boolean of

tme:(r: real);

falae:(i: integer);

end;

lo que ya no provoca que se asigne espacio para el discriminante en la memoria. De es- ta forma, los compiladores de Pascal casi nunca intentan usar el discriminante como una verificación de legaiidad. Ada, por otra parte, tiene un mecanismo semejante, pero insiste

Page 323: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tipos de datos y verificación de tipos 319

en que, en cualquier momento que se asigne un componente de unión, se debe asignar el discriminante de manera simultánea con un valor que pueda ser verificado para asegurar la legalidad.

Mediante los lenguajes funcionales, como el ML, se adopta un enbque diferente. En este lenguaje, un tipo unión se define utilizando una barrü vertical para indicar unión, y dando nombres a cada componente para discriminarlos, como en

IsReal o£ real I IsInteger of int

Ahora también se deben emplear los nombres IsReal e IsInteger siempre que se uti- lice un valor de este tipo, como en (IsReal 2.0) o en (Iainteger 2). Los nombres IsReal e IsInteger se conocen como constructores de valor, porque ellos "constru- yen" los valores de este tipo. Puesto que siempre se deben utilizar cuando se hace la refe- rencia a valores de este tipo, no puede ocurrir ningún error de interpretación.

Pointer (Apuntador) Un tipo pointer o apuntador se compone de valores que son referencias a valores de otro tipo. De esta forma, un vdor de un tipo apuntador es una dirección de memo- ria cuya ubicación mantiene un valor de su tipo base. Los tipos apuntador se imaginan con frecuencia como tipos numéricos, porque se pueden realizar cálculos aritméticos en ellos, tales como desplazamientos de suma y multiplicación por factores de escala. No son en rea- lidad tipos simples, ya que se construyen a partir de tipos existentes mediante la aplicación de un constructor de tipo apuntador. Tampoco existe una operación de conjuntos estándar que corresponda directamente al constructor de tipo apuntador, del modo que el producto cartesiano corresponde al constructor de registro. De este modo, los tipos apuntador ocupan una posición algo especial en un sistema de tipos.

En Pascal el carácter A corresponde al constructor de tipo apuntador, de manera que la expresión de tipo "integer significa "apuntador a un entero". En C, una expresión de tipo equivalente es int *. La operación básica estándar en un valor de tipo apuntador es la operación de desreferenciación. Por ejemplo, en Pascal, el A representa el opera- dor de desreferenciación (además del constructor de tipo apuntador). Y si g es una variable del tipo "integer, entonces g A es el valor desreferenciado de g y tiene tipo integer. Una regla similar se mantiene en C, donde * desreferencia a una variable apuntador y se escribe *p.

Los tipos apuntador son muy útiles para describir tipos recursivos, los cuales comenta- remos en breve. Al hacerlo así, el constructor de tipo apuntador se aplica m& frecuentemente a tipos de registro.

Los tipos apuntador reciben espacio de asignación sobre la base del tamaño de dirección de la máquina objetivo. Por lo regular, éste es de cuatro, o en ocasiones, ocho bytes. A veces, una arquitectura de máquina obligará a un esquema de asignación más complejo. En PC basadas en DOS, por ejemplo, se haee una distinción entre apuntadores near (para direcciones dentro de un segmento, con un tamaño de dos bytes) y apuntadores far (direc- ciones que pueden estar fuera de un segmento simple, con un tamaño de cuatro bytes).

Function (funcih) Ya advertimos que un arreglo se puede ver conlo una función de su con- junto de índicer hacia su ~nnjunto de componentes. Muchos lenguajes (pero no Pascal o Ada) tienen una capacidad más general para describir tipos función. Por ejemplo, en Modu- la-2 la declaraciiin

VAR f: PROCEDURE (INTEGER): INTEGER;

declara la variable f corno de tipo funci0ti (o procedimiento), con un parámetro entero y produciendo un resultado entero. En noracih matemática este conjunto se describiría coino

Page 324: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 ANALISIS IEMANTICO

+'f#

el conjunto de funciones { j I -+ I ] , donde I es el conjunto de los enteros. En el lengu&$$ --. .>'",..

je ML este mismo tipo se escribiríacomo int -> int. El lenguaje C también tiene.&@$ ..,;.:y

pos función, pero se deben escribir como "apuntador a función", en una notación algo te@$ . , $& pe. Por ejemplo, la declaración de Modula-2 que acabamos de dar en C se debe escnbiG& como

int (*f) (int);

Se asigna espacio a los tipos función de acuerdo con el tamaño de dirección de la m na objetivo. Dependiendo del lenguaje y la organización del ambiente en tiempo de ej&&@# ción, un tipo función puede necesitar tener espacio asignado a un apuntador de c<idigo s6&8 (apuntando al código que implementa la función) o a un apuntador de código y un a de ambiente (apuntando a una ubicación en el ambiente en tiempo de ejecución). El apuntador de ambiente se comentará en el capítulo siguiente.

Class (Clae) La mayoría de los lenguajes orientados a objetos tienen una declaraci semejante a una declaracián de registro, excepto porque incluye la definición de nes, denominadas mbtodos o funciones miembro. Las declaraciones de clase pu crear nuevos tipos en un lenguaje orientado a objetos (en C+ + lo hacen). Incluso si és caso, las declaraciones de clase no son únicamente tipos, porque permiten el uso de car rísticas que están más allá del sistema de tipos, tales como la herencia y el enlace din Estas iíltimas propiedades deben ser mantenidas por estructuras de datos separada$ tal mo la jerarquía de clase (una gráfica acíclica dirigida), la cual implementa la herenc la tabla de método virtual, la cual implementa el enlace dinámico. Volveremos a habl estas estructuras en el capítulo siguiente.

6.42 Nombres de tipos, declaraciones de tipo y tipos recunivos

Los lenguajes que tienen un rico conjunto de constructores de tipo por lo regular tam tienen un mecanismo para que un programador asigne nombres a expresiones de tipo. T declaraciones de tipo (en ocasiones también conocidas como definiciones de tipo) i cluyen el mecanismo typedef de C y las definiciones de tipo de Pascal. Por ejemplo, código en C

typeaef struct

(: double ri int i r

) RealIntRec;

define el nombre RealIntRec como un nombre pan el tipo de registro construido por la expresión de tipo struct que lo precede. En el lenguaje ML, una declaración similar (pero sin los nombres de campo) es la siguiente:

type RealIntRec = realeint;

10. En algunos lenguajes, como en C+ t, la herencia se refleja o se duplica en el sistema de tipos, puesto que las subclases se visualizan como subtipos (un tipo S es iin subtipo del tipo T si todos sus valores se pueden contemplar como v:ilores de T o, en terminología de conjuntos, si S c T).

Page 325: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tipos de datos y verificación de tipos 32 1

El lenguaje C tiene un mecanismo de nombrado de tipos adicional en el que un nombre se puede asociar directamente con un constructor de struct o union sin utilizar una typedef directamente. Por ejemplo, el código en C

struct RealIntRec

i double r; int ii

).;

también declara un nombre de tipo RealIntRec, pero se debe usar con el nombre de cons- tructor struct en declaraciones de variables:

struct RealIntRec x; / * declara x como una variable de tipo RealIntRec * /

Las declaraciones de tipo ocasionan que se introúuzcan los nombres de tipo declarados en la tabla de símbolos de la misma manera que las declaraciones de variable determinan que se introduzcan los nombres de variables. Una cuestión que se presenta aquí es si se pueden o no volver a utilizar los nombres de tipo como nombres de variable. Por lo regular no se puede (excepto cuando se permite mediante las reglas de anidación de ámbito). El lenguaje C tiene una pequeña excepción a ebta regla, ya que los nombres asociados con las declara- ciones struct o union se pueden colver a utilizar como nombres typedef:

struct RealIntRec

{ double r;

int i;

1; typedef struct RealIntRec RealIntRec;

/ * ;es una declaración legal! * /

Esto se puede lograr considerando que el nombre de tipo introducido mediante la declara- ción struct es la cadena completa "struct RealIntRec", la cual es diferente del nombre de tipo RealIntRec introducido mediante la typedef.

Los nombres de tipo están asociados con atributos en la tabla de símbolos en una forma similar a las declaraciones de variable. Esos atributos incluyen el ámbito (el cual puede ser inherente en la estructura de la tabla de símbolos) y la expresión de tipo correspondiente al nombre de tipo. Como los nombres de tipo pueden aparecer en expresiones de tipo, se presen- tan cuestiones acerca del uso recursivo de los nombres de tipo que son similares a las defi- niciones recursivas de las funciones analizadas en la sección anterior. Estos tipos de datos recursivos son muy importantes en los lenguajes de programación modernos e incluyen listas, árboles y muchas otras estructuras.

Los lenguajes se dividen en dos grupos generales en su manejo de los tipos recursivos. El primer grupo se compone de aquellos lenguajes que permiten el uso directo de la recur- sividad en declaraciones de tipo. Un lenguaje de esta clase es ML. En ML, por ejemplo, un &bol de búsqueda binaria que contiene enteros se puede declarar como

datatype intBST = Ni1 I Node o£ int*intBST*intBST

Esto se puede ver como una definición de intBST, o bien, como la unión del valor Ni1 con el producto cartesiano tle los enteros con dos copias (le intBST mimo (una para el

Page 326: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

subárbol izquierdo y una para el subárbol derecho). Una declaración equivalente en Forma ligeramente alterada) es

struct intBST

{ int isNull;

int val;

struct intBST left,right;

Esta declaración, sin embargo, generará un mensaje de error en C, el cual es causado uso recursivo del nombre del tipo intBST. El problema es que estas declaraciones n terminan el tamaño de la memoria necesaria para asignar una variable de tipo intBS clase de lenguajes, como ML, que pueden aceptar tales declaraciones son aquellos q necesitan tal información antes de la ejecución, y ésos son los lenguajes que proporci un mecanismo de asignación y desasignación de memoria automático general. Tales dades para la, administración de memoria son parte del ambiente en tiempo de ejecuc se comentan en el capítulo siguiente. C no tiene un mecanismo de esta naturaleza, ento - debe hacer tales declaraciones de tipo recursivo ilegales. C es representativo del seg grupo de lenguajes: aquellos que no permiten el uso directo de la recursividad en decl ciones de tipo.

La solución para tales lenguajes es permitir la recursividad sólo indirectamen través de apuntadores. Una declaración correcta para intBST en C es

struct intBST

( int val;

struct intBST *left,*right;

1; typedef struct intBST * intBST;

typedef struct intBST * intBST; struct intBST

{ int val;

intBST 1eft.right;

1;

(En C, las declaraciones recursivas requieren del uso de la forma struct o union para la declaración del nombre de tipo recursivo.) En estas declaraciones el tamaño de la memo- ria de cada tipo se calcula directamente por el compilador, pero el espacio para los valores se debe asignar manualmente por el programador a través del uso de procedimientos de asig- nación tales como malloc.

6.4.3 Equivalencia de tipos Dadas las posibles expresiones de tipo de un lenguaje, un verificador de tipo con frecuencia drbe responder la cuestión de cuándo dos expresiones de tipo representan al mismo tipo. És- ta es la cuestión de la equivalencia de tipos. Existen muchas maneras posibles para que la equivalencia de tipos sea definida por un lenguaje. En esta sección comentaremos breve- mente sólo las formas más comunes de equivalencia. En este análisis representamos la equi- valencia de tipos como sería en un analizador semántica de compilador, a saber. como una función

Page 327: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tipos de datos y verificación de tipos

function t yeEqual ( r l , t2 : TypeExp ) : Boolmn;

que toma dos expresiones de tipo y devuelve verrkdero si representan el mismo tipo de acuerdo con las reglas de equivalencia de tipo del lenguaje yj¿t/.so si no es así. Daremos va- rias descripciones en pseudocódigo diferentes de esta función para distintos algoritinos de equivalencia de tipos.

Una cuestion que se relaciona directamente con la descripcitín de los algoritmos de equi- valencia de tipos es la manera en que se representan las expresiones de tipo dentro de un compilador. Un inbtodo directo es utilizar una representación de árbol sintáctico, ya que es- to facilita traducir directamente desde la sintaxis en declaraciones hasta la representación in- tema de los tipos. Para tener un ejemplo concreto de tales representaciones considere la gra- mática para expresiones de tipo y declaraciones de variable dadas en la figura 6.19. Allí aparecen versiones simples de muchas de las estructuras de tipo que hemos analizado. Sin embargo, no hay declaraciones de tipo que permitan la asociación de nuevos norribres de ti- po a expresiones de tipo (por lo tanto, no son posibles los tipos recursivos, a pesar de la pre- sencia de tipos apuntador). En esa figura querernos describir una posible estructura de árbol sintáctico para expresiones de tipo correspondiente a las reglas gramaticales.

Considcre, en primer lugar, la expresión de tipo

record

x : gointer t o rea l ;

y: a r ray [lo] of i n t

end

Esta expresión de tipo se puede representar mediante el árbol sintáctico

Aquí los nodos hijos del registro están representados corno una lista de hermanos, puesto que el número de componentes de un registro es arbitrario. Advierta que los nodos que re- presentan tipos simples forman las hojas del árbol.

Figura 6.19 vur-decls -, vur-rlecls ; vcir-cled / vczr-clecl

Una gramatica simple pan iar-decl -+ i d : g p e - a p

expresiones de tipo type-exp -+ simpie-<v;oe 1 structured-type simple-&!?e -+ int / bool 1 real / char 1 void crruclured-íype - array inuml of rype-erp /

record inr-t1ecl.s end 1 union wr-clecls end pointer to iypc-e.rp j proc ( l'pe-r.rpl,s ) Iyx-o-ql

Page 328: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

record

x: pointer to real;

y: array [lo] of int

end

en vez de eso debemos escribir

tl = pointer to real;

t2 = array [lo1 of int;

t3 = record

x: tl:

y: t2

end

CAP. 6 1 ANALISIS SEMAN'TIEO

De manera similar, la expresión de tipo

groc(boo1,union a:real; b:char end,int):void

se puede representar mediante el árbol sintáctico * real char

Advierta que los tipos de parámetro también están dados como una lista de hermanos, tras que el tipo de resultado (en este ejemplo void) se distingue haciéndolo direct un hijo de proc.

La primera clase de equivalencia de tipo que describimos, y la única disponible en sencia de nombres para tipos, es la equivalencia estructural. En esta vista de equiva dos tipos son los mismos si y sólo si tienen la misma estructura. Si los árboles sintá emplean para representar tipos, esta regla nos dice que dos tipos son equivalentes si si tienen árboles sintácticos que sean idénticos en estructura. Como un ejemplo de c equivalencia estructural se verifica en la práctica, la figura 6.20 proporciona una descri en pseudocódigo de la función typeEqual para la equivalencia estructural de dos expre nes de tipo dadas por la gramática de la figura 6.19, utilizando la estructura de árbol sin tico que acabamos de describir.

Observamos del pseudocódigo de la figura 6.20 que esta versión de equivalencia es tural implica que dos arreglos no son equivalentes a menos que tengan el mismo tamañ tipo de componente, y dos registros no son equivalentes a menos que tengan los mism componentes con Los mismos nombres y en el mismo orden. Es posible hacer diferentes lecciones en un algoritmo de equivalencia estructural. Por ejemplo, el tamaño del m e g se podría ignorar en la determinación de la equivalencia, y también se podda permitir q los componentes de una estructura o unión se presenten en un orden diferente.

Una equivalencia de tipos mucho más restrictiva se puede definir cuando se pued declarar nuevos nombres de tipo para expresiones de tipo en una declaración de tipo. En figura 6.21 modificamos la gramática de la figura 6.19 para incluir declaraciones de tipo también restringimos declaraciones de variable y subexpresiones de tipo a tipos simpl y nombres de tipo. Con estas declaraciones ya no se puede escribir

Page 329: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

pleudo~Ódig~ para una función iypeEqual que pnieba la equivalencia

de t~po de la gramática de la figura 6.19

Tipos de datos y verificación de tipos

function t)?peEquul( 11.12 : TypeExp ) : Boolean; var temp : Booleun ;

pl, p2 : TypeExp ; begin

if t l y t2 son de tipo sirnple then return tl = t2 else if t1.clase = urreglo and t2.clase = arreglo then

return tl.tamaño = t2.tamaño and typeEquul( tl.hijol, t2.hijol ) else if tl.rlase = registro and t2.clase = registro

or tl.clase = unión and t2.clase = unión then hegin

pl := tl.hijol ; p2 := t2.hijol ; temp : = verdadero ; while temp and pl # ni1 and p2 # ni1 do

if pl.nombt-e # p2.nombt-e then temp : = falso

else if not typeEqual ( pl.hijol , p2.hijol ) then temp : = falso else hegin

p1 := pl.hermano ; p2 : = p2. hermano ;

end; return temp and pl = ni1 and p2 = ni1 ;

end else if tl.cluse = apuntador and r2.clase = apuntador then

return typeEquul ( tl.hijol ,t2.hijol ) else if tl.clase = proc and t2.cluse = proc then begin

pl := t1.hijol ; p2 := t2.hijol ; temp : = verdadero ; while temp and pl # ni1 and p2 # ni1 do

if not typeEqual (pl.hijo1, p2.hijol )

then tenzp : = falso else begin

pl : = pl.hemtuno ; p2 : = p2. hermano ;

end; return temp and pl = ni1 and p2 = ni1

and iypeEqual( tl.hijo2, t2.hijo2 ) end else return falso ;

end ; (* typeEqual *)

Page 330: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Fipurt 6.21 Expresiones de tipo con declaraciones de tipo

CAP. 6 1 ANALISIS SEH~NTICO

var-decls -t vnr-ciecls ; var-decl / vnr-decl var-decl -+ id : simple-type-exp type-decls -+ type-decls ; type-decl / type-decl type-decl-+ id = type-exp iype-exp -+ simple-type-exp / structured-type sirtple-type-exp -+ simple-type / id simple-iype -+ int / bool / real / char / void structnr-ed-type -+ array [num] of simple-iype-exp /

record var-decls end / union var-decls end / pointer to .simple-type-exp / proc ( type-exps ) simple-tye-e.wp

type-exps -+ type-exps , simple-iype-exp 1 ,simple-tyl~e-exp

Ahora podemos definir la equivalencia de tipos basados en nombres de tipo, una f o m equivalencia de tipos que se denomina equivalencia de nombre: dos expresiones de son equivalentes si y sólo si son el mismo tipo simple o el mismo nombre de tipo. És una clase muy fuerte de equivalencia de tipo, puesto que ahora, dadas las declaracione tipo

tl = int; t2 = int

tos tipos tl y t2 no son equivalentes (porque tienen nombres diferentes) y tampoco s equivalentes respecto a int. La equivalencia de nombre pura es muy fácil de implement ya que una función typeEqual se puede escribir en unas cuantas líneas:

function typeEqual ( t l , t2 : TypeExp ) : Boolenn; var temp : Boolean ;

pl , p2 : TypeExp ; begin

i f t l y t2 son de tipo simple then return t i = t2

else i f t l y t2 son nombres de tipo then return t l = t2

else return$zlse ; eud;

Naturalmente, las expresiones de tipo reales correspondientes a nombres de tipo se deben introducir en la tabla de símbolos para permitir el cálculo posterior del tamaño de memoria para asignación de almacenamiento y para verificar la validez de operaciones tales como la desreferenciación de apuntador y selección de componente.

Una complicación en la equivalencia de nombre es cuando otras expresiones de tipo además de los tipos simples o nombres de tipo, continúan permitiéndose en declaraciones de variable, o como subexpresiones de expresiones tipo. En tales casos una expresión de tipo puede no tener un noinbre explícito asignado, y un compilador tendrá que generar un iiombre interno para la expresión de tipo que es diferente a cuülquicr otro nombre. Por cjem- plo, dadas las declaraciones de variable

Page 331: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tipos de datos y verificación de tipos

x: array [lo1 of int:

y: array [lo1 o£ int;

las variables x y y tienen asignados nombres de tipo diferentes (y únicos) correspondientes a la expresión de tipo array t 101 of in t .

Es posible conservar la equivalencia estructural en presencia de nombres de tipo. En este caso, cnsndo se encnentra un nombre, su expresión de tipo correspondiente debe obte- nerse de la tabla de símbolos. Esto se puede llevar a cabo agregando el caso siguiente al ctídigo de la figura 6.20,

else if t l y t2 son nowzbres de tipo then return @prEquat(getTvl>~~E.~pítl), getTvpeExp(t2))

donde jietTyptErp es una operación de tabla de símbolos que devuelve la estructura de ex- presión de tipo asociada con su parimetro (que debe ser un nombre de tipo). Esto requiere que se inserte cada nombre de tipo en la tabla de símbolos con una expresión de tipo que re- presente su estructura, o cuai~do menos aquellas cadenas de nombres de tipo resultantes de declaraciones de tipo tales como

t2 = tl;

conduzcan finalmente de regreso a una estructura de tipo en la tabla de sí~ribolos. Debe tenerse cuidado al implementar la equivalencia estructural cuando sean posibles

referencias de tipo recursivos, porque el algoritmo que se acaba de describir puede resultar en un bucle infinito. Esto se puede evitar modificando el coniportamiento de la llamada t.yppeEquul(t1 t2) en el caso en que t l y t2 son nombres de tipo para incluir la suposición de que ya son potencialmente iguales. Entonces, si la función ~peEqual siempre regresa a la misma llamada, se puede declarar un éxito en ese punto. Considere, por ejemplo, las decla- raciones de tipo

ti = record x; int;

t: pointer to t2;

end;

t2 = record

x: int;

t: pointer to tl;

end ;

Dada la Ilamada typeEqwl(tl, t2), la función typeEqual supondrá que t l y 12 son potencial- mente iguales. Entonces se obtendrán las estructuras tanto de t l como de t2, y el algoritmo continuará con éxito hasta que se haga la llamada typeEq~~al(t¿>, t l ) para analizar los tipos hijos de las declaraciones de apuntador. Entonces esta llamada inmediatamente devolvertí true, debido a la suposición de igualdad potencial que se hizo en la llamada original. En ge- neral, este algoritmo necesitará efectuar suposiciones sucesivas de que los pares de nonrbres de tipo pueden ser iguales y ncumular las suposiciones en una lista. Finalmente, por supues- to. el algoritmo debe obtener ixito o fallar (es decir, no puede entrar en un bucle infinito), ~xrqw existe scílo un número Iiriito de nombres de tipo en cualquier programa dado. Dejamos los detalles de las tnodificaciones al pseudocódigo de tjpeEqiud para un ejercicio (véase tamhiin la seccitín de notas y rel'erenci:~~).

Page 332: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 ANALBIS SEMANTICO

Una variación Final acerca de la equivalencia de tipos es una versión menor de I valencia de nombres utilizada por Pascal y C, denominada equivalencia de declaraci este tipo de método, las declaraciones del tipo siguiente

t2 = tl;

se interpretan como las que establecen alias de tipo, en vez de nuevos tipos (como equivalencia de nombre). De este modo, dadas las declaraciones

tl = int; ta = int

tanto tl como t 2 son equivalentes a i n t (es decir, son sólo alias para el nombre del ti i n t ) . En esta versión de equivalencia de tipo, todo nombre de tipo es equivalente a nombre de tipo base, el cual es, o bien un tipo predefinido, o está dado mediante u presión de tipo resultante de la aplicación de un constructor de tipo. Por ejemplo, dad declaraciones

tl = array [lo] of int; t2 = array [lo] o£ int; t3 = tl;

los nombres de tipo t 1 y t 3 son equivalentes bajo la equivalencia de declaración, pero n guno es equivalente a t 2 .

Para implementar la equivalencia de declaración, debe proporcionarse una nueva o ración getBaseTypeName mediante la tabla de símbolos, la cual obtiene el nombre de t base en vez de la efpresi6n de tipo asociada. Un nombre tipo puede distinguirse en la ta de símbolos como un nombre de tipo base si es un tipo predefinido o si está dado por expresión de tipo que no sea simplemente otro nombre de tipo. Observe que la equiva de declaración, similar a la equivalencia de nombre, resuelve el problema de bucles 1

tos en la verificación de tipos recursivos, puesto que dos nombres de tipo base sólo pue ser equivalentes en declaración si son el mismo nombre.

Pascal utiliza de manera uniforme la equivalencia de declaración, mientras que C e plea equivalencia de declaración para estructuras y uniones, pero equivalencia estructur para apuntadores y arreglos.

Ocasionalmente, un lenguaje ofrecerá una selección de equivalencia estructural, declaración o de nombre, donde se utilizan diferentes declaraciones de tipo para diferen formas de equivalencia. Por ejemplo, el lenguaje ML permite que los nombres de tipo se claren como alias utilizando la palabra reservada t y p e , por ejemplo en la declaración

type RealIntRec = real*int;

Esto declara RealIntRec como un alias del tipo de producto cartesiano r e a l * i n t . Por otra parte, la declaración datatype crea un tipo completamente nuevo, como la declaración

datatype intBST = Ni1 I Noae of int*intBST*intBST

Advierta que una declaración d a t a t y p e también debe contener nombres de constructor de valores (Ni1 y Node en la declaración dada), a diferencia de una declaración t y p e . Esto

Page 333: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tipos de datos y veriticacidn de tipos 3 29

hace los valores del nuevo tipo distinguibles de los valores de tipos existentes. De este mo- do, dada la declaración

datatype NewRealInt = Prod of real*int;

el valor (2.7,10) es del tipo RealIntRec o real*int, mientras que e1 valor Prod(2.7,lO) es del tipo NewReallnt (pero no real*int).

6.4.4 Inferencia de tipo y verificación de tipo Procederemos ahora con la descripción de un verificador de tipo para un lenguaje simple en términos de acciones semánticas, basadas en una representación de tipos y una operación iypeEquu1 como se comentó en sesiones anteriores. El lenguaje que usamos tiene la gramá- tica dada en la figura 6.22, la cual incluye un pequeño subconjunto de las expresiones de tipo de la figura 6.19, con un pequeño número de expresiones y senteiicias agregadas. También suponemos la disponibilidad de una tabla de símbolos que contenga nombres de vliriable y tipos asociados, con operaciones de inserción (insert), las cuales insertan un nombre y un tipo en la tabla, y de búsqueda (lookup), que devuelve el tipo asociado de un nombre. No especificaremos las propiedades de estas operaciones en sí mismas en la gra- mática con atributos. Analizamos las reglas de verificación de tipo y la inferencia de tipo para cada clase de construcción de lenguaje de manera individual. La lista completa de acciones semánticas se proporciona en la tabla 6.10 (página 330). Estas acciones no e s t h dadas en forma de gramitica con atributos pura, y utilizamos el símbolo := en vez del de igualdad en las reglas de la tabla 6.10 para indicar esto.

Figura 6.22 Una gramática simple para programa -+ var-decls ; senis

ilustrar la venficauán de vur-decls 4 var-rlecls ; vur-decl / var-decl

tipos var-deel-+ id : type-uxp type-exp -+ int / bool / array [numl of iype-exp sents -+ sents ; sent 1 sent sent 4 if exp then sent / id : = exp

Derlanc;ones Las declaraciones causan que el tipo de un identificador se introduzca en la ta- bla de símbolos. De este modo, la regla gramatical

tiene la acción semántica asociada

que inserta un identificador en Id tdbla de símbolos y asocia un tipo a1 mismo. El tipo aso- ciado en esta inserción se construye de acuerdo con las reglas gramaticales para iype-exp.

Page 334: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 ANALISIS SEHANTICO

Tabla 6.10 Gramática con atributos Regla gramatical Reglas sem&nticas para veriíícación de tipos

vur-decl-. i d : type-e.rp insen(id .name, type-exptype) de la gramatica simple de

iype-exp -i i n t ype-e.rp.tye := integer la figura 6.12

tvpe-exp -+ bool type-exp.type := boolean

type-e.rp, -+ ar ray type-exp, sype := [numl of type-e.rp2 makeTypeNode(urrav, num sire, ..,. , .

ype-exp2 .ype) ~..,. ,,.. ! .; :p x.5 ; ....,

seut -+ i f esp then sent if not typeEquul(exp.type, boolean) .:S . .,. : , ;..,.* ;

then type-errotfscnt) . , ~ . ~~:.: ,.,. , , . .~, ,'; . ~ . . ,. sent -+ i d : = exp if not typeEqual(lookup(id .nome), ...S ...~ . .

exp.type) then type-errorfsent)

erp, -+ exp, + exp3 if not (iypeEqualfexp, .rype, integer) and typeEquulfexp3 .ype, integer))

then type-error(expl) ; exp, .type := integer

e.rp, -+ exp, o r e.rp3 if not (typeEquul(exp2 .type, booleun) and typeEqual(exp, .type, boolean))

then type-errotfexp,) ; exp, .ype : = booleun

exp 1 -t expz i exp3 1 if isArruyType(uxp2 . type) and typeEquul(exp, .type, integer)

then expl . type : = exp2 .fype.childl else type-error(e.rpI)

r.rn -+ nwn exn.me := inteper

iirp -+ t r u e erp.type := boolean

exn -+ f a l s e e.ia.n.oe : = boolean

exp -+ i d exp.type := lookup(id .finme)

Se supone que los tipos se mantienen como alguna clase de estructura de árbol, de mane que el tipo estructurado array en la gramatica de la figura 6.22 corresponde a la acc' semántica

makeType&de(array size, type)

la cual construye un nodo de tipo

donde el hijo del nodo iway (arreglo) es cl árbol de tipo dado por el parámctro Vpe. Se su- pone que los tipos simples itltejier y hooleun se construyen como nodos de hoja estándar en la representación de árbol.

Sentencias Las sentencias no tienen tipos en sí mismas. sino subestructuras que es necesario verificar en cuanto a la exactitud de tipo. Situaciones rípicas se muestran mediante las dos

Page 335: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tipos de datos y veriiicacián de tipos 33 1

reglas de sentencias en la gramática simple, la sentencia if y la sentencia de asignación. En el caso de la sentencia if, la expresión condicional debe tener tipo booleano. Esto se indica mediante la regla

if not typeEyuul(exp.tpe, boolean) then type-errar (sent)

donde fype-error indica un mecanismo de informe de error cuyo comportamiento se descri- birá en breve.

En el caso de la sentencia de asignación, el requerimiento es que la variable que se está asignando debe tener el mismo tipo que la expresión ciiyo valor está por recibir. Esto depen- de del algoritmo de equivalencia de tipos, como se expresó mediante la función typeEqua1.

Expresiones Las expresiones constantes, tales como números, y los valores booleanos t rue y f alse, tienen tipos implícitamente definidos integer y boolean. Los nombres de variable tienen sus tipos determinados mediante una búsqueda en la tabla de símbolos. Otras ex- presiones se forman mediante operadores, tales como el operador aritmético +, el operador booleano or y el operador de subíndice [ 1. En cada caso las subexpresiones deben ser del tipo correcto para la operación indicada. En el caso de los subíndices, esto se indica mediante la regla

if isArrayType(exp2. type) and typeEqual(e.rp.,.type, integer)

then e.rp,.type := e-~p~.type.childl else iype-errotfexp,)

Aquí la función isArrayType prueba que su parhetro es un tipo arreglo ("array"), es decir, que la representación de árbol del tipo tenga un nodo raíz que represente el constructor de tipo arreglo. El tipo de la expresión de subíndice resultante es el tipo base del arreglo, el cual es el tipo representado por el (primer) hijo "child" del nodo raíz en la representación de ár- bol de un tipo arreglo, y esto se indica escribiendo exp,.type.childl.

El resto describe el comportamiento de un verificador de tipo así en la presencia de errores, como se indica mediante el procedimiento type-error en las reglas semánticas de la tabla 6.10. Las cuestiones principales son cuándo generar un mensaje de error y cómo continuar la verificación de tipos en la presencia de e m e s . No se debería generar un mensaje de error cada vez que ocurre un error de tipo; de otro modo, un error simple podría causar que se im- primiera una cascada de muchos errores (algo que también puede pasar con errores de aná- lisis sintáctico). En su lugar, si el procedimiento type-error puede determinar que un error de tipo ya debe haber ocurrido en un lugar relacionado, entonces debería suprimirse la gene- ración de un mensaje de error. Esto se puede señalar teniendo un tipo de error interno especial (el cual se puede representar mediante un árbol de tipo nulo). Si OpeError encuentra este ti- po de error en una subestructura, entonces no se genera ningún mensaje de error. Al mismo tiempo, si un error de tipo también significa que no se puede determinar el tipo de una cstructura, entonces el verificador de tipo puede usar el tipo de error como su (en realidad desconocido) tipo. Por ejemplo, en las reglas semánticas de la tabla 6.10, dada una expresión con subíndice expl --+ e.wp2 ie.xp3 1, si expz no es un tipo arreglo, entonces no se puede asig- nar a C A P , un tipo válido, y no hay asignación de un tipo en las acciones semánticas. Esto supone que los campos de tipo se han inicializado para algún tipo de error. Por otra parte, en el caso de las operaciones + y or, incluso en la presencia de un error de tipo, puede hacer- se la suposición de que cl resultado que es significativo es de tipo entero o booleano, y las reglas de la tabla 6.10 utilizan ese hecho para asignar un tipo al resultado.

Page 336: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 / ANÁLISIS S E ~ A X T I C O

6.4.5 Temas adicionales en la verificación de tipos En esta subsecciún comentaremos brevemente algunas de las extensiones comunes p algoritmos de verificación de tipo que hemos analizado.

lobrecarga Un operador está sobrecargado si se utiliza el mismo nombre de operad dos operaciones diferentes. Un ejemplo común de sobrecarga es el caso de los ope aritméticos, que con frecuencia representan operaciones sobre valores numéricos difere Por ejemplo, 2+3 representa la suma entera, mientras que 2 .1+3 . O representa la de punto flotante, que debe ser implementada de manera interna mediante una instru conjunto de instrucciones diferente. Una sobrecarga así se puede extender a procedi y funciones definidos por el usuario, donde se puede emplear el mismo nombre para ope nes relacionadas, pero definido para parámetros de tipos diferentes. Por ejemplo, po desear definir un procedimiento de valor máximo para valores tanto enteros como re

procedure max íx,y: inteser): integer;

procedure max (x,y: real): real;

En Pascal y C esto es ilegal, ya que representa una declaración repetida del mismo no dentro del mismo ámbito. Sin embargo, en Ada y Cf +, una declaración así es legal, po el verificador de tipo puede decidir cual procedimiento max es significativo con base e tipos de los parámetros. Un uso así de verificación de tipo para eliminar la atnbigüed significados múltiples de nombres puede implementarse de varias maneras, dependi de las reglas del lenguaje. Una manera seda aumentar el procedimiento de búsqueda d tabla de símbolos con parámetros de tipo, permitiendo que la tabla de símbolos encuen la coincidencia correcta. Una solución diferente para la tabla de símbolos es mantener c juntos de tipos posibles para los nombres y devolver el conjunto al verificador de tipo p eliminar la ambigüedad. Esto es útil en situaciones más complejas, donde un tipo único puede ser determinado de inniediato.

Coerción y conver@n de tipor Una extensión común de las reglas de tipo de un lenguaje permitir expresiones aritméticas de tipo mezclado. tal como 2.1 + 3, donde se suma número real y un número entero. En tales casos, debe hallarse un tipo común que sea c patible con todos los tipos de las subexpresiones, y deben aplicarse operaciones para co vertir los valores en el tiempo de ejecución a las representaciones apropiadas antes de apli el operador. Por ejemplo, en la expresión 2 . 1 + 3, el valor entero 3 se debe convertir a p flotante antes de la suma, y la expresión resultante tendrá el tipo de punto flotante. Ex1 dos métodos que un lenguaje puede tomar para taies conversiones. Modula-2, por ejem requiere que e l programador suministre una función de conversión, de manera q ejemplo que se acaba de dar debe escribirse como 2 .1 + FLOAT (3 ) , o se causará un error de tipo. La otra posibilidad (utilizada en el lenguaje C) es que el verificador de tipo sumi- nistre la operación de conversión de manera automática, basándose en los tipos de las s bexpresiones. Una conversión automática de esta naturaleza se conoce como coerción. La coerción se puede expresar de manera implícita mediante el verificador de tipo, ya que el tipo inferido de una expresión cambia a partir de una subexpresión, como en

/ \,(diferencia en el tipo implica la conversión de 3 a 3.0)

Page 337: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tipos de datos y veriíicación de tipos 333

Esto requiere que un generador de código posterior examine los tipos de expresiones para determinar si se debe aplicar una conversión. De manera alternativa, la conversión se pue- de suministra de manera explícita mediante el verificador de tipo insertando un nodo de conversión en el árbol sintáctico, como en

Las coerciones y conversiones de tipo también se aplican a las asignaciones, tal como

en C. en donde, si r es de tipo d o u b l e e i es i n t , se sujeta el valor de i a d o u b l e an- tes de ser almacenado como el valor de r . Tales asignaciones pueden perder información durante la conversión, como podría ocurrir si la asignación se hiciera en la dirección opuesta (también legal en C):

Una situación similar ocurre en los lenguajes orientados a objetos, donde generalmente es permitida la asignación de objetos de subclases a objetos de superclases (con una correspon- diente pérdida de información). Por ejemplo, en C+ +, si A es una clase y B es una subcla- se, y si x está en A y además y está en B, entonces la asignación x=y está permitida, pero no lo contrario. (Esto se conoce como principio del subtipo.)

fipificado polimórfco Un lenguaje es polimórfico si permite que las construcciones de lengua- je tengan más de un tipo. Hasta ahora hemos analizado la verificación de tipo para un len- guaje que es esencialmente monomórfico debido a que todos los nombres y expresiones que se requieren tienen tipos únicos. Una relajación de este requerimiento de monomorfis- mo es la sobrecarga. Pero la sobrecarga, aunque es una forma de polimorfismo, se aplica só- lo a situaciones donde se realizan v¿uias declaraciones por separado al mismo nombre. Una situación diferente ocurre cuando sólo una declaración se puede aplicar a cualquier tipo. Por e,jemplo, un procedimiento que intercambia los valores de dos variables podría en principio aplicarse a variables de cualquier tipo (mientras que tengan el mismo tipo):

procedure swap (var x , y : anytme) ;

El tipo de este procedimiento swag (" in tercambio") se dice que está parametrizado mediante el tipo anytype ("cualquier t ipo") y anytype se considera una variable de tipo, la cual puede suponerse de cualquier tipo real. Podemos expresar el tipo de este pro- cedimiento como

procedure(vnr anytype, irnr nnytype): void

donde cada ocurrencia de cinyiype se refiere a1 mismo (pero no especificado) tipo. Un tipo así es en reialidad un patrón de tipo o esquema de tipo, más que un tipo real, y un verifica- dor de tipo t lek determinar un tipo real en toda situación donde se utilice swap que coincida con este patr6n de tipo « declarar un crror de tipo. Por ejemplo, dado cl c6ctigo

Page 338: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]
Page 339: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Un analizador semantico para el lenguaje TINY 335

Separaremos el anilisis del código para el analizador semántico TINY en dos partes. Primero comentaremos la estructura de la tabla de símbolos y sus operaciones asociadas. Luego describiremos la operación del analizador semántico mismo, incluyendo la construc- ción de la tabla de símbolos y la verificación de tipo.

6.51 Una tabla de sirnbolos para TINY En el diseño de la tabla de símbolos para el analizador semántico de TINY, primero de- bemos determinar qué información necesita mantenerse en la tabla. En casos típicos esta información incluye el tipo de datos y la información de ámbito. Como TINY no tiene in- formación de ámbito, y puesto que todas las variables tienen tipo de datos entero, una tabla de símbolos de TINY no necesita contener esta información. Sin embargo, durante la genera- ción del código, las variables necesitarán ser asignadas en ubicaciones de memoria y, pues- to que no hay declaraciones en el árbol sintáctico, la tabla de símbolos es el lugar lógico pa- ra almacenar estas ubicaciones. Por ahora, las ubicaciones se pueden visualizar simplemente corno índices enteros que se incrementan cada vez que se encuentra una nueva variable. Con el fin de hacer más interesante y más útil la tabla de símbolos, también la utilizamos para generar un listado de referencia cruzada de números de línea donde se tiene acceso a las variables.

Como un ejemplo de la información que se generará mediante la tabla de símbolos, con- sidere el programa muestra de TINY (con números de Iínea agregados):

{ Programa simple

en lenguaje TINV -- calcula el factorial

1 read x; f introduce un entero 1. if O c x then I no calcule si x <= O 1

fact := 1;

regeat

fact := fact * x; x : = x - 1

until x = O;

write fact { salida del factorial de x )

end

Después de la generación de la tabla de símbolos para este programa, el analizador se- mántica ofrecerá como $alida (con TraceAnalyze = TRUE) la 5iguiente información al archivo del listado:

Symbol table:

Variable Name Location Line Numbezs

x O 5 6 9 10 10 11 f act 1 7 9 9 12

Advierta que niírltiples referencias en la misma Iínea generan múltiples entradas para esa Iínea en la tabla de símbolos.

Page 340: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 AN~LISIS SEMANTICO

El código para la tabla de símbolos está contenido en los archivos symtab. h y tab. c listados en el apéndice B (líneas 11 50- 1179 y 1200- 1321, respectivamente).

La estructura que usamos para la tabla de símbolos es la tabla de dispersión enca da separadamente como se describió en la sección 6.3.1, y la función de dispersión es 1 se dio en la figura 6.13 (pigina 298). Como no hay infomacidn de &hito, no hay n dad de una operación de eliminación delete, y la operación de inserción insert neces lo parámetros de ubicación y número de línea, además del identificador. Las otras do raciones necesarias son un procedimiento para imprimir la información del resumen se acaba de mostrar para el archivo del estado, y una operación de búsqueda lookup p tener los números de ubicación de la tabla (necesarios más adelante para el generador digo, y también para el constructor de la tabla de símbolos a fin de verificar si ya visto una variable). Por Lo tanto, el archivo de cabecera symtab. h contiene las de raciones siguientes:

void st-insert( char * name, int lineno, int loc );

int st-lookup ( char * name );

void grintSyloifab(F1LE * listing);

Puesto que sólo existe una tabla de símbolos, su estructura no necesita ser declarad archivo de cabecera, ni necesita aparecer como un parhetro para estos procedimient

El código de implementación asociado en symtab. c utiliza una lista ligada diná mente asignada con un nombre de tipo LineLf st (líneas 1236-1239) para almacenar números de Línea asociados con cada registro de identificador en la tabla de disp Los registros de identificador en sí mismos se conservan en una cubeta c m nombre BucketList (Iíneas 1247-1252). El procedimiento st-insert (líneas 1262-1295) ga nuevos registros de identificador al frente de cada cubeta, pero los números de Ií agregan al final de cada lista de números de línea, a fin de mantener el orden numérico. eficiencia de st-insert se podría mejorar utilizando una lista circular de apuntadores perioreslinferiores para la lista de números de línea; véanse los ejercicios.)

6.52 Un analizador semantico para TlNY La semántica estática de TlNY comparte con los lenguajes de programación estándar I propiedades de que la tabla de símbolos es un atributo heredado, mientras que el tipo de d tos de una expresión es un atributo sintetizado. Así, la tabla de símbolos se puede cons diante uñ recorrido preorden del árbol sintáctico, y la verificación de tipos se puede e mediante un retorcido postorden. Mientras que estos dos recorridos se pueden combi cilmente en uno solo, para hacer clara la distinción de las operaciones de ambos pasos procesamiento, los divídimos en dos pasos por separado sobre el árbol sintáctico. De modo, la interfase del analizador semántico para el restodel compilador, que ubicano el archivo analyze .h (apéndice B, Iíneas 1350-1370), se compone de dos procedimi tos, dados por las declaraciones

void buildSymtab(TreeNode *);

void typeCheck(TreeN0de * ) ;

El primer procedimiento realiza un recorrido preorden del árbol sintáctico, llamando al procedimiento st-insert de la tabla de símbolos a medida que encuentra id&iicadores de wíable en el árbol. Después que se completa el lransver\al, llama entonces a grintSymTab vara imprimir la iriformxión almacenada al archivo del listado. El segundo procedimiento - realiza un recorrido postorden del irbol sintáctico, insertando tipos de datos en los nodos del

Page 341: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

donde checkNode es un procedimiento definido de manera apropiada que calcula y veri- fica el tipo en cada nodo. Resta describir el funcionamiento de los procedimientos in- sertNode y checkNode.

El procedimiento insertNode (Iíneas 1447-1483) debe determinar cuándo inser- tar un identificador (junto con su número de línea y ubicación) en la tabla de símbolos, basado en la clase de nodo del árbol sintáctico que recibe a través de su p'drámetro (un apun- tador a un nodo de árbol sintáctico). En el caso de nodos de sentencia, los únicos nodos que contienen referencias de variable son nodos de asignación y nodos de lectura, donde el nom- bre de variable a asignar o a leer está contenido en el campo attr .name del nodo. En el caso de nodos de expresión, los únicos nodos de interés son los nodos de identificador, donde el nombre nuevamente se almacena en attr .name. De este inodo, cn esos tres I~~garcs. el procedimiento insertNode contiene una Ilarnada

Un analizador remantito para el lenguaje TlNY 337

&bol a medida que se calculan e informando de cualquier error de verificación de tipo al ar- chivo del listado. El código para estos procedimientos y sus procedimientos auxiliares está contenido en el archivo analyze . c 'péndice B, Iíneas 1400-1558).

Para enfatizar las técnicas involucradas de recorrido de un árbol estándar, impiementa- mos tanto buildsymtab como typecheck utilizando la misma función de transversal genérica traverse (Iíneas 1420- 1441), que toman dos procedimientos (además del irbol sintáctico) como parámetros, uno para realizar el procesamiento preorden de cada nodo y el otro para efectuar el procesamiento postorden:

static void traverse( TreeNode * t, void ( * preProc) (TreeNode *) ,

void ( * postproc) (TreeNode *) )

í if (t != NULL)

[ preProc (t ) ;

í int i; for (i=O; i < NAXCHILDREN; i++)

traverse(t->child[il,preProc,postPro~);

1 postProc(t);

traverse(t->sibling,preProc,postProc);

1 1

Dado este procedimiento, para obtener un recorrido preorden necesitamos definir un pro- cedimiento que suministre procesamiento preorden y lo pase a traverse como pre- groc, mientras pasa un procedimiento "que no hace nada" como gostgroc. En el caso de la tabla de símbolos de TWY. el procesador de preorden se llama insertNode, por- que realiza inserciones en la tabla de símbolos. El procedimiento "que no hace nada" Fe de- noniina nullProc y se define con un cuerpo vacío (Iíneas 1438-1441). Entonces el reco- nido preorden que construye la tahla de símbolos se llevará a cabo mediante la llamada simple

dentro del procedimiento bui ldSymtab (Iíneas 1488-1494). De manera similar, el reco- rrido postorden requerido por typecheck (Ilneas 1556-1558) se puede obtener mediante la llamada simple

Page 342: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

!

CAP. 6 1 ANÁLISIS SEHÁNTICO

si la variable todavía no se ha contemplado (lo que almacena e increnienta el contador ubicación además del número de Iínea) y

si la variable ya está en la tabla de símbolos (que almacena el número de línea pero ubicación).

Finalmente, desputs que se ha construido la tabla de símbolos, el buildS efectúa una llamada a grintSymTab para escribir la información del número de archivo de listado, bajo el control del marcador TraceAnalyze (que está estahleci main . c).

El procedimiento checkNode del paso de verificación de tipo tiene dos tareas. primer lugar, debe determinar si, basado en los tipos de los nodos hijo, se ha presen un error de tipo. En segundo lugar, debe inferir un tipo para el nodo actual (si tiene un t y asignar este tipo a un campo nuevo en el nodo del árbol. Éste campo se conoce como c po tyge en TreeNode (definido en globals . h; véase el apéndice B, Iínea 216). C sólo los nodos de expresión tienen tipos. esta inferencia del tipo únicamente se pres para nodos de expresión. En TINY existen sólo dos tipos, entero y booleano, y estos ti están definidos en un tipo enumerado en las declaraciones globales (véase el apén Iínea 203):

trpedef enum Woid, Integer, Boolean) ExgType;

Aquí el tipo Void es un tipo "no-tipo" que se utiliza sólo en la inicialización y para v rificación de errores. Cuando se presenta un error, el procedimiento checkNode llama procedimiento tygeError, el cual imprime un mensaje de error al archivo del listad basado en el nodo actual.

Resta catalogar las acciones de checkNode. En el caso de un nodo de expresión, el no- do es un nodo hoja (una constante o un identificador, de clase ConstK o IdK) o un nodo de operador (de clase OpK). En el caso de un nodo hoja (líneas 1517- 1520), el tipo es siem- pre Integer (y no ocurre verificación de tipo). En el caso de un nodo de operador (líneas 1508-1516), las dos subexpresiones hijas deben ser de tipo Integer (y sus tipos ya s calculado, puesto que se está realizando un recorrido postorden). El tipo del nodo O determina entonces a partir del operador mismo (sin tener en cuenta si se ha presentado u error de tipo): si el operador es un operador de comparación (< o =), entonces el tipo es booleano (Boolean), si no lo es, es entero (Integer).

En el caso de un nodo de sentencia no se infiere ningún tipo, pero en todos los dem casos aparte de éste debe realizarse alguna verificación de tipo. Éste es el caso de una sentencia ReadK, donde la variable que se está leyendo debe ser automáticamente de tipo Integer, de modo que no es necesaria la verificación de tipo. Las otras cuatro clases de sentencia necesitan alguna forma de verificación de tipo: las sentencias IFK y RepeatK necesitan sus expresiones de prueba verificada? para asegurarse que tienen tipo Boolean (líneas 1527-1530 y 1539-1542), y las sentencias WriteK y AssignK necesitan una veri- ficación (líneas 1531-1538) de que la expresión que se está escribiendo o asignando no es booleana (ya que las variables sólo pueden mantener valores enteros, y sólo los valores en- teros se pueden escribir):

x := 1 < 2; { error - el valor booleano no se puede asignar 1

write 1 = 2; { un error también 1

Page 343: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

... :.-. ,. . :, .. ,....~ .. ,, ,..-,.,.,

...,. . ,... ;:, : ,.:.> EJERCICIOS 6.1 Escriba tina gramática con atributos para ei valor entero de un número dado por In gramática

siguiente:

número -+ dígito número / dígito d í S i t o + 0 / 1 / 2 / 3 / 4 / 5 / 6 / 7 / 8 / 9

6.2 Escriba una gramática con atrihutos para el valor de punto Ilotante de un número decimal dado por la gramática siguiente. (Sugerencia: utilice un atributo contro para contar el número de dí- gitos a la derecha del punto decinial.)

únum 4 rtuin.num num -+ num clígito 1 dígito t l i g i t o + 0 / 1 / 2 / 3 i 4 1 5 / 6 / 7 / 8 / 9

6.3 La gramática para números tleciinales del ejetcicio anterior se puede volver a escrihir de manera que sea innecesario un atributo conte» (y se eviten las ecuaciones que invt~lucran exponentes). Vuelva a escribir la gramática para hacer esto y proporcione una nueva gramática con atributos para el valor de un dntnn.

6.4 Considere una gramática de expresión como se escribiría para un analizador sintáctico pre- dictivo con la recursividad por la i~quierda eliminada:

exp 4 term e-~p' crp' -+ + term exp' 1 - term exp' 1 F

rerm +j¿fctor terni' term' -+ *factor term' / E

factor ( erp ) / número

Escriba una grarnática con atributos para el valor de una expresión dada por esta gramática. 6.5 Vuelva a escrihir la gramática con atributos de la tabla 6.2 para calcular un atributo de cadena

posIfijo en lugar de vul, que contenga la forma postfija para la expresión entera sim& Por ejemplo, el atributo postfijo para (34-3) *42 resultaría ser "34 3 - 42 *". Se puede suponer un operador de concatenación II y la existencia de iin atribitto número.vt~icaden«.

6.6 Considere la gratiiática siguiente para irboles hinarios enteros (en forma linealizada):

(irbol + ( número círbol drbol ) / ni1

Escriba una gramática con atributos para veri,iicar que está ordenado un árbol binzrio, es decir, qtte los valores de los números del primer subirbol sean I que el valor del núniero aciual y los valores de todos los iiúmeros del segtindo subárbol sean 2 al valor del número actual. Por ejcnlplo ( 2 (1 ni1 nil) (3 ni1 ni1 f ) cstá ordenado, pero (1 (2 ni1 ni11 (3 ni1 nil) ) o« lo está.

6.7 Considere la graniática siguiente para declaraciones simples estilo Pascal:

clecl -+ vcrr-ii.vt : type var-lis¡ -+ wr-lis! , i d / id f y e + integer 1 real

Page 344: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 AW~LISIS SEMANTICO

6.8 Considere la gramática del ejercicio 6.7. Vuelva a escribir la gramática de manera que el ti

de una variable se pueda definir como un atributo puramente sintetizado, y proporcione u nueva gramática con aiributos para el tipo que tiene esta propiedad.

6.9 Vuelva a escribir la gramiitica y gramática con atributos del ejemplo 6.4 (página 266) de ma que el valor de un num-base se calcule s61o por atributos sintetizados.

6.10 a. Dibuje las gráficas de dependencias correspondientes a cada regla gramatical del ejempl 6.14 (página 283) y para la expresión 5 /2 / 2 . O .

b. Describa los dos pasos que se requieren para calcular los atributos eti el árbol sintáctico 5 /2 /2 .0 , incluyendo un posible orden en el cual se podrían visitar los nodos y los valor

de ainbuto calculados en cada punto. c. Escriba el p~eudocódigo para procedimientos que realilarían los cálculos descritos en el

inciso b. 6.1 1 Dibuje las gráficas de dependencia correspondientes a cada regla gramatical para la gramática

con atributos del ejercicio 6.4, y para la cadena 3 * ( 4 + 5 ) * 6.

6.12 Dibuje las gráficas de dependencia correspondientes a cada regla gramatical para la gramática con atributos del ejercicio 6.7, y dibuje una gráfica de dependencia para la declaración x, y, z: real.

6.13 Considere la siguiente gramática con atributos:

Regla gramatical Reglas semánticas

a. Dibuje el árbol de análisis gramatical para la cadena abc (la Única cadena en el lenguaje), y dibuje la gráfica de dependencia para los atributos asociados. Describa un orden correcto para la evaluación de los atributos.

b. Suponga que se asigna a S.u el valor 3 antes de comenzar la evaluación de atributo. ¿Cuál es el valor de S.v cuando ha terminado la evaluación?

c. Suponga que las ecuaciones de atributo se modifican como sigue:

Regla gramatical Reglas semánticas

S - t A B C B.u = S.u C.u = A.v A.u = B.v + C.v

¿Qué valor tiene S.v dcspués de la evaluación de atributo, si S.ii = 3 antes de comenzar la cvaluación'?

Page 345: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejertitios

6.14 Muestre que, dada la gramática con atributos,

Regla gramatical Reglas semánticas

decl -i tvpe vur-list viir-list.dt).pe = type.dtype type -. i n t epe.dtype = integer type -t f loat ryl>e.<ltype = real wr-li.stl + id , var-/ist2 id .dtype = vrrr-li.stl.dr)pe

vur-/i.st2.rl~ype = vor-listl.rlfype vur-list - id id .dtype = vnr-list.dtypc

si el atributo @pe.drype se mantiene en la pila de valores durante un análisis sintáctico LR, entonces este valor no se puede encontrar en una posici6n fija en la pila cuando ocurren reduc- ciones a una vur-lisr.

6.15 a. Demuestre que la gramática B -. B b I u es SLR( 1 j, pero que la gramática

[construida a partir de la gramática anterior al agregar una producciún E simple) no es LR1k) para ninguna k. b. Dada la gramática del inciso a (con la producción E), ¿que ciidenas aceptarían un analizador

sintáctico generado por Yacc? c. ¿Es probable que esta situación se presente durante el ;inálisis semántica de un lenguaje de

programación "real"? Justifique su respuesta. 6.16 Vuelva a escribir la gramática de expresión de la sección 6.3'.5 en una gramática no ambigua,

de manera tal que 1% expresiones escritas en esa sección sigan siendo legales, y viielua ;i cscri- bir la gramática con atributos de la tabla 6.9 (página 31 1) para esta nueva gramática.

6.17 Vuelva a escribir la graniática con atributos de la tabla 6.9 para utilizar declaraciones colatera- les en lugar de declaraciones secuenciales. (Véaie la página 307.)

6.18 Escriba unigramática con atributos que calcule el valor de cada expresión para la gramática de expresión de la sección 6.3.5.

6.19 Modifique el pseudocódigo para la función fypeiFqua1 de Lea figura 6.20 (página 325) a fin de incorporar nombres de tipo y el algoritmo sugerido para determinar la equivalencia estructural de tipos recursivos como se describió en la página 327.

6.20 Considere la siguiente gramQtica (ambigua) de expresiones:

exp -+ exp + exp / exp - exp j exp * exp / exp / exp / ( exp ) / num 1 num.num

Suponga que se siguen las reglas de C al calcular el valor de cualquier expresión así: si dos subexpresiones son de tipo mixto, entonces la subexpresión en modo entero se convierte a iriodo de punto flotante, y se aplica el operador de punto flotante. Escriba una gramática con atributos que convertirá a tales expresiones en expresiones legtlcs en Modul;i-2: las conversio- nes de itíimeros enteros a núineros de punto flotante se expresan aplicando la funcih FLOAT,

y el operiidor de división / se considera como d iv si sus operitndos son enteros. 6.21 Considere la siguiente extensión de la gramática de la I'igiira 6.22 (piigina 129) para incluir

Iliimadas y declaraciones de i'uncioi~es:

Page 346: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 ANÁLI$IS SEMANTICO

progrcima -+ r'ur-dec1.s ; fiin-rfecl.~ ; sents

vur-llec1.s -+ iar-úecls ; var-decl / iur-rlecl

vcrr-clrd -+ id : type-e.rp

iype-exp -t int / bool / array [num] of @[)e-e.rp

Jun-decls -+ fun id ( vnr-decls ) : Qpe-exp ; cirerpo

cuerpo --. e.xp

w i t s -+ sc.nt,s ; sent 1 sent senr -t if cxp then sent / id : = exp

exp -t exp + erp / exp or exp / e.xp 1 e.rp 1 / id ( exps )

/ n u m / true/ false / id erps -t exps , exp inp e.rp

a. Disehe una estnichlra adecuada de árbol para las iiuevas estructuras de tipo de hinción, escriba una función @~~eEqual para dos tipos de función.

b. Escriba regias semánticas para la verificación de tipo de declaraciones de frtnción y llai das de función (representada por la regla exp -t i d ( crps ) ) similares a las reglas de la hla 6.10 (página 330).

6.22 Considere la siguiente ambigüedad en expresiones en C. Dada la expresión

( A ) -X

- si x es tina vuiahle entera, y A esta definida en una typedef como equivalente a double, entonces esta expresi6n asigna el v:tlor de -x a double. Por otra parte, si A es una variable entera, entonces esto calcula la diferencia en valor entero de las dos variables.

a. Uescriha cómo puede utilizar el analizador sintáctico la tabla de símbolos para eliminar la ainbigiiedad de estas dos interpretaciones.

b. Describa cómo puede emplear el analizador léxico la tabla de símholos para eliminar la ambigüedad de estas dos interpretaciones.

6.23 Escriba una gramática con atributos correspondiente a las restricciones de tipo impuestas por el : verificador de tipos de TINY.

6.24 Escriba una gramitica con atributos para la construcci6n de la tabla de símbolos del analizador ;

semántica de TINY.

6.26 a. Vuelva a ewibir e1 evaluador descendente recuríivo de la figura 4.1 (página 148) de manera que imprlma la traducción postfija en lugar del valor de una eupie~tón (véabe el e.jercicio 6.5).

b. Vi~elva a escribir el evaltiador descendente recursivo de manera que imprima trinto el vdor com« la traducción postfija.

6.27 a. Viielva a escribir la especificación Yacc de la figura S. 10 (pígina 228) para una ciilculadora de enteros simple de inanem que imprima una traducción postfija eri lugar del wlor (véase el ejercicio 6.51.

b. Vuelva a escribir la especilicacih Yacc de inanera que imprima i<uirr~ el vdor como la ira- (lucci6t1 postfiin.

6.28 Ecriha iiiia ecpccitic;ici6n Yace que imprimir5 la traduccidn en Mtidula-2 de tina expresiijn i l ah para I;i p;imática del ejercicio 6.3).

Page 347: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Notas y referencias 343

6.29 Escriba una especificación Yacc para un programa que calculará el valor de ona expresi611 con bloques lrt (tabla 6.9, página 3 11). (Se pueden abreviar los tokens let e in a caracteres simples y también restringir identificadores o usar Lex para generar un analizador léxico adecuado.)

6.30 Escriba una especificación Yacc y Lex de un programa para verificar tipos del lenguaje cuya gramiítica está dada en la figura 6.22 (página 329).

6.31 Vuelva a escribir la implementación de la tabla de símbolos para el analizador scni5ntico de TINY con el fin de agregar un apuntador inferior a la estrtictura de datos LineList y tncjorar la eficiencia de la operación insert.

6.32 Vuelva a escribir el analizador semántico de T1NY de manera que s6lo realice un paso sohre el

árbol sintictico. 6.33 El analizador semántico de TINY no intenta asegurar que se ha asignado una variable :luces de

utilizarse. De este modo, el siguiente código TINY se considera senxánticamente correcta:

Vuelva a escribir e1 analizador TINY de manera que haga verificaciones "razonables" de que ha ocurrido una asignación a una variable antes de utilizar esa variable en una expresicin. <,Qué impide a tales verificaciones ser infalibles?

6.34 a. Vuelva a escribir el ~malizador seniántico de TI?SY para permitir que se almacenen valores booleanix en variables. Esto requerirá que se dé a las variables tipos de datos hooleano o entero en la tabla de símbolos y que la verificación de tipo incluya una búsqueda en la tabla de símbolos. La exactitud del tipo de un programa TINY ahora debe incluir el requerimien- to de que todas las asignaciones a (y usos de) una variable sean consistentes con sus tipos de datos.

b. Escriba una gramática con atributos para el verificador de tipo TlNY como h e modificado en cl inciso a.

NOTAS! EI trabajo inicial rtiis importante acerca de gramática con atributos es el de Knuth [1968].

REFERENCIAS hnportantesestudios del uso de las gramáticas con atributos en la construcción de compilado- res ap:uecen en Lorho [19841. El uso de gramiticas con atributos para especificar formalmen- te la semintica de los lenguajes de programación se estudia en Slonneger y Kurtz /1995], donde se proporciona una gramitica con atributos completa para la semántica estática de un lenguaje similar a TINY. Más propiedades matemáticas de las gramáticas con atribiitos se pueden encontrar en Mayoh [1981 J. Una prueba para la no circularidad se puede hallar en Jazayeri, Ogden y Rounds [1975].

La cuestión de la evaluación de atributos durante el análisis sintáctico se estudia con al- go más de detalle en Fischer y LeRlmc [1991]. Las condiciones que aseguran que esto se puede hacer durante un anilisis sintáctico LR aparecen en Jones [19801. La cuestión de mantener análisis sintáctico LR determinístico mientras se agregan nuevas producciones E

p:m programar acciones (conio en Yacc) se cstudia en Purdorn y Brown [IOXO]. Las cstmcturas de datos para la irnplen~entación de la tabla de símbolos, incluyemlo tabl:is

de dispersión y análisis de sus eficiencias, se pueden encontrar en muchos textos; v6cw. por cjemplo, Aho, Hopcrolt y IJllman [l983] o Corrnen. Leiscrson y Rivcst 11990j. Un cuida- doso estudio de la selección de tina fnncirín de tlispersiíín aparece en Knuth (1W731.

L o s sistemits ilc tipos, cvactitud dc tipos e irifcrcncia de tipos forman uno de los c m - p~spriricilxiles del estudio & la ciencia cornput;~cional tci>rica, con q~licriciones :i rniichos

Page 348: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 6 1 ANÁLISIS SENANTICO

lenguajes. Para una perspectiva general tradicional, véase Louden 119931. Para una pectiva más profunda, véase Cardelli y Wegner [1985]. C y Ada utilizan formas mi equivalencia de tipos similar a la equivalencia de declaración de Pascal, que son di de describir de manera concisa. Lenguajes más antiguos, tales como FORTRAN77, Al y Algo168 utilizan equivalencia estructural. Los lenguajes funcionales rnodernos como Haskell utilizan equivalencia de nombre estricta, con sinónimos de tipo tornando el 1 la equivalencia estructural. El algoritmo para equivalencia estructunl descrito en la 6.4.3 se puede encontrar en Koster [19691; una moderna aplicación de un algoritmo se encuentra en Amadio y Cardelli 119931. Los sistemas de tipo polimórficos y algori de inferencia de tipos se describen en Peyton Jones 119871 y Reade 119893. La versión inferencia de tipos polimórfica utilizada en ML y Haskell se conoce como inferencia de Hindtey-Milner [Hindley, 1969; Milner, 19781.

En este capítulo no describimos todas las herramientjs para la construcción auto de evaluadores de atributos, puesto que ninguna es de uso general (a diferencia de los g dores de analizadores léxicos y analizadores sintácticos Lex y Yacc). Algunas herra interesantes basadas en gramáticas con atributos son LINGUIST [Fmow, 19841 [Kastens, Hutt y Zimmermann, 19821. El Synthesizer Generator ("Generador Sintetiz [Reps y Teitelbaum, 19891 es una herramienta madura para la generación de editores se bles a1 contexto basados en gramáticas con atributos. Una de las cosas interesantes qu pueden hacer con esta herramienta es construir un editor de lenguaje que suministra de nera automática declaraciones de variable basadas en el uso.

Page 349: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Capítulo 7

Ambientes de ejecución

Organización de memoria 7.4 durante la ejecución del 7.5 programa

Ambientes de ejecución 7.6 completamente estáticos

Ambientes de ejecución basados en pila

Memoria dinámica

Mecanismos de paso de parámetros

Un ambiente de ejecución para el lenguaje TlNY

En capítulos anteriores estudiamos las fases de un compilador que realiza análisis estático del lenguaje fuente. Esto incluyó análisis léxico, análisis sintáctico y análisis semántico es- - . tático. Este análisis sólo depende de las propiedades del lenguaje fuente: es independiente por completo del lenguaje objetivo (ensamblador o de máquina) y de las propiedades de la máquina objetivo y su sistema operativo.

En este capitulo y el siguiente volveremos a la tarea de estudiar cómo un compilador genera código ejecutable. Esro puede involucrar análisis adicional, como el que realiza un optimizador, y parte de éste puede ser independiente de la máquina. Pero gran parte de la tarea de la generación de código depende de los detalles de la máquina objetivo. N o obstante, las caracterísdcas generales de la generación de código permanecen iguales a través de una amplia variedad de arquitecturas. Esto es particularmente cierto para el ambiente en tiempo de ejecución o ambiente de ejecución, el cual es la estructura de la memoria y los registros de la computadora objetivo con los que se administra la memoria y se mantiene la información necesaria para guiar el proceso de la ejecución. De hecho, casi todos los lenguajes de programación utilizan una de tres clases de ambiente en tiempo de ejecución, cuya estructura esencial no depende de los detalles específicos de la máquina objetivo. Estas tres clases de ambientes son: el ambiente completamente estático característico de FORTRAN77; el ambiente basado en pila de lenguajes como C. C+ +, Pascal y Ada; y el ambiente completamente dinámico de lenguajes funcionales como LISP. También pueden utilizar híbridos a partir de todos los anteriores.

En este capitulo comentaremos cada una de estas clases de ambientes por turno, junto con las características del lenguaje que determinan qué ambientes son factibles, y cuáles deben ser sus propiedades. Esto incluye cuestiones de ámbito y asignación, la naturaleza de llamadas a procedimientos, y las variedades de mecanismos para paso de parámetros. Aquí, el enfoque se centrará en la estructura general del ambiente y en el siguiente capítulo, en el código real que es necesario generar para mantener el ambiente. En este aspecto es importante tomar en cuenta que un compilador puede mantener un

Page 350: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

ambiente sólo de manera indirecta, ya que debe generar código para realizar las oper ciones de mantenimiento necesarias durante la ejecución del programa. En contraste, interprete tiene una tarea más fácil, puesto que puede mantener el ambiente directamen dentro de sus propias estructuras de datos.

La primera sección de este capítulo contiene un análisis de las caracteristicas ge les de todos los ambientes en tiempo de ejecución y sus relaciones con la arquitectura máquina objetivo. Las siguientes dos secciones analizan los ambientes estático y basa en pila, junto con ejemplos de su funcionamiento durante la ejecución. Como el ambi basado en pila es el más común, se proporciona algo de detalle acerca de las diferen variedades y la estructura de un sistema basado en pila. Una sección posterior analiza tiones de memoria dinámica, incluyendo ambientes completamente dinámicos y ambien orientados a obietos. A continuación se tiene un análisis del efecto de diversas técnicas paso de parámetros sobre el funcionamiento de un ambiente. El capitulo se cierra con una breve descripción del ambiente simple necesario para implementar el lenguaje TINY.

7.1 ORGAN~ZACION DE MEMORIA DURANTE LA EJECUCIÓN DEL PROGRAMA

La memoria de una computadora típica está dividida en un área de registro y una memoria acceso aleatorio (RAM), más lenta, que se puede abordar directamente. E l área de RAM ta bién puede dividirse en un área de código y un área de datos. En la mayoría de lenguajes com pilados no es posible efectuar cambios al área de código durante la ejecución, y el área de c digo y de datos pueden visualizarse como separadas de manera conceptual. Además, conlo área de código se fija antes de la ejecucibn, todas las direcciones de código se pueden calcul en tiempo de compilación, y el área de código se puede contemplar de la manera siguiente:

Punto de entrada mediante el procedimiento 1 -+

Punto de entrada mediante el procedimiento 2 -+

Punto de entrada mediante el procedimiento n 4

el procedimiento rl el procedimiento

cddigo para / el procedimiento 1 Código en memoria

En particular, e l punto de entrada de cada procedimiento y función se conoce en tiempo de compilación.' No se puede decir lo mismo de la asignación de datos, ya que solamente una pequeíia de la misma puede asignarse en ubicaciones fi jas en memoria antes de que comience la c,jecucibn. Gran parte del resto del capítulo se dedica a cómo manejar la asig- nación de datos que n o es fija, o bien, que es dimimica.

l. [.o 1m3s probül>le es que c l códi~o se carga mediante un cargador en un &ea en memoria que es- 14 ahigriada al principio de la cjccución y, de este nrodo, $10 es absolut:iniente predecible. Sin embargo, todas las direcciones re:iles son entonces c:ilculadas de riianera automitica tnediante despl;tzamiento ilestie una dirección de carg hase lija, de Iorma que e1 principio de direcciones fij:is permanece igual. En ucasioiies, e l cscritor dcl compilndor debe tener cuidado (le gencrx el denon~in;idu código relo- calizahle, en el cttiil 10s s:~It«s, I1am;rdas y refewirci:is son twlas realizadas cn relaci~in c y :ilg~~n:i hase, por lo rcgnl;it- iiii t-cgislrc). Eje~nplos de eslo se pn)poi-ciitn:i~-;ir> cir el c:ipiiiil« higuie~irc.

Page 351: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Organización de memoria durante la ejetucian del programa 347

Existe una clase de datos que pueden ser fijados en memoria antes de la ejecución y que comprenden los datos globales y10 estiticos de un programa. (En FORTRAN77, a diferencia de La niayoría de los lenguajes, todos los datos son de esta clase.) Tales datos por lo regular se asignan de manera separada en un área fija de manera similar al código. En Pascal, las varia- bles globales se encuentran en esta clase, lo mismo que las variables estáticas y externas de C.

Una cuestión que surge en la organización del área globaWestática involucra constantes que son conocidas en tiempo de compilación. Éstas incluyen Pas declaraciones const de C y Pascal, además de valores de literales utilizadas en el código mismo, tal como la cadena "Hello%d\nn y el valor entero 12345 en la sentencia C

grintf ( "Hello %d\n", 12345) ;

Las constantes pequeñas de tiempo de compiiación, tales como O y 1, por lo regular se insertan directamente en el código por medio del compilador y no se les asigna ningún espacio de da- tos. Tampoco se necesita asignar espacio en el área de datos global para procedimientos o fun- ciones globales, puesto que el compilador conoce sus puntos de entrada y también se pueden insertar directamente en el código. Sin embargo, a los valores enteros grandes, los valores de punto flotante y, en particular, las literales de cadena se les asigna por lo regular memoria en el área globaííestática, se almacenan una vez en el inicio y posteriormente se buscan en esas ubicaciones mediante el código de ejecución. (En realidad, en las literales de cadena C son visualizados como apuntadores, de modo que deben ser almacenadas de esta manera.)

El área de memoria utilizada para la asignación de datos dinámicos puede ser organiza- da de muchas maneras diferentes. Una organización típica divide esta memoria en un área de pila y un'área de apilamiento o montículo (heap), donde el área de pila es utilizada pa- ra los datos cuya asignación se presenta en modo LIFO (por las siglas del término en inglés "último en entrar, primero en salir", es decir, "last-in, first-out") y el área de apilamiento es empleada para la asignación dinámica que no cumple con un protocolo LIFO (la asigna- ción de apuntadores en C, por ejemplo).z A menudo la arquitectura de la máquina objetivo incluirá una pila de procesador, y al usar esta pila permitirá emplear soporte de procesador para llamadas de procedimiento y retornos (el mecanismo principal que utiliza asignación de memoria basada en pila). En ocasiones, un compilador tendrá que hacer preparativos para la asignación explícita de la pila del procesador en un lugar apropiado en la memoria.

Una organización general de almacenamiento en tiempo de ejecución que tiene todas las categorías de memoria descritas puede tener el aspecto que se presenta a continuación:

1 área de código l área global/est&tica u

! apilamiento o montículo (heap)

1. Debería observarse que el apilümiciito es por lo regular uiia siinple irea de memoria lineal. Se denomina ripiliiniiento o montículo (heap) por rnmnes histhricas, y no estli relaciotiado coi1 la estruc- tura de datos de pilns utilimda en :ilgoritnios taics como el de or<bn:imiento de pilns.

Page 352: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 7 1 AMBIENTES DE E J E C U C I ~ N

Las flechas en esta imagen indican la dirección de crecimiento de la pila y el apila (heap). Tradicionalmente, la pila se representa incrementándose hacia abajo en la memo manera que su tope o parte superior en realidad está en la parte inferior de su área ilu El apilamiento también se representa de manera similar a la pila, pero no es una est LIFO, y su incremento y decremento son más complicados de lo que indica la flecha la sección 7.4). En algunas organizaciones se asigna a la pila y al apilamiento seccion paradas de memoria, en vez de ocupar la misma área.

Una unidad importante de la asignación de memoria es el registro de activac procedimiento, el cual contiene la memoria asignada a los datos locales de un procedi to o función cuando son llamados o activados. Un registro de activación, como mínimo, contener las secciones siguientes:

- 1 espacio para argumentos 1 (parámetros)

espacio para inlormacion 1 de adm~nistrachn. incluyendo 1 1 direcc~ones de retorno ,

/ espaao para datos locales / a I

locales :

Aquí hacemos énfasis (y lo haremos de manera repetida en las secciones siguientes), en qu esta imagen sólo ilustra la organización general de un registro de activación. Los detalles pecíficos, incluyendo el orden de los datos que contiene, dependerán de la arquitectura la máquina objetivo, de las propiedades del lenguaje que se está compilando, e inclu de las preferencias del escritor del compilador.

Algunas partes de un registro de activación tienen el mismo tamaño para todos los pro- cedimientos; el espacio para la información de administración, por ejemplo. Otras partes, como el espacio para los argumentos y datos locales, pueden permanecer fijas para cada procedimiento individuai, pero variarán entre un procedimiento y otro. Algunas partes del registro de activación también pueden ser asignadas de manera automática mediante el pro- cesador o llamadas de procedimiento (el almacennmiento de la dirección de retorno, po ejemplo). Otras partes (como el espacio temporal local) pueden necesitar ser asignadas de manera explícita mediante instrucciones generadas por el compilador. Dependiendo del lenguaje, los registros de activación pueden ser asignados en el área estática (FORTRAN77), el área de la pila (C, Pascal) o el área de apilamiento (LISP). Cuando los registros de activación se conservan en la pila, en ocasiones se hace referencia a ellos como mar de pila.

Los registros de procesador también forman parte de la estmdtura del ambiente de eje- cución. Los registros pueden ser utilizados para almacenar elementos temporales, variables locales o incluso variables glohales. Cuando un procesador tiene muchos registros, como es el caso en los procesadores RISC más recientes, toda el área estática y los registros de acti- vación completos puedenser conservados por completo en registros. Los procesadores tam- bién tienen registros de propósito específico para mantenerse a1 tanto de la ejecución, tales como el contador de programa (pc, program counter) y el apuntador de pila (sp, stack pointer) en la mayoría de las arquitecturas. ~ambién puede haber registros específica- mente diseñados para mantenerse al tanto de las activaciones de procedimiento. Algunos registros típicos de esta naturaleza son el apuntador de marco (fp, frame pointer), que

Page 353: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ambientes de ejecution completamente esdticos 349

apunta al registro de activación actual, y el apuntador de argumento (ap, argument pointer), que apunta al área del registro de activación reservado para argumentos (valores de parámetro)."

Una parte particularmente importante del diseño de un ambiente de ejecución es la determinación de la secuencia de operaciones que deben ocumr cuando se llama a un pro- cedimiento o funciún. Tales operaciones pueden incluir la asignación de memoria para el re- gistro de activación, el cálculo y almacenamiento de los argumentos, y el almacenan~iento y establecimiento de los registros necesarios para efectuar la llamada. Estas operaciones por lo regular se conocen como secuencia de Ilamada. Las operaciones adicionales necesarias cuando un procedimiento, o una función, es devuelto, tal como la ubicación del valor de re- tomo al que se puede tener acceso mediante el elemento que llama, el reajuste de los regis- tros y la posible liberación de la memoria del registro de activación, por lo general también se consideran parte de la secuencia de Ilamada. Si es necesario, haremos referencia a la parte de la secuencia de llamada que se realiza durante una llamada como la secuencia de llamada y la parte que es efectuada al regreso como la secuencia de retorno.

Los aspectos importantes del diseño de la secuencia de llamada son 1) cómo dividir las operaciones de la secuencia de llamada entre el elemento que llama y el elemento llamado (es decir, cuánto código de la secuencia de llamada colocar en el punto de llamada y cuán- to colocar al principio del código de cada procedimiento) y 2) hasta qué punto depender del soporte del procesador de llamadas en vez de generar chdigo explícito para cada paso de la secuencia de Ilamada. El punto 1) es un asunto particularmente espinoso, puesto que por lo regular es más fácil generar el código de la secuencia de llamada en el punto de llamada en vez de hacerlo desde el interior del elemento llamado, pero al hacerlo así se provoca que crezca el tamaño del código generado, puesto que el mismo código debe ser duplicado en cada sitio de llamada. Más adelante se tratarán estas cuestiones con más detalle.

Como mínimo, el elemento que llama es responsable de calcular los argumentos y colocarlos en ubicaciones donde puedan ser encontrados por el elemento llamado (quizhs directamente en el registro de activación del elemento llamado). Además, el estado de la má- quina en el punto de Ilamada, incluyendo la dirección de retomo y, posiblemente, los regir- tros que se encuentran en uso, deben ser guardados, ya sea por el elemento que llama o por el elemento Ilamado, o de manera parcial por ambos. Finalmente, tambien debe ser estable- cida cualquier información adicional de administración, de nueva cuenta, quizá de alguna manera en la que intervengan tanto el elemento que llama como el elemento llamado.

72 AMBIENTES DE EJECUCION COMPLETAMENTE ESTATICOS La clase más simple de un amhiente de ejecución es aquella en la cual todos los datos son estáticos y permanecen fijos en la memoria mientras dure la ejecución del programa. Un ambiente de esta clase puede utilizarse para implementar un lenguaje en el cual no hay apuntadores o asignación dinámica, y en el cual los procedimientos no puedan ser llamados de manera recursiva. El ejemplo estándar de un lenguaje así es FORTRAN77.

En un ambiente completamente estático no sólo las variables globales, sino todas las variables, son asignadas de manera estática. Así, cada procedimiento tiene sólo un registro de activación Único, que es asignado estáticamente antes de la ejecución. Se puede tener xceso directamente a todas las variables, ya sean locales o globales, mediante direcciones fijas, y la memoria completa del programa \e puede visualizar como sigue:

3. Estos nonihres son tomados de la arquitectura VAX. pero nombres sinlilares se presentan cn otras :~rquitc~turils.

Page 354: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejemplo 7.1

CAP. 7 1 AMBIENTES DE EIECUCION

código para el procedimiento principal

1 cddigo para el procedimiento 1 1

código para el procedimiento n

área de datos globales

registro de activación / del procedimiento principal

registro de activación del procedimiento 1

registro de activación del procedimiento n

~ ~.

Área de código

Área de datos

En un ambiente así hay relativamente poco gasto general en términos de administració información a mantener en cada registro de activación, y no es necesario mantener info ción extra acerca del ambiente (aparte de, tal vez, la dirección de retorno) en un registr activación. La secuencia de llamada para un ambiente de esta naturaleza también es parti larmente simple. Cuando se llama a un procedimiento, cada argumento es calculado y alma cenado en su ubicación de parámetro apropiada en la activación del procedimiento que está llamando. Entonces la dirección de retorno en el código del elemento que llama se g ba, y se efectúa un salto al inicio del código del procedimiento llamado. Al regreso, se hace un simple salto a la dirección de retorno:

Como un ejemplo concreto de ebta clase de ambiente, considere el programa en FORTRAN de la figura 7.1. Este programa tiene un procedimiento principal y un solo procedimie adicional QuADMEWI.~ Existe una sola variable global dada por la declaración COMMON MAXSIZE tanto en el procedimiento principal como en QUADM~.'

4. En la mayoría de las arquitecturas, un salto de subrutina guarda automáticamente la dirección de retomo; esta dirección también se recarga (le nianera automática cuando se ejecuta una instrucción de retorno.

5. Ignoramos la función de biblioteca SQRT. la cual es Il:tmnd~ por QUADMEAN y cstd vinculada iintes de la ejeci~ción.

6. De Iiecho, FOWRAN77 permite que v;iri;ihles COM,LlON teiigdn notiihres diferentes en pro- cedimientos diferentes, nrientras que todavía Iiaccn referencia a la misma uhicaciijn de ~netrioria. Desde ahora en este ejemplo ign~rarcrnos discret:iriiente tales complejidades,

Page 355: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ambientes de ejecución completamente estáticos

PROGRAM TEST

COMMON MAXSIZE

INTEGER MAXSIZE

REAL TABLE ( 10 ) , TEMP MAXSIZE = 10

READ *, TABLE(l),TABLE(2),TABLE(3)

CALL QuADMEAN(TABLE,~,TEMP)

PRINT *, TEMP

END

SUBROUTINE QUADMEAN(A,SIZE,QMEAN)

COMMON MAXSIZE

INTEGER MAXSIZE, SIZE

REAL A(SIZE),QMEAN, TEMP

INTEGER K

TEMP = 0.0

IF ((SIZE.GT.MAXSIZE).OR.(SIZE.LT.l)) GOTO 99

DO 10 K = 1,SIZE

TEMP = TEMP + A(K)*A(K) 10 CONTINUE

99 QMEAN = SQRT(TEMP/SIZE)

RETURN

ENE

Ignorando la posible diferencia en tamaño entre los valores enteros y de punto flotante cn memoria, ilustramos un ambiente de ejecución para este programa en la figura 7.2 (página 352): En esta imagen indicamos con flechas los valores que los parámetros A, S i Z E y Q M W del procedimiento QUADMEAN tienen durante la llamada desde el procedimiento principal. En FORTRAN77, los valores de parámetro son implícitamente referencias a me- moria, de modo que las ubicaciones de los argumentos de la llamada (TABLE, 3 y TEMP) se copian en las ubicaciones de paránietro de QumMEAN. Lo anterior tiene varias conse- cuencias. La primera es que, se requiere otra derrefereneiación para tener acceso a valores de parámetro. La segunda es que los parámetros de arreglo no necesitan ser reubicados y eo- piados (así, se asigna al parjmetro de arreglo A en QUADMEAN sólo un espacio, que apunta a la ubicación base de TABLE durante la llamada). La tercera es que los argumentos cons- tantes, como el valor 3 en la llamada, deben aimacenarse en una localidad de memoria y és- ta debe utilizarse durante la llamada. (Los mecanismos de paso de parámetro se comentan más ampliamente en la sección 7.5.)

Otra característica de la figura 7.2 que requiere ser explicada es la ubicación sin nombre que se asignó al final del registro de activación de QUADMW. Esta ubicación es una localidad "improvisada" que se utiliza para almacenar valores temporales durante el cilculo de expresiones aritméticas. En QUADMEAN existen dos cilculos donde esto pnede

7. Nuevaniente enf:itiz:irnoi que los c1ctalles de csta figura son sGlo ilustt~;itivt)s. Las iinpleincnta- ciones realcs pueden dikrir en li~rriia sust;~i~ci:il Je I;is prop«rcio~~ad;is :rqrií.

Page 356: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 7.2 Un ambiente de ejecutih para el programa de la figura 7.1

ser necesario. El primero es el cálculo de TEMP + A (K) * A ( K) en el ciclo, y el segu es el cálculo de TEMP/SIZE como ei parámetro en la hmada a SQRT. Ya coment la necesidad cle asignar espacio a valores de parámetro (aunque en una Ilamada a una fu de biblioteca la convención puede ser, de hecho, diferente). La razón de que una localid memoria tentporal también pueda ser necesaria para el cálculo del ciclo es que cada I ción aritmética debe aplicarse en un solo paso, de manera que A ( K ) * A ( K ) se calcu luego se suma al valor de TEMP en el siguiente paso. Si no hay suficientes registros nibles para mantener este valor temporal, o si se hace una llamada requiriendo que est lor sea guardado, entonces el valor ser6 almacenado en el registro de activación an culminación del cálculo. Un compilador siempre puede predecir si esto será neces rante la ejecucih, y hacer preparativos para la asignación del número (y tamaño) ap do de las ubicaciones temporales.

Área global

Registro de activación del procedimiento principal

Registro de activación del procedimiento QUADMEAN

73 AMBIENTES DE EJECUCIÓN BASADOS EN PILA En un lenguaje en el que se permitan las llamadas recursivas, y en el cual las variabl locales sean nuevamente asignadas en cada Ilamada, los registros de activación no pue- den ser asignados de manera estática. En vez de esto, los registros de activación deben ser asignados dé manera basada en pila, en la cual cada nuevo registro de activación es asignado en la parte superior de la pila a medida que se hace una nueva llamada de proce- dimiento (una inserción del registro activación) y desasignado cuando la llamada termina (tina extracción del registro de activación). La pila de registros de activación (también co- nocida como pila de ejecución o pila de llamadas) cntonces se inerementa y decrementa con la cadena de llamadtis que se han presentado en el programa en ejecucibn. Cada proce- dimiento puede tener varios registros de activación diferentes a la vez en la pila de llama- das, y cada una representrirá una llamada distinta. Un ambiente de esta clase requiere una estrategia m:is compleja para su administraciGn y acceso n variables que un ambiente comple- tamente cstcitico. En particular, la información de administración adicional debe inantener- se en 10s registros de activacibn. y la secuencia de llamada también debe incluir los pasos

Page 357: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejemplo 7.2

Ambientes de ejecución basados en pila 353

necesarios para estabtecerla y mantenerla. La exactitud de un ambiente basado en pila, y la cantidad de información de administración requerida, depende fuerteinente de las propieda- des de lenguaje que se está compilando. En esta sección consideraremos 13 organización de tos ambientes basados en pila en un orden de comple,jidad creciente, clasificados por las pro- piedades de lenguaje involucrado.

'13.1 Ambientes basados en pila sin procedimientos locales En un lenguaje donde los procedimientos son globales (corno el lenguaje C), un ambiente basado en pila requiere de dos cosas: el mantenimiento de un apuntador hacia el registro de activación actual para permitir acceso a variables locales y un registro de la posición o ta- maño del registro de activación inmediatamente precedente (el registro de activación del elemento que llama) con el fin de permitir que el registro de activación sea recuperado (y la activación actual sea descartada) cuando la llamada actual finalice. El apuntador a la activación actual por lo regular se conoce como apuntador de marco, o fp, por las siglas del término en inglés, es decir, frame pointer, y generalmente \e mantiene en un registro (a menudo también conocido como fp). La información acerca de la activación anterior por lo común se conserva en la activación actual como un apuntador hacia el registro de ac- tivación previo al que se conoce como vínculo de control o vínculo dinámico (dinkmico, porque apunta al registro de activación del eleinento que Ilama durante la ejecución). En ocasiones este apuntador es denominado fp antiguo, porque representa el valor anterior del fp. Por lo general este apuntador se conserva en algún lugar a la mitad de la pila, entre el área de parámetro y el irirea de variable local, y apunta a la ubicación del vínculo de con- trol del registro de activación previo. Adicionalmente, puede haber un apuntador de pila, o sp (por las siglas del término en inglés, es decir, stack pointer). que siempre apunta a la ú1- tima ubicación asignada en la pila de llamada (la cual a menudo se denomina tope del apun- tador de pila, o tos, por las siglas del término en inglés, es decir, top of stack pointer).

Veamos algunos ejemplos.

Considere la implementación recursiva simple de1 algo~itmo de Euclides para calcular el máximo común divisor de dos enteros no negativos, cuyo código (en C) se proporciona en la figura 7.3.

Fisura '1 3 #incLude <stdio.h>

Código en C para el ejemplo 7.2 int x , y ;

int gcd( int u, int V)

( if (V == O) return u;

else return gcd(v,u % v);

1

main ( )

{ scanf ( "%ddad" , &x, &Y) ;

printf("%d\n".gcd(x,y));

return O;

1

Page 358: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 7 1 AMBIENTES DE E J E ( U C I ~ N

Suponga que el usuario introduce los valores 15 y 10 a este programa, de manera main inicialmente hace la llamada gcd (15, 1 0 ) . Esta llamada da como resultado u gunda llamada recursiva gcd (lo, 5 ) (puesto que 15 % LO = S), y ésta produ tercera llamada gcd (5 ,O) (puesto que 10 % 5 = O), lo que devuelve entonces el v Durante la tercera llaniada el ambiente de ejecución puede visualizarse como en la 7.4. Advierta cómo cada llamada a gcd agrega un nuevo registro de activación de e mente el misino tamaño a la parte superior o tope de la pila, y en cada nuevo regis activación el vínculo de control apunta al vínculo de control del registro de activaci terior. Advierta también que el fp apunta al vínculo de control del registro de activac' tual, de modo que en la siguiente llamada el (P actual se convierte en el vínculo de del siguiente registro de activación.

Ambiente basado en pila

para el ejemplo 1.2 r-

Figura 1.4

vínculo de control dirección de retorno

X: 15

u: 10

vinculo de control dlrecc~ón de retorno

vínculo de control

Área globallestática

Registro de activación de main

Registro de activación de la primera llamada a gcd

Registro de activación de la segunda llamada a gcd

Registro de activación de la tercera llamada a gcd

Dirección de crecimiento de la pila

Después de la llamada final a gcd, cada una de las activaciones es eliminada por tu de la pila, de manera que cuando la sentencia grintf es ejecutada en main. sólo el tro de activación para main y el &rea globallestática permanecen en el ambiente. ( inos el registro de activación de main como vacío. En realidad, contendría inform sería utilizada para transferir el control de regreso al sistema operativo.)

Finalmente, obwvamos que no ue necesita espacio en el elemento que llama para los valores de argumento en las llamadas a gcd (a diferencia de la constante 3 en el ambiente FORTRAN77 de la figura 7.2), ya que el lenguaje C emplea parámetro~ de valor. Este pun- to se analizará con más detalle en la sección 7.5.

Ejemplo 7.3 Considere el código C de la figura 7.5. Este código contiene variables que serán utilizadas para ilustrar otros puntos niás adelante en esta sección, pero su Funcionamiento básico cs de

Page 359: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

ura ?,5 grama e n C ejemplo 7.3

Ambientes de ejecución basados en pila

int x = 2;

void g(int); / * prototipo * /

void £ (int n)

{ static int x = 1;

g(n);

x- -;

1

void g(int m)

main ( )

< g(x): return O;

1

la manera siguiente. Ida primera Ilamada desde main es a g ( 2 ) (puesto que x tiene el vü-

lor 2 en ese punto). En esta llamada, m se convierte en 2 micritras que y toma el valor de 1. Entonces, g hace la Ilainada f (1) , y f a su vez hace la llamada g ( 1). En esta llanta- da a g, m se convierte en I y y en O, de modo que no se hacen mis llarnadas. El ambiente de ejecución en este punto (durante la segunda llamada a g) se muestra en la figura 7.6a) (página 356).

Ahora las llamadas a g y f salen de escena (con f disniinuyendo su variable local está- tica x antes de regresar), sus registros de activación son extraídos de la pila y se devuelve el control al punto directamente a continuación de la llamada a f en la primera llamada a g . Ahora g disminuye la variable externa x y hace una llamada adicional g ( 1) , la cual establece m a I y y a O, lo que da conio resultado el ambiente de ejecución mostrado en la figura 7.6b). Una vez que ya no se hacen Ilamadas adicionales, los registros de activación restantes son extraídos de la pila, y el programa llega a su fin.

Advierta cómo, en la figura 7.6b), el registro de activación de la tercera Ilnmada a g ocu- pa (y sobreescribe) el irea de memoria previamente ocupada por el registro de activación de f . Observe también que la variable esttítica x en f no puede ser asignada en un registro de ac- tivación tle f . puesto que debe corttinuar n triivcs (le todas I:is Il:tnt;idas a f . De este modo, debe ser asignada en i I irea glohiillcstitica jimto con la variable externa x, atril criandt) no wa una variable global. En efecto. n o puede haber coiilusión con la variable extcrna x por- q~ie la tabla (le símbolos sicnipre las tlistirigikí y dctwminari la v:iri;ihle correcta para tener :receso a cada puritti del progrartxi.

Page 360: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Firj~lra 1.6 (a)

(a) Ambiente de ejecución x (from f): 1

del programa de la figura 7.5 durante la regunda llamada

a cr vinculo de control

dirección de retorno

vinculo de control dirección de retorno

vínculo de control dirección de retorno

Área global/estática

Registro de activación de main

Registro de activación de la llamada a g

Registro de activación de la llamada a f

Registro de activación de la llamada a g

(b) Ambiente de ejecución (b) x (from f ) : Area global/estática

del programa de la figura 7.5 durante la tercera llamada Registro de act~vación

m: 1

fp-, vínculo de control dirección de retorno

Registro de activación de la llamada a g

Registro de activación de la llamada a g

Una herramienta útil para el análisis de estructuras de llamada complejas en un pr grama es el árbol de activacián: cada registro (o Ilamada) de activación se convierte en u nodo en este &bol, y los descendientes de cada nodo representan todas las llamadas hechas durante la llanada conespondiente a ese nodo. Por ejemplo, el árbol de activación del grama de Ia figura 7.3 es lineal y se representa (para las entradas 15 y 10) en la figura 7 mtentras que el árbol de activación del programa de la figura 7.5 se ilustra en la Figura 7. Adkierta que los ambientes inoatrados en las figuras 7.4 y 7.6 representan los ambientes durante las llamadas representadas por cada una de las hojas de los árboles de activación. En general, la pila de los regi5tros de activación al principio de una llamada particular ticnc una e5tructura equivalente n la trayectoria desde el nodo correspondiente en el irbol de ac- tivación hasta la r a í ~ .

Acceso a nombres En un ambiente basado en pila, ya no se puede tener acceso a los paráme- Iros y las variables iocales rnediante direcciones fijas como en un ambiente completan~cnte cst5tico. En v c ~ de eso, tlchen ser eiicontradas mediante despl:tmmiento desde cl apuntador

Page 361: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ambientes de ejecucion basados en pila

main ( )

l

de marco actual. En la mayoría de los lenguajes el desplrzamiento para cada declaración local se calcula todavía de manera estática mediante el compilador, puesto que las declara- ciones de un procedimiento esrán fijadas en tiempo de compilación y el tamaño de memoria para asignarse a cada declaración está fijado por su tipo de datos.

Consideremos el procedimiento g e n el programa C de la figura 7.5 (véanse también los ambientes de ejecución ilustrados en la figura 7.6). Cada registro de activación de g tiene exactamente la misma forma, y el parámetro m así como la variable local y, se encuentran siempre en exactamente la misma ubicación relativa en el registro de activación. Llamenios a estas distancias moff set y yOf f set. Entonces, durante cuaiquier llamada a g, tenemos la siguiente ilustración del arnhiente local

/ vínculo de control / t rn~ffset £ P - ~ . - L

dirección de retorno

Se puede tener acceso tanto a m como a y mediante sus desplazamientos ("offsets") fija- dos desde el fp. Por ejemplo, supongamos concretamente que la pila de ejecución se in- crementa desde las direcciones de memoria más altas hasta las más bajas, que las varia- bles enteras requieren de 2 bytes de almacenamiento y que las direcciones requieren de 4 bytes. Con la organización de un registro de activación como el que se muestra, tenemos que mof f set = +4 y yOff set = -6, y las referencias a m y a y pueden escribirse en código de máquina (suponiendo convenciones de ensamblador estándar) como 4 ( fp) y - 6 ( fp), respectivament+.

Las estructuras y arreglos locales sólo tienen dificultad para asignar y calcular direccio- nes en el caso de las variables simples, como lo demuestra el ejemplo siguiente.

Ejemplo 7.4 Considere el procedimiento C

void f (int x, char c)

{ int aflO1;

double y;

Page 362: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 7 1 AMBIENTES DE EJECUCIÓN

El regi~tro de activación para una llamada a f aparecería como

-71 - De8pkir

- ----_Desplazamiento

vinculo de control

dirección de retorno I I i

y, suponiendo dos bytes para enteros, ciiatro bytes para direcciones, un byte para caractere ocho bytes para punto flotante de precisión doble, tendríamos los siguientes valores de des- plazamiento (suponiendo nuevamente una dirección negativa de creeimientu para la pila todos los cuales se calculan en tiempo de comp.ilación:

Nombre Desplazamiento

X +S C + 4 a - 24 Y -32

(aquíd factor de 2 en el producto 2 * i es el factor de escala resultante de la suposi- ción de que los valores enteros ocupan dos bytes). Un acceso a memoria así, dependiendo

§ de la ubicación de i y la arquitectura, puede necesitar una simple instruccióri.

No puede tenerse acceso a los nombres estiticos y no locales en este ambiente de 13 mis- rna manera que :i los nornbrcs locales. Eri realidad, en el caso que estamos considerando aquí (Icnguajes 'on auscncia de procediniientos locales) ti;dos los nombres no 1oc;iles son glob:i- Ics y. por 10 tanto, csti(iicos. De este motlo, en la Figura 7.6, la variable x externa (global) d<: C ticne una uAicacicín est;itica ~ i j ;~ . y :%sí se pucdc tener ;icccsi) a ella directairicnte (o n~edi:iritc

Page 363: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejemplo 7.5

Ambientes de ejecución basados en pila 359

desplazamiento desde algún otro apuntador base aparte del fp). Se tiene acceso a la variable local estática x de f exactamente de la misma manera. Advierta que este mecanismo imple- nienta ámbito estático (o léxico), como se describió en e1 capítulo anterior. Si se desea ámbito dinániico, entonces se requiere de un niecanistno de acceso niis complejo (éste se describirá rnás adelante en esta sección).

La secuencia de llamada La secuencia de Ilamüda ahora comprende aproximadamente los p;,sos siguientes.* Cuando un procedimiento es llamado,

1. Calcula los argumentos y los almacena en sus posiciones correctas en el nuevo regis- tro de activaciún del procedimiento (esto se logra insertándolos en orden en la pila de ejecución).

2. Almacena (inserta) el fp como el vínculo de control en el nuevo registro de acti- vaci0n.

3. Cambia el fp de manera que apunte al principio del nuevo registro de activación (si existe un sp, esto se consigue copiando éste en el fp en este punto).

4. Almacena la dirección de retorno en el nuevo registro de activación (si es necesario). 5. Realiza un salto hacia el código del procedimiento a ser llamado.

Cuando un procedimiento sale de escena,

1. Copia el fp al sp. 2. Carga el vínculo de control en el fp. 3. Realiza un salto hacia la dirección de retorno 4. Cambia el sp para extraer los argumentos.

Considere la situación precisamente antes de la última llantada a g en La figura 7.6b):

/ 1 (resto de la pila) 1

/ dirección de retorno 1 de la llamada a g

1 i vínculo de control fR-+ 1

Cuando se realiza la nueva llamada a g, se inserta primero el valor del parhet ro m en la pila de ~jecución:

Registro de activación

8. Esta <lescripcidn ignora coiilquier grab:icih~ de registros que deba tener lugar. T:~nibién igiiiirn I:i iiccesidad dc siru;ir un virlor <le retorno cn tina iihic;iciOn dispoiiihle

Page 364: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 7 1 AMBIENTES DE ~ J E C U C ~ ~ N

1 1 (resto de la pila) 1 vinculo de control 7:. Registro de activación

dirección de retorno de la llamada a g

Entonces el fp se inserta en la pila:

/ 1 (resto de la pila) 1

vinculo de control En- Resistro de activación - f 1 dirección de retorno 1 de ¡a llamada a g

vínculo de control

Ahora se copia el sp en el fp, la dirección de retorno se inserta en la pila y se hace el a la nueva llamada de g:

(resto de la p~la)

vínculo de control

dirección de retorno i i

vinculo de control fP -+

SP -,

Registro de activac~ón de la llamada a g

Nuevo registro de activación de la llamada a g

' espacio libre 1

Page 365: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ambientes de ejecucibn basados en pila 36 I

Finalmente, g asigna e inicializa la nueva y en la pila para completar la construcción del nuevo registro de activacibn:

1 1 (resto de la pila) 1 m: 2

-vínculo de control Regfstro de activación

dirección de retorno de la llamada a g

y : 1

m: 1

Nuevo registro de act~vación vínculo de control

f~ - de la llamada a g

dirección de retorno l v l

Tratamiento de datos de longitud variable Hasta ahora hemos descrito una situación en la cual todos los datos, ya sean locales o globales, pueden encontrarse en un lugar fijo o en un des- plazamiento fijo del fp que puede ser calculado por el cornpilador. En ocasiones un compi- lador debe tratar con la posibilidad de que los datos puedan variar, tanto en el número de objetos de dato? como en el tamaño de cada objeto. Dos ejemplos que se presentan en lenguajes que soportan amh~entes basados en pila son 1) el número de argumentos en una llamada puede tener variaciones de una llamada a otra, y 2) el tamaño de un paráinetro de arreglo o una vanable de meglo locd puede variar de llamada en llamada.

Un ejemplo típico de la situación número 1 es la función p r in t f de C, donde el nú- mero de argumentos 5e determina a partir de la cadena de formato que se pasa como el pri- mer argnmento. Por consiguiente,

tiene cuatro argumentos (incluyendo la cadena de formato "%¿i%s%cl'), mientras que

tiene sólo un argumento. Los compiladores de C por lo regular manejan esto insertando los argumentos para una llamada en orden inverso en la pila de ejecución. Entonces, c1 primer parhmetro (el cual le dice al código para p r i n t f cuántos parámetroi 1n5s hay) está siempre ubicado en ctn desplazamiento fijo desde el fp en la implementación descrita anteriormente (de hecho -+4, crnpleando las suposiciones del ejemplo anterior). Otra opción cs utilizar tiii tnecanisrno preprocesador corno el ap íarguinent pointer, apuntador de argu-

I tnentos) en las itrquitecttir;~~ VAX. Ésta y otras posibilidades se tratan con mis dctnlle en los I

Page 366: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 7 1 AMBIENTE5 DE E J E C U C I ~ N

Un ejemplo de la situación número 2 es el arreglo no restringido de tida:

type Int-Vector is

array(1NTEGER ranga e > ) of INTEGER;

grocadure Sum (low,high: INTEGER;

A: Int-Vector) return INTEGER

is

temp: IntArray (low..high);

begin

... end Sum;

(Advierta que la variable local temp también tiene un tamaño impredecible.) Un método pico para manejar esta situación es emplear un nivel extra de indirección para los datos longitud variable, en tanto que se almacena un apuntador a los datos reales en una uhicaci que puede ser predecida en tiempo de compilación, mientras se realiza la asignación rea el tope de la pila de ejecución de una manera que pueda ser administrada mediante el sp rante la ejecución.

Ejemplo 7.6 Dado el procedimiento Sum de Ada, como se definió anteriormente, y suponiendo 1 misma organización para el ambiente que antes,' podríamos implementar un registro de tivación para sum de la manera siguiente (esta imagen muestra, en concreto, una llamada Sum con un arreglo de tamaño 10):

f 1 (resto de la pila) 1 low: ... high: . . .

tamaño de A : 10 t vínculo de control

dirección de retorno

i: ...

. . .

Registro de activación de la llamada a sum

Area de datos de longitud variable

Ahora, por ejemplo, el acceso a A [ i ] puede con\eguirse calcu1;indo

!J. Eho en re:ilid:td no es siilicicnte para Ada, el cual perniite pmccdimientos ;ini<fados: véase cl ;iii:ilisis inis :iciel;iiite en esta sccciiín.

Page 367: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ambientes de ejetution basados en pila 363

Nuevo registro de activación de la llamada a f (por ser creado)

donde la @ significa indirección, y donde de nueva cuenta suponemos que hay dos bytes para enteros y cuatro bytes para direcciones. 9

Advierta que en la implementación descrita en el ejemplo anterior, el elemento que Ila- rn:i debe conocer el tamaño de cualquier registro de activación de Swn El tamaño de la par- te del parrímetro y la parte de administración es conocido por el compilador en el punto de llamada (puesto que se pueden contar los tamaños de argumentos y la parte de administra- ción es igual para todos los procedimientos), pero el tamaño de la parte de la variable local, por lo común no se conoce en el punto de Ilamada. De este modo, esta implementación re- quiere que el compilatlor calcule previamente un atributo de tamaño de variable locd para cada procedimiento y lo almacene en la tabla de símbolos para su uso posterior. Las varia- bles locales de longitud variable pueden ser tratadas de manera similar.

Vale la pena reiterar que los arreglos en C no caen dentro de la clase de tales datos de longitud variable. En efecto, los arreglos en C son apuntadores, de modo que los parámetros de arreglo son pasados por referencia en C y no asignados localmente (y no conducen infor- mación de tamaño).

Declaraciones anidadas y temporales locales Éstas son dos complicaciones más para el ambiente de ejecución básico basado en pila que merecen mencionarse: las declaraciones anidadas y temoorales locales.

Las temporales locales son resultados parciales de c6lculos que deben ser grabados a tra- vés de las Ilamacias de procedimiento. Considere, por ejemplo, la expresión en C

En una evaluación de izquierda a derecha de.esta expresión, es necesario grabar tres re- sultados parciales por medio de la llamada a f : la dirección de x [i ] (para la asignación pendiente), la suma i+ j (en espera de la multiplicaciún), y el cociente i / k (pendiente de la suma con el resultado de la llamada f ( j ) ). Estos resultados parciales podrían calcularse en registros y grabarse y recuperarse según algún mecanismo de administración de registros. o podrían almacenarse como temporales en la pila de ejecución antes de la llamada a f . En este último caso la pila de ejecución puede tener el aspecto que sigue en el punto justo antes de la llamada a f :

I l (resto de la pila)

vinculo de control dirección de retorno

dirección de x I i 1

resultado de i/ j

Registro de activación del procedimiento que contiene la expresión

Pila de temporales

En esta situación, la secuencia de llamada descrita previamente utilizando el sp funciona sin cambio. De manera alternativa, el compilador también puede calcular fácilmente la posición del tope de la pila a partir del fp (cuando no hay datos de longitud variable), ya que el nú- mero de teniporales requeridos es una cantidad cn tiempo de compilación.

Page 368: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 7 1 AMBIENTES DE EJECUCI~N

Las declaraciones anidadas presentan un problema semejante. Considere el código e

void p ( int x, double y) í char a;

int i;

. . . A: { double x;

int j;

... 1 ...

B:{ char * a; int k;

. . . 1 . . .

1

En este código tenemos dos bloques (también llamados sentencias compuestus) etiquet A y B, anidados dentro del cuerpo del procedimiento g, cada uno con dos declaracio locales cuyo ámbito se extiende sólo sobre el bloque en el cual se encuentran localizados decir, hasta la siguiente llave de ciene). Las declaraciones locales de cada uno de estos ques no necesitan asignarse hasta que se entre al bbque, y las declaraciones del bloque el bloque B no necesitan asignase simultáneamente. Un compilador trntariu un bloque la misma manera que trataría un procedimiento y crearía un nuevo registro de activación c da vez que se entre a un bloque y lo descartaría a la salida. Sin embargo, esto sería inefic puesto que tales bloques son mucho más simples que los procedimientos: un bloque así tiene parimetros ni dirección de retorno y siempre se ejecuta de inmediato, en vez del que se llama de otra parte. Un método más simple es manejar las declaraciones en bloques ani- dados como se inanejan las expresiones temporales, asignándolas en la pila a medida que se entra al bloque y desasignándolas a la salida.

Por ejemplo, justo después de introducir el bloque A en el código C de muestra que se acaba de proporcionar, la pila de ejecución tendría el aspecto que sigue:

resto de la pila) l i Registro de activación de la llamada a P

fR--.

Área asignada al bloque A

3P-,

Y:

vínculo de control

dirección de retorno

I espacio libre

Page 369: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ambientes de ejecución basados en pila

y justo después de entrar al bloque B tendría el aspecto que sigue:

(resto de la pila)

vínculo de control fu --,

dirección de retorno

Registro de activación de la llamada a P

Área asignada al bloque B

Al realizar una implementación de esta naturaleza se debe tener cuidado de asignar declara- ciones anidadas de manera tal que los desplazamientos desde el fp del bloque de procedi- miento circundante se puedan calcular en tiempo de compilación. En particular, tales datos deben ser asignados antes de cualquier dato de longitud variable. Por ejemplo, en el código que se acaba de dar, la variable local j para el bloque A tendría un desplazaniiento de - 17 desde el fp de p (suponiendo otra vez 2 bytes para enteros, 4 bytes para direcciones, 8 bytes para números reales de punto flotante y I byte para caracteres), mientras que k en el bloque B tendría un desplazamiento de - 13.

7.3.2 Ambientes basados en pila con procedimientos locales Si se permiten declaraciones de procedimiento local en el lenguaje que se está compilando, entonces el ambiente de ejecución que hemos descrito hasta ahora es insuficiente, puesto que no se ha previsto nada para referencias no locales y no globales.

Considere, por ejemplo. el código en Pascal de la figura 7.8, página 366 (en Ada podrían escribirse programas similares). Durante la llamada a q el ambiente de ejecución podría parecerse al de la figura 7.9. Si se utiliza la regla de ámbito estático estándar, cualquier mención de n dentro de q debe hacer referencia a la variable entera local n de p. Como po- demos ver en la figura 7.9, esta n no se puede encontrar utilizando algo de la información de administración que se conserva en el ambiente de ejecución hasta ahora.

Se podría encontrür a n empleando tos vínculos de control, si estamos dispuestos a acep- tar el ámbito dinámico. Al observar la figura 7.9 vemos que la n en el registro de activación de r se podría encontrar siguiendo el vínculo de control, y si r no tuviera declaración den. ciit.onces la n de p se podría encontrar siguiendo un segundo vínculo de control (este proce- so se conoce como encadenamiento, un niétodo que veremos de nuevo en breve). Desgra- ciaciatncnte, no sólo esto irnple~nenta el ámbito dinümico, sino que los tiesplazamientos pa- ra los cuales n puede encontrarse pueden variar con tfifercntcs ll:tm;idas (observe que la n cn r tiene un desplazamiento diferente de la n cn g). De cstc niodo, cn tina iniplcnienlacicín

Page 370: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

1

CAP. 7 1 AMBIENTES DE EjECuClbN i

I .,

así, las tablas de símbolos locales de cada procedimiento deben conservarse durante la eie-:.,-i cución con el fin de permitir que un identificador sea buscado en cada registro de activ para ver s i existe, y determinar su desplazamiento. Ésta es una complicaciún adiciona portante para ekambiente de ejecución.

F i ~ u r a 1.8 program nonLocalRef;

Programa en Pascal que muestra la referencia no procedure p;

local y no global var n: integer;

procedure q;

begin

( * una referencia a n es ahora

Figura 7.9 Pila de ejecución para e l programa de la Cgura 7.8

no local no global * )

end; ( * g * )

procedure r(n: integer);

begin

S ;

end; ( * r * )

begin ( * p * )

n := 1;

r(2);

end; ( * p * )

begin ( * main * )

P 1

end.

vinculo de control direcc~ón de retomo

vinculo de control

vínculo de control dirección de retorno

SP - espacio libre

Registro de activación del programa principal

Registro de activación de la llamada a p

Registro de activación de la llamada a r

Registro de activación de la llamada a q

Page 371: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Firlura 7 10 P~la de elecuoón para el programa de la C p a 7.8 con vinculos de acceso agregados

Ambientes de ejecución basados en pila 3 67

La solución a este problema. que también irnpleinenta el ámbito estático, es agregar una pieza extra de inforinación de administración que se conoce como vínculo de acceso a cada registro de activación. Este vínculo de acceso es como el vínculo de control, sólo que éste apunta al registro de activación que representa el ambiente de d@nición del procedimiento en vez de hacerlo al ambiente de llamada. Por esta razón, el vínculo de acceso en ocasiones también se conoce como vínculo estatico. aun cuando no es una cantiJüd propia del tiempo de compilación.'"

La figura 7.10 muestra la pila de ejecución de la figura 7.9 modificada para incluir vínculos de acceso. En este nuevo ambiente, los vínculos de acceso de los registros de acti- vación, tanto de r como de q. apuntan al registro de activación de g, ya que tanto r como q están declarados dentro de p. Ahora una referencia no local a n dentro de q provocará que se siga el vínculo de acceso, en donde n se encontrará en un desplazümiento tl,jado, puesto que éste siempre será un registro de activación de p. Por lo común. eslo puede conseguirse en código cargando el vínculo de acceso en un registro y posteriormente accediendo a n mediante desplazamiento desde este registro (que ahora funciona como el fp). Por qjemplo, utilizando las convenciones de tamaño descritas anteriormente, si el registro r se utiliza pa- ra el vínculo de acceso, entonces se puede tener acceso a n dentro de p como -6(f) después de que r se haya cargado coi1 el valor 4jfp) (el vínculo de acceso tiene desplazamiento +4 desde el fp de la figura 7.10).

Advierta que el registro de activación del procedimiento g mismo no contiene ~ínculo de acceso, como se mdica por el comentario entre picoparéntesis en la ubicación donde po- dría ir. Esto se debe a que g es un procedimiento global, de modo que ninguna referencia no local dentro de g debe qer una referencia global, y se tiene acceso a él mediante el me-

&

- - -

canismo de referencia global. De este modo, no hay necesidad de un vínculo de acceso. (De hecho, se puede insertar un vínculo de acceso nulo o de otro modo arbitrario sólo para ser consistentes con los otros procedimientos.)

El caso que hemos estado describiendo es realmente la situación más simple, donde la referencia no local es a una declaración en el siguiente ámbito exterior, También es posible que las referencias no locales hagan referencia a declaraciones en ámbitos más distantes. Considere, por ejemplo, el código en la figura 7.11.

LO. El procedimiento de definicid es por supuesto conocido, pero no la uhicaci<in exacta de sii registro de activirciún.

<no hay vinculo de acceso> vínculo de control

dirección de retorno n: 1 - n: 2

vínculo de acceso -- vinculo de control

dirección de retorno

vinculo de acceso--- vínculo de control

dirección de retorno

Registro de activación del programa principal

- Registro de activación de la llamada a Q

Registro de activación de la llamada a r

Registro de activación de la llamada a q

Page 372: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

~ i ~ u r a j.11 Código en Pascal que muestra el encadenamiento de acceso

CAP. 1 1 AMBIENTES DE E I E C U C I ~ N

grograni chain;

grocedure g; var x: integer;

procedure q;

procedure r; begin

x := 2;

i f ... then p; end; ( * r * )

begin

r ; end; (* q * )

begin

9;

end; ( * p * )

begin ( * main * )

B ; end.

En este código el procedimiento r es declarado en el procedimiento q, el cual a su es declarado en el procedimiento p. De este modo, la asignación para x dentro de r, hace referencia a la x de g, debe atravesar dos niveles de ámbito para encontrar x. La if ra 7.12 muestra la pila de ejecución después de la (primera) llamada a r (puede haber má de una llamada a r, puesto que r puede llamar de manera recursiva a p). En este ambien te, x debe ser alcanzada siguiendo dos vínculos de acceso, un proceso que se denomina encadenamiento de acceso. Este encadenamiento de acceso es implementado al ir a buscar de manera repetida el vínculo de acceso utilizando el vínculo previamente alcanzado como si fuera el fp. Como ejemplo concreto, se puede tener acceso a x de la figura 7.12 (utilizan- do las conuenciones de tamaño anteriores) de la manera siguiente:

Cargar 4(fp) en el registro r.

Cargar 4(r) en el registro r.

Acceder ahora a x como -6(r).

Para que el método de encadenamiento de acceso funcione, el compilador debe ser capaz de determinar a través de cuintos niveles de anidación se hará el encadenamiento antes de tener acceso al nombre localmente. Esto requiere que el compilador calcule previamente un atributo de nivel de anidación para cada declaración. Por lo regular, el ámbito más remoto (el nivel de programa principal en Pascal o el ámbito externo de C) tiene asignado el nivel de anidación 0, y cada vez que se introduce una función o procedimiento (durante la com- pilación), el nivel de anidación se incrementa en I , y se tiecrementa en la inisrna cantidad a la salida. Por ejemplo, en el código de la figura 7.1 1 e1 procedimiento g tiene asign~do el nivel de anidación O porque es global; la variable x tiene asignado el nivel de anidación 1, por- que el nivel de anidacidn se increnienta cuando se introduce p: el proccdiniiento q también riene asignado un nivel de anidación 1, porque es local a g, y e1 procediniiento r tiene asigna- do el nivel de anidación 2, porque nuevamente el nivel de anidación se incrernenta cuando se introduce q. Finalmente, dentro de r el nivcl tic anid;tción nuev:nnente se incrcrncnta a 3.

Page 373: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ambientes de ejecución basados en pila

espacio libre 8D -- r --1

i)

- -t

fo-

Registro de activación del programa principal

Registro de activación de la llamada a p

<no hay vínculo de acceso> vínculo de control

dirección de retorno x : . . .

vinculo de acceso vinculo de control

dirección de retorno

vinculo de acceso vincuto de control

dirección de retorno

Registro de activación de la llamada a q

C-

Registro de activación de la llamada a r

Ahora la cantidad de encadenamiento necesaria para tener acceso a un nombre no local 5e puede determinar comparando el nivel de anidación en el punto de acceso con el ni- vel de anidación de la declaración del nombre; el número de vínculos de acceso a seguir es la diferencia entre estos dos niveles de anidación. Por ejemplo, en la situación anterior, la asignación a x se presenta en el nivel de anidación 3, y x tiene nivel de anidación 1, de modo que deben seguirse dos vínculos de acceso. En general, si la diferencia entre niveles de anidación es m, entonces el código que debe uef generado para el encadenaniiento de acceso debe tener m cargas para un registro r, la primera utilizando el fp, y el resto uti- lizando r.

Puede parecer que el encadenamiento de accew es un método ineficiente para el acce- so de variables, ya que es necesario ejecutar una larga secuencia de instrucciones para cada referencia no local con una gran diferencia de anidación. Sin embargo, en la práctica, los ni- veles de anidación rara vez tienen más de dos o tres niveles de profundidad, y la mayoría de las referencias no locales son para variables globales (nivel de anidación O), a las cuales .ie puede continuar teniendo acceso mediante los métodos directos previamente analizados. Existe un método para implementar vínculos de acceso en una tabla de búsqueda indizada mediante nibe1 de anidación que no conduce al gasto de ejecución del encadenamiento. La estructura de datos utilizada para este método se denomina despliegue ("display"). Su es- tructura y uso se tcitan en los ejercicios.

l a secuencia de llamada Los cambios a la secuencia de Ilamada necesarios para tmplementar los vínculos de acceso son relativamente directos. En la implementación mostrada, durante una llamada, el vínculo de acceso debe ser insertado en ia pila de ejecución justo antes del fp, y después de una salida el sp debe ser ajustado mediante una cantidad extra para elimi- nar tanto el vínculo de acceso como los argumentos.

El único problema es el de encontrar el vínculo de acceso de un procedimiento durante una Ilamada. Esto se puede conseguir empleando la información del nivel de anidacibn (en tiempo de coinpilacibn) adjunta a la declaración del procedimiento que se e\tá llamando. En redidad, todo lo que necesitarnos hacer es generar una cadena de acceso, precisamente como si fuéramos a tener acceso a una vuflable en el tnisrno nivel de anidacirín que el del proccdi- mento que se e\tá llamando. La dirección así calculada cerá la del vínculo de accew apropia- (lo. Naturalmente, \i el procedimiento es local (la ditcrencia cn nicele\ de anidacilin e5 O), el vínculo de accew y el vínculo de control wn iguales (y también wn iguales a l fp en el punto dc la Ilarna<la).

Page 374: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 7 1 AMBIENTES DE EJECUCION

Figura 1.13 Pila de ejecución despues de la segunda llamada a r en el código de la Cgura 7.1 1

Considere, por ejemplo, la Ilamada r i q desde dentro de r en la figura 7.8. En el de r estanios a nivel de anidación 2, mientras que la declaración de q conlleva un anidación de l (porque q es local a p y dentro de p el nivel de anidación es 1). De e ma, se reqiiiere un paso de acceso para calcular el vínculo de acceso de q, y, efectiv en la figura 7.10, el vínculo de acceso de q apunta al registro de activación de p (y es mo que el vínculo de acceso de r).

Advierta que incluso en la presencia de activaciones múltiples del ambiente de d ción, este proceso calculará el vínculo de acceso correcto, ya que el cálculo es real en tiempo de ejecución (utilizando los niveles de anidación en tiempo de compilación tiempo de compilación. Por ejemplo, dado el código de la figura 7.1 1, la pila de ej después de la segunda llamada a r (suponiendo una llamada recursiva a g) se p al de la figura 7.13. En esta ilustración r tiene dos registros de activacidn diferen dos vínculos de acceso diferentes, apuntando a los diferentes registros de aCtivación los cuales representan diferentes ambientes de definicidn para r.

vínculo de control dirección de retorno

d

-

1 vínculo de acceso vínculo de control r dirección de retorno

Registro de activación del programa principal

<no hay vínculo de acceso> vínculo de control

dirección de retorno x:. . .

vínculo de acceso vinculo de control

Registro de activación de la llamada a p - Registro de activación de la llamada a q

'dirección de retorno P

vínculo de aeceso- vínculo de control

dirección de retorno Registro de activación de la llamada a r

Registro de activación de la llamada a p

Registro de activación de la llamada a q

Registro de activación de la llamada a r

7.3.3 Ambientes basados en pila con parámetros de procedimiento

En algunos lenguajes no sólo se permiten procedimientos locales, sino que los procedimien- tos también se pueden pasar como paránietros. En un lenguaje así, cuando se llama un pro- cedimiento que ha sido pasado como un parámetro, es imposible para un compilador gene- rar cddigo que permita calcular el vínculo de acceso a1 niomento de la Ilamada, de la manera en que se describió en la sección anterior. En ver: de esto, se debe calcular previamente el

Page 375: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ambientes de ejecución basados en pila 37 1

F~qura 7 14 Cbdigo en Parcal estándar ton un procedim~ento como parámetro

grocedure r;

begin

writeln(x);

end;

begin

x := 2;

pfr);

end; ( * q * )

begin ( * main * )

9;

end.

vínculo de acceso para un procedimiento y pasarse junto con un apuntador al código para el procedimiento cuando éste se pasa como un parámetro. De esta forma, un valor de parime- tro de procedimiento ya no puede ser visto como un simple apuntador de código, sino que también debe incluir un apuntador de acceso que defina cl ;unbicntc en el cual las referencias no locales son resueltas. Este par de apuntadores (un apuntador de código y un vínculo de xceso, o bien, un apuntador de instrucción y un apuntador de ambiente) juntos represen- tan el valor de un parimetro de procedimiento o función y se tes conoce coniúnmente como cerradura (porque el vínculo de acceso "cierra" los "huecos" causados por las referencias no locales).' ' Escribiremos las cei~aduras como <¡p. ep>, ip para referirnos al apuntador de instrucción (apuntador de código o apuntador de entrada) del procedimiento, y ep para re- ferirnos al apuntador de ambiente (vínculo de acceso) del procedimiento.

Considere el programa en Pascal estándar de la figura 7.14, el cual tiene un proccdirnieuto p3 con un parámetro a que también es un procedimiento. Después de la llarnada a p en q, en la cual el procedimiento local r de q es pasado a g, la Ilamada a a dentro de g en realidad llama a r , esta llamada todavía debe encontrar la variable no local x en la activación de q. Cumdo p es llamado, a es constmido como una cerradura <ip, ep>, donde ip es un apuntador :ti código de r y ep es und copia del fp en el punto de la llamada (es decir, apunta 4 ambiente de ia llamada a q en la cual está definida r) . El valor del ep de a se indica mediante la línea discontinua de la figura 7.15 (página 372), la cual representa el ambiente justo después de la llamada a g en q. Entonces, cuando a es llamada dentro de p, el ep de a es utilizado como el vínculo estático en su registro de activación, como se indica en la figura 7.16. 5

program closureEx(output);

procedure ~(procedure a);

begin

a;

end ;

procedure g;

var x: integer;

Page 376: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

372 CAP. 1 / AMBIENTES DE E J E C U C I ~ N

Figura 7.15 Pila de ejecución justo después de la llamada a g en el código de la figura 1.14

f igu ra 1.16 Pila de ejecución justo después de la llamada a a en el código de la ligura 1.14

<no hay vínculo de acceso> vinculo de control

, /

I , a:<ig,,_ep>

íño3áfvi~culo de acceso, £9 - vínculo de control

dirección de retorno ---¡ sg .---,

ospacio libre

<no hay vínculo de acceso> vínculo de control

, /

I \

vinculo de acceso vinculo de control

Registro de activación . , ,.,. del programa principal . . .~. -<: ..' . .

, ~ , . ~ ~ . . ~ ,... ,A,. .~, .+$ ,..~".. ...., ..,

Registro de activación ..,' . ~

,',.. . .. ....~.. de la llamada a Q

, . ... ,..

Registro de activación de la llamada a p

Registro de activación del programa principal

Registro de activación de la llamada a Q

Registro de activación de la llamada a p

Registro de activación de la llamada a a

La secuencia de llamada en un ambiente como el que acabamos de describir debe ah ra distinguir claramente entre procedimientos ordinarios y parámetros de procedimiento. U procedimiento ordinario, como antes, es llamado buscando el vínculo de acceso por medi del nivel de anidación del procedimiento y saltando directamente al código del proce miento (el cual es conocido en tiempo de compilación). Un parametro de procedimient otra parte, ya dispone de un vínculo de acceso almacenado en el registro de activación el cual debe ser obtenido e insertado en el nuevo registro de activación. La ubicación cúdigo para el procedimiento, no obstante, no se conoce directamente por el compilador; en su lugar debe realizarse una llamada indirecta al ip almacenado en el registro de activación actual.

Un escritor de compiladores puede querer evitar, por razones de simplicidad o uniformidad, esta distinción entre procedimientos ordinarios y parjmetros de procedimiento y conservar te- tlos los procedimientos como cerraduras en el ambiente. En realidad, cuanto mis general es un lenguaje en su tratamiento de procedimientos, m& razonable se vuelve un método así. Por ejemplo, si se permiten variables de procedimiento, o si los valores de procedimiento se pue- den calcular dinámicaniente, entonces la representación <¡p. ep> de los procedimientos se vuelve un requcriniiento todas esas situaciones. La figura 7.17 muestra que el ambien- te de la figura 7.16 ditría la impresión de que todos los valores de procedimiento están al- m;icen;dos en el mibiente como cerraduras.

Page 377: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Memoria dinámica

P:~.P,,-> ~ r e a globallestática q:<iP,,->

Registro de activación del programa principal

<no hay vínculo de acceso> vínculo de control

dirección de retorno Registro de activación de la llamada a q

r: <ig,, eg,>

a:<ip,, ep,,

vínculo de acceso> Registro de activación vínculo de control de la llamada a p

vínculo de acceso vínculo de control Regfstro de activacidn

dirección de retorno de la llamada a a

espacio libre 1 Finalmente, observamos que tanto C como Modula-2 y Ada evitan las complicaciones

dc~critas en esta subsección: C. porque no tiene procedimientos locales (aunque tiene va- riables y parámetros de procedimiento); Modula-?, porque tiene una regla especial que restringe los valores de parámetro de procedimiento y variable de procedimiento a procedi- mientos globales; y Ada, porque no tiene variables o parámetros de procedimiento.

MEMORIA DINAMICA

1 Ambientes de ejecucibn completamente dinámicos Los ambientes de ejecución basados en pila que se analizaron en la sección anterior son las formas más comunes de ambiente entre los lenguajes imperativos estándar tales como C, Pascal y Ada. Sin embargo, tales ambientes tienen limitaciones. En particular, en un len- guaje donde una referencia a una variable local en un procedimiento puede ser devuelta al elemento que llama. ya sea de manera implícita o explícita, un ambiente basado en pila pro- ducirá una referencia colgante cuando se salga del procedimiento, ya que el registro de activación del procedimiento será desasignado de la pila. El ejemplo más h p l e de esto es cuando la dirección de una variable local es devuelta, como en el código de C:

int * dangle(void) í int x ;

return &x;)

Una asignación addr = dangle í ) causa ahora que addr apunte a una ubicación in- segura en la pila de activxiún cuyo valor puede ser cambiado arbitrariamente por llamadas posteriores a cualquier procedimiento. C evita este problema declarando simplemente que un programa así es erróneo (aunque el compilador no dará un mensaje de error). En otras pal;tbras, la semántica de C está constririda alredcdor del ambiente tundaincntal htsado cn pila.

Page 378: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 1 1 AMBIENTES DE EJECUCION

Un ejemplo algo más complejo de una referencia colgante se presenta si una futici' cal puede ser devuelta mediante una Llainacka. Por ejemplo, si C permitiera dclinicio función local, el código de la figura 7.18 produciría una referencia colgante indire parámitro x de g, a1 cual se puede tener acceso llamando a f después de que g ha sali escena. Naturalmente, C evita este problema prohibiendo procedimientos locales. lenguajes, como Modula-2, que tienen procedimientos locales adenxís de variables de cedimiento, parámetros y valores devueltos, deben establecer una regla especial que erróneos a tales programas. (En Modula-2 la regla es que sólo los procedimientos glob pueden ser argumentos o valores devueltos: una retirada importante incluso de paráme de'procedimiento estilo Pascal.)

Fivure ? 18 typedef int ( * proc) (void) ;

CSdigo preudo-l que muestra una refertncta colgante proc gfint x)

causada por el retorno de { int f (void) / * función local ilegal */ una functón local { return x; }

return E; 1

main ( )

{ proc c;

c = g(2);

printf("%d\nw,c0); / * debería imgrimir 2 */ return O;

1

Sin embargo, existe una extensa clase de lenguajes donde tales reglas son inaceptable es decir, los lenguajes de programación funcionales, conlo LlSP y ML. Un principio esen- cial en el diseño de un lenguaje funcional es que las funciones sean tan generales como sea posible, y esto significa que las funciones deben poderse definir localmente, pasarse como parámetros y devolverse como resultados. De este modo, para esta amplia clase de lenguajes, un ambiente de ejecución basado en pila es inadecuado, y se requiere una forma de ambiente más general. Llamaremos a un ambiente así completamente dinámico, porque pueden desasignar registros de activación sólo cuando han desaparecido todas las referencias a ellos, y esto requiere que los registros de activación sean dinámicamente liberados en momentos arbitrarios durante la ejecución. Un antbiente de ejecución completamente itináinico es mucho más coinplicado que un ambiente basado en pila, ya que involucra el seguimiento de las re- ferencias durante la ejecución, y la capacidad de encontrar y desasignar áreas inaccesibles de memoria en momentos arbitrarios durante la ejecución (este proceso se denomina reco- lección de basura).

A pesar de la complejidad adicional de esta clase de anihiente, la estructura básica de un . .

registro de activación permanece igual: el espacio debe ser asignado para par6mctros y va- riables locales, y todavía se neccsitan vínculos de acceso y de control. Naturalmente, ahora c~i'indo el control es tleiuelto al elemento que llama (y el vínculo de control es utilizado pa- ra restaurar el ambiente :interior), el registro de activación que wle pennnnecc en la rnerno- ria, para ser desusigriatlo en algún momento posterior. De esta forma, tod:i la complejidad ;id¡cional de csw ;mhiente puede ser encapsulada en un administrador de menioria que i-eetnpliiia las operaciones de la pila de ejecucirín con rut¡ti:is inis gciici-:iIcs de asignacitin

Page 379: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Memoria dinimica 375

y desasignación. Analizaremos algunas de estas cuestiones en el diseño de un administrador de memoria de esta naturaleza, más adelante en esta sección.

74.2 Memoria dinámica en lenguajes orientados a objetos Los lenguajes orientados a objetos requieren de mecanismos especiales en el ambiente de ejecución para incrementar sus características adicionales: objetos, métodos, herencia y fijación dinámica. En esta subsección proporcionamos una breve perspectiva general de la variedad de técnicas de implementación para estas características. Suponemos que el lector está familiarizado con la temiinologia y conceptos básicos orientados a objetos.'"

Los lenguajes orientados a objetos varían en gran medida en sus requerimientos para el ambiente de ejecución. Smalltalk y C+ + son buenos representantes de los extremos: Smailtdk requiere un ambiente completamente dinimico similar al de LISP, mientras que gran parte del sfuerzo de diseño en C + + se ha ido en retener el ambiente basado en pila de C, Fin la necesidad de administración de memoria dinámica autom6tica. En ambos lenguajes un ob- jeto en memoria puede ser visto como una combinación de una estructura de registro tradi- cional y un registro de activación, con las variables de instancia (miembros de dalos) como los campos del registro. Esta estructura difiere de un registro tradicional en la manera en que se accede a las características de herencia y métodos.

Un mecanismo directo para iniplementar objetos podría Fer copiar por código de inicia- lización todas las cnructerísticas (y métodos) comúnmente heredadas de manera directa en la estructura del registro (con los métodos como apuntadores de código). Sin embargo, esto implica un gasto excesivo de eFpacio. Una alternativa es mantener una descripción cornple- ta de la estructura de clase en la memoria en cada punto durante la ejecución, con la heren- cia mantenida mediante apuntadores de superclase, y todos los apuntadores de método con- servados como campos en la estructura de clase (esto se corioce en ocasiones como grófica de herencia). Cada objeto conserva entonces, junto con campos para sus variables de ins- tancia, un apuntador a su clase de definición, a través del cual son encontrados todos los mé- todos (tanto locales como heredados). De esta manera, los apuntadores de método se registran sólo una vez (en la estructura de clase) y no se copian en la memoria para cada objeto. Este mecanismo también irnplementa herencia y fijación dinimica, porque tos métodos son en- contrados mediante una búsqueda de la jerarquía de clase. La desventaja de esto es que. mientras que las variables de instancia pueden tener desplazamientos preúecibles (del mismo modo que las variables locales en un ambiente estándar), no ocurre lo mismo con los méto- dos, por lo que deben ser mantenidos por nombre en una estructura de tabla de símbolos con capacidad de búsqueda. Aun así, ésta es una estructura razonable para un lenguaje aitamente dinámico como Smalltalk, donde pueden ocurrir cambios a la estructura de clase durante la ejecución.

Una alternativa a conservar toda la estructura de clase dentro del ambiente es calcular la lista de apuntadores de código para métodos disponibles de cada clase, y dmacenarla en la memoria (estática) como una tabla de función virtual (en la terminología de C+ +). És- ta tiene la ventaja de que puede Eer arreglada de manera que cada método tenga un despla- zamiento predecibIe, y ya no es necesario un recorrido en la jerarquía de clase con una serie de búsquedas en tabla. Ahora cada objeto contiene un apuntador a la tabla de función iir- tual apropiada, en veL de a la ratructura de clase. (Por supuesto, la ubicación de este ,ipun- tador también debe tener un despla~ainiento predecible.) E m cimplificación rOlo funciona rt la misma estructura de claw e\tá tijada antes de la ejecución. Es el rnétodo de clección en C+ t.

- 12. El an6lisis siguiente iarnhi&n supone que siilo es13 disponible la herencia Ginple. I..:I iwrciici:~

inúltiple se irata en alguno de los trabajos citados en I:I scccicin de notas y ~refcrenci;is.

Page 380: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

3 76

Ejemplo 7.8

CAP. 7 1 AMBIENTES DE EJECUCI~N

Considere las siguientes declaraciones de clase de C++:

class A

{ public:

double x , y ;

void £0; virtual void g0 ;

k;

class B: public A

{ public:

double z;

void f0; virtual void h( ) ;

1.;

un objeto de clase A aparecería en ra siguiente:

la memoria (con su tabla de función

apuntador de tabla tabla de funcibn vrrtual para A

mientras que un objeto de clase B aparecería como se muestra a continuación:

de función virtual tabla de función virtual para B

Advierta cómo el apuntador de función virtual, una vez agregado a la estructura del objeto manece en una ubicación fija, de manera que se conoce su desplazamiento antes de la ción. Advierta tainbikn que la función f no obedece a la fijación dinámica en C+ + (puesto no es declzada "virtud"), y de esta forma no aparece en la tabla de función virtual (ni en gún otro sitio en el ambiente); una llamada a f se resuelve en tiempo de compilación.

7.4.3 Administración del apilamiento o monticulo (heap) En la sección 7.4.1 analizamos la necesidad de un ambiente de ejecución que fuera más dinámico que el ambiente basado en pila utilizado en la mayoría de los lenguajes com- piladores, si queríamos que las funciones generales tuvieran soporte completo. Sin embar- go, en la mayoda de los lenguajes, incluso un ambiente basado en pila necesita de algunas capacidades dinámicas con el fin de manejar la asignación y desasignadn de apuntadores. La estructura de datos que maneja una asignación así se denomina cipilumirnto o montículo thetrp), y este apilamiento por lo regular es asignado como un bloque lineal de mcmoria, de tal

Page 381: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Memoria dinamica 377

manera que pueda aumentar, si es necesario. al tiempo que interfiere lo menos posible con la pila. (En la página 347 mostramos el apilamiento situado en un bloque de memoria eti el extremo opuesto del irea de la pila.)

Hasta ahora en este capítulo nos hemos concentrado en la organi~ación de los registros de activación y de la pila de ejecución. En esta sección queremos describir cómo se puede administrar el apilamiento, y cómo se pueden extender las operaciones de apilamiento para proporcionar la clase de asignación dinámica requerida en lenguajes con capacidades de función generales.

Un a ilamiento o montículo proporciona dos operaciones, asignación (ullocare) y libe- ración & ee). La operación de asignación toma un parámetro de tamaño (ya sea explícita o implícitamente), por lo regular en bytes, y devuelve un apuntador a un bloque de memoria del tamaño correcto, o un apuntador nulo si no existe ninguno. La operación de liberación toma un apuntador a un bloque de memoria asignado y lo marca como libre de nuevo. (La operación de liberación también debe ser capaz de descubrir el tamaño del bloque que será liberado, ya sea de manera implícita o mediante un parámetro explícito.) Estas dos operacio- nes existen bajo diferentes nombres en muchos lenguajes: se denominan new y d i a p o s e en Pascal, y new y delete en C+ +. El lenguaje C tiene vanas versiones de estas opera- ciones, pero las versiones básicas son conocidas como malloc y f ree y son parte de la biblioteca estándar ( s t d l i b . h), donde tienen esencialmente las declaraciones siguientes:

void * malloc (unaigned nbytes);

void free (void * ap) i

Utilizaremos estas declaraciones como la base para nuestra descripción de la administración del apilamiento.

Un método estándar para mantener el apilamiento e implementar estas funciones es em- plear una lista ligada circular de bloques libres, de los cuales la memoria se toma mediante malloc y se devuelve mediante free. Esto tiene la ventaja de la simplicidad, pero también tiene desventajas. Una de éstas es que la operación free no puede decir si su argumento de apuntador esta en realidad apuntando a un bloque legítimo que fue previamente asignado mediante malloc. Si el usuario llegara a pasar un apuntador inválido, entonces el apilamien- to se corrompería rápida y fácilmente. Una segunda, pero mucho menos grave, desventaja es que debe tenerse cuidado de fusionar bloques que son devueltos a la lista libre con blo- ques que se encuentran adyacentes, de manera que resulte un bloque libre de tamaño máxi- mo. Sin esta fusión, el apilamiento rápidamente se fragmenta, es decir, se divide en un gran número de bloques pequeños, de modo que la asignación de un bloque grande puede fallar, aun cuando haya suficiente espacio total disponible para asignarlo. (La fragmentación por supuesto puede oc&r incluso sin que se produzca este fenómeno de fusión.)

Aquí ofrecemos una implementación hasta cierto punto diferente d e m a l l o c y f ree que utiliza una estructura de datos de lista ligada circular, la cual se mantiene al tanto de bloques asignados y liberados (por lo que es menos susceptible a la corrupción) y tiene también la ventaja de proporcionar bloques autofusionables. El código se proporciona en la figura 7.19 (página 379).

Este código utiliza un arreglo asignado estáticamente de tamaño MEMSIZE como apila- miento, pero también se podría emplear una llamada del sistema operativo para asignar el api- I d e n l o . Definimos un tipo de datos Header ("Encabezado") que mantendrá la itifomü- ción de administración de cada bloque de memoria, y definimos el arreglo del apilamiento para que tenga elementos de tipo Header, de manera que la información de administración pueda niantenerce fkilmente en los misnios bloques de memoria. El tipo Header contiene trei \egmentos de información: un apuntador al siguiente bloque en la l i m el tamaño del eípacio 'tsignado actualmente (que viene enseguida cn memoria) y el tmafio de cualquier eípxio t i - hre que le siga (zi es que hay alguno). Por con\ig~~icnte, hloqtie en la I~rta c\ de la forma

Page 382: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

tAP. 7 1 AMBIENTES DE E~EcUCI~N

/+h freesize

1 espaciousado 1

La definicibn del tipo Header en la figura 7.19 también utiliza una declaración u n i o un tipo de datos A l i g n (que hemos establecido a d o u b l e en el código). Esto con el fi "alinear" los elementos de memoria en un límite de byte razonable y, dependiendo del tema, puede o no ser necesario. Esta complicación puede ignorarse sin consecuencias en resto de esta descripción.

El único segmento adicional de datos necesario para las operaciones del apilamiento un apuiitador a uno de los bloques en la lista ligada circular. Este apuntador se denomi m e m p t r y siempre apunta a un bloque que tiene algún espacio libre (por lo regular e1 tno espacio por asignarse o liberarse). Es inicializado a NULL, pero en la primera llam malloc se ejecuta el ccítligo de inicialización que est~blece a memptr a principio del arr glo del apilarniento e inicializa el encabezado en el arreglo de la manera siguiente:

memptr > next

Este encabe~ado inicial que se asigna en la primera llamada a malloc nunca \era liberado. tlliora liay un bloque en la lista, y el resto del código de r n a l l o c busca la lista y devuelve un nuevo bloque a partir del primer bloque que tenga suficiente eipacio libre (esto es un al- goritmo de acceso primero). De este modo, después de, digamos, tres llamadas a m a l l o c , la lista tendría un aspecto como el siguiente:

usado

usado memptr +

usado

Page 383: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Código en C para mantener

lista de apuntadores hacia blaques tanto usados como

Memoria dinámica

#define NULL O #define ElEMSIZE 8096 / * cambio para tamaños diferentes * /

typedef double Align; typedef union header

{ struct ( union header *next; unsigned usedsize; unsigned freesize;

1 S; Align a;

1 Header;

static Header mem[MEMSIZEI ; static Header *memptr = WLLI

void *malloc (unsigned nbytes) i Header *p, *newp; unsigned nunits; nunits = (nbytes+aizeof(Header)-l)/aizeof(Header) + 1; if (memptr == NULL) i memptr->s.next = memptr = mem; memptr->s.usedsize = 1; memgtr->s.freesize = MEMSIZE-1;

1 for(p=memptr;

(p->s.next!=memgtr) && (p->s.freesize<nunits); g=g->a.next);

if (p->s.freesize < nunits) return NULL; / * no hay blogue suficientemente grande * / newp = p+p->s.usedsize; newp->s.usedsize = nunits; newp-zs.freesize = p->s.freesize - nunits; newp->s.next = p->s.next; p->s.freesize = 0; p->s.next = newp; memptr = newpt return (void * ) (newp+l);

1

void free(void *ap) i Header *bp, *p, *prev; bp = (Header *) ap - 1; for ~prev=memptr,p=memgtr->s.next;

(p!=bp) && (g!=memptr); prev=g,p=p->s.next); if (p!=bp) return; / * lista dañada, no hacer nada * / prev->s.freesize += p->s.usedsize + p->s.freesize; prev->s.next = p->s.next; memptr = prev;

1

Page 384: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 7 1 AMBIENTES DE E J E C U C I ~ N ! 1 L I

Advierta que a medida que los bloques se asignan en sucesión, se crea un nuevo bl cada vez, y el espacio libre que deja el bloque anterior se va con él (de manera que el es libre del bloque desde el cual tiene lugar la asignación siempre tiene f r e e a i z e - e s t do a cero). 'Tambien, memptr sigue la construcción de los nuevos bloques, y así a siempre a un bloque con algo de espacio libre. Advierta también que m a l l o c sie crementa el apuntador al bloque recientemente creado, de manera que el encabe protegido de ser sobreescrito mediante el programa cliente (mientras que en la me vuelta s61o se utilizan desplazamientos positivos).

Ahora consideremos el código para el procedimiento f r e e . Primero decrementa e tador pasado por el usuario para encontrar el encabezado del bloque. Luego busca en un apuntador que sea idéntico a éste, con lo que protege a la lista de la cormpción, y t permite que se calcule el apuntador a i bloque anterior. Cuando se encuentra, el bloque es minado de la lista, y tanto su espacio utilizado como el libre se agregan al espacio libre bloque anterior, fusionando así de manera automática el espacio libre. Observe que me también se establece rara auuntar al bloque que contiene la memoria recién liberada. . .

Como ejemplo supongamos que se libera el bloque de en medio de los tres bloqu lizados en la ilustración anterior. Entonces el apilamiento y la lista de bloque asociad él tendrían el aspecto siguiente:

menptr - F R \ usado

I usado I

7.4.4 Administración automática del apilamiento o montículo (heap) El uso de malloc y f ree para realizar asignación y desasignación dinámica de apuntador es un método manuai para la administración del apilamiento, puesto que el programador de- be escribir llamadas explícitas para asignar y liberar memoria. En contraste, la pila de ejeeu- ción es administrada automáticnmente mediante la secuencia de llamada. En un lenguaje que necesite un ambiente de ejecución completamente dintímico, el apilamiento debe ser adminis- trado automáticamente de la misma manera. Por desgracia, mientras que las llamadas a m a l l o c se pueden programar fácilmente en cada llamada de procedimiento, las llamadas a f ree no se pueden programar de manera similar al salir de escena, debido a que los registros de activación deben mantenerse hasta que hayan desaparecido todas las referencias a ellos. De este modo, la admini\tración de memoria automática involucra el reclamo del almacenamiento previamente asignado pero ya no utilizado, tal vez tiempo después de que fue asignado, y sin una llamada explícita a free. Este proceso se denomina recolección de basura.

Reconocer cuando un bloque de almacenamiento ya no está referenciado, ya sea directa o indirectamente a través de apuntadores, es una tarea mucho más difícil que la de mantener una lista de bloques de alniacenamiento en forma apilada. La técnica estándar consiste en realizar recolección de basura de marcado y barrido.I3 En este método no se libera memoria hasta que falla una llamada a m a l l o c , en cuyo punto se activa un recolector de basura que

13. Una alternativa más simple Ilamada eonteo de referencia se emplea tamhih ocasionalniente. Vhse la seccih de notas y referencias.

Page 385: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Mecanirmor de paro de parametros 38 1

busca todo el almacenamiento que puede ser referenciado y Libera todo el almacenamiento no referenciado. El procedimiento se realiza en dos pasos. El primer paso sigue a todos los apuntadores de manera recursiva, comenzando con todos los valores de apuntador actual- mente accesibles, y marca cada bloque de almacenamiento alcanzado. Este proceso requiere un bit de almacenamiento extra para el marcado. El segundo paso hace entonces un barrido lineal a través de la memoria, devolviendo los bioctues no marcados a la memoria libre. Aun- que este proceso por lo general encontrará suficiente memoria libre contigua para satisfacer una serie de nuevos requerimientos, es posible que la memoka esté todavía tan fragmenta- - da que aún no pueda satisfacer un requerimiento grande de memoria, incluso después de que se haya realizado la recolección de basura. Por lo tanto, una recolección de basura por lo re- gular también realiza compresión de memoria moviendo todo el espacio asignado a un ex- tremo del apilamiento, dejando sólo un bloque grande de espacio libre contiguo en el otro extremo. Este proceso también debe actualizar todas las referencias a aquellas áreas en la memoria que fueron desplazadas dentro del programa en ejecución.

La recolección de basura de marcado y barrido tiene varias desventajas: requiere de alma- cenamiento extra (para las marcas), y el doble paso a través de la memoria provoca un retar- do importante en el procesamiento, en ocasiones tanto como algunos segundos, cada vez que se invoca el recolector de basura, lo que puede tomar algunos minutos. Esto evidentemente es . * - inaceptable para muchas aplicaciones que involucran una respuesta inmediata o interactiva.

La administración de este proceso se puede mejorar dividiendo la memoria disponible en dos mitades y asignando almacenamiento s61o a partir de una mitad a la veL. Entonces, durante el paso de marcado, todos los bloques alcanzados son inmediatamente copiados a la segunda mitad del almacenamiento que no está en uso. Esto ~ignifica que no se requerirá un bit de marcado extra en el almacenamiento, y sólo se requerir6 de un paso. También realiza la compresión de manera automática. Una vez que todos los bloques alcanzables en el área utilizada han sido copiados, la mitad utilizada y la no usada de la memoria son intercambia- das y el procesamiento continúa. Este método se denomina recolección de basura de paro y copia o de espacio doble. Desgraciadamente, no hace mucho por mejorar los retardos del procesamiento durante la petición de almacenamiento.

Recientemente, fue inventado un método que reduce este retardo en forma significativa. Denominado recolección de basura generacional, agrega un área de almacenamiento per- manente al esquema de petición del párrafo anterior. Los objetos asignados que sobreviven tiempo suficiente simplemente son copiados en espacio permanente y nunca son desasigna- dos durante las peticiones posteriores de almacenamiento. Esto significa que el recolector de basura necesita buscar sólo una sección muy pequeña de memoria para las asignaciones de almacenamiento más recientes, con lo que el tiempo para una búsqueda así se reduce a una fracci6n de segundo. Naturalmente, todavía es posible que la memoria permanente llegue a agotarse con el almacenamiento no alcanzable, pero esto es un problema mucha menos grave que antes, porque el almacenamiento temporal tiende a desaparecer con rapidez, mien- tras que el almacenamiento que permanece asignado durante algún tiempo tiende a permanecer de cualquier manera. Se ha demostrado que este proceso funciona muy bien, especialmente con un sistema de memoria virtual.

Remitimos al lector a las fuentes enumeradas en la sección de notas y referencias para m6s detalles acerca de éste y otros métodos de recolección de basura.

7.5 MECANISMOS DE PASO DE PAR~METROS Hemos visto cómo, en una llamada a procedimiento, los parámetros corresponden a ubicacio- nes en el registro de activación, las cuales son rellenadas con los argumentos, o valores de pa- rámetros, mediante el elemento que llama, antes de saltar al código del pmedimiento Ilamüdo. De este modo, para el código del procedimiento llamado. un parámetro representa un valor puramente formal al que no está ligado ningún código, pero que sirve sólo püra establecer

Page 386: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 7 / AMBIENTES DE EJECUCION

una ubicación en el registro de activación, donde el código puede encontrar su valor el cual sólo existirá una vez que ha tenido lugar una llamada. En ocasiones se refiere proceso de construir estos valores como a la fijación de los parámetros a los argum tos. Cómo interpreta el código del procedimiento los valores del argumento depen mecanismo o mecanismos de pa.0 de parámetros en particular adoptado por el le fuente. Como ya indicamos, FORTRAN77 adopta un mecanismo que fija los parame ubic:tciones en vez de a valores, mientras que C contempla todos los argumentos co lores. Otros lenguajes, como C+ +, Pascal y Ada, ofrecen una selección de mecanisnio paso de pwámetros.

En esta sección comentaremos los dos mecanismos de paso de parámetros más co nes (el paso por valor y el paso por referencia, referidos a veces como llrimuda por v y Ilarnnila por rejirenciu), además de dos métodos adicionales importantes, el paso valor-resultado y el paso par nombre (también denominado evaluación retarda Algunas variaciones de éstos serán analizadas en los ejercicios.

Una cuestión que el mecanismo de paso de parámetros en sí no aborda es el orden que se evalúan los argumentos. En la mayoría de las situaciones este orden no es impo te para la ejecución de un programa, y cualquier orden de evaluación producirá los mis resultados. En ese caso. por eficiencia u otras razones, un compilador puede elegir variar el - orden de la evaluación de los argumentos. Sin embargo, muchos lenguujes permiten argu- mentos para llamadas que causan efectos laterales (cambios en la memoria). Por ejemplo, la - . llamadade función en c

provoca un cambio en el valor de x, de modo que diferentes órdenes de evaluación tienen diferentes resultados. En tales lenguajes, un orden de evaluación estrúidar puede especificar- se como el de i~yuierda a derecha, o puede dejarie al escritor del compilador, en cuyo caso el resultado de una llamada puede variar de una implementación a otra. Los comp~ladores de C por lo regular evalúan sus argumentos de derecha a izquierda, en vez de izquierda a derecha. Esto ttene en cuenta un número variable de argumentos (tal como en la función grintf), como se conientó en la sección 7.3.1, página 361.

7.5.1 Paso por valor En este mecanismo los argumentos son expresiones que son evaluadas en el momento de la llamada, y sus valores se convierten en los valores de los parámetros durante ta 'ejecución del procedimiento. Éste es el único mecanismo de paso de parámetros disponible eii C, y es el mecanismo predeterminado en Pascal y Ada (Ada también permite que tales parámetros sean explícitamente especificados como parámetros in).

En su forma mrís simple, esto significa que los parámetros de valor se comportan como valores constantes durante la ejecución de un procedimiento, y se puede interpretar el paso por valor como el reemplazo de todos los parjmetros en el cuerpo de un procedimiento median- te los valores de los argumentos. Esta forma de paso por valor es empleada por Ada, donde ta- les parámetros no pueden ser asignados o utilizados de otro modo, por ejemplo como variables locales. Una visión rnás relajada es la de C y Pascal, donde los parknetros de v a 1 or son con- templados esencialmente como variables locales inicializadas, que se pueden utilizar como variables ordinarias, pero los cambios en ellas nunca provocan que tenga lugar algún cam- bio no local.

En un lenguaje como C, que ofrece sólo paso por valor, es iniposible escrihir directamen- te un procedimiento que tenga efecto haciendo cambios a sus paráinetros. Por ejemplo, Iü siguiente función inc2 escrita co C no logra el efecto deseado:

Page 387: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Mecanismos de paro de parámetros

void inc2( int x)

/ * iincorrectol * /

i ++x;++x; 3

Mientras que en teoría es posible, con una adecuada generalidad de funciones, realizar todos los cálculos devolviendo valores apropiados en vez de cambiando valores de pará- metros. los lenguajes como C por lo regular ofrecen un método en el que se eniplea el paso por valor de manera tal que obtenga cambios no locales. En C, esto toma la forma de pasar la dirección en lugar del valor (con lo que se cambia el tipo de datos del pará- metro):

void inc2( int* x)

/ * ahora está bien */ ( ++(*X);++(*X); 3

Por supuesto, ahora, pura incrementar una variable y, esta funcibn debe ser llamada como inc2 (&y). ya que se requiere la dirección de y y no su vaior.

Este método funciona especialmente bien en C para arreglos, ya que éstos son apun- tadores de manera implícita, y de este modo el paso por valor permite que los elementos individuales del arreglo sean cambiados:

void init (int xll , int size) 1* esto funciona bien cuando se llama

como init(a), donde a es un arreglo * / f int i;

for(i=O;i<size;++i) x[il=O;

3

El paso por valor no requiere de esfuerzo especial por parte del compilador. Se implemen- ta fiícilmente tomando la vi~ibn más directa del cálculo del argumento y la constnicción del registro de activación.

75.2 Paso por referencia En el paso por referencia los argumentos deben (al menos en principio) ser variables con ubicaciones asignadas. En vez de pasar el valor de una variable, el paso por referencia pasa la ubicación de la variable, de manera que el parámetro Fe convierte en un alias para el argumento, y cualquier cambio que se haga al partímetro también se hace al argumento. En FORTRAN77, el paso por referencia es el único mecanismo de paso de parámetros. En Pascal, el paso por referencia se consigue utilirando la palabra reservada var y en C+ + el h h o l o eyecial 6 en la declaración de partímetro:

void inc2f int & x)

/ * garámetro de referencia de C++ */ ( ++x;++x; 3

Esta función ahora puede scr llamada sin un uso especial del operador de direcciún: inc2 (y) funciona bien.

El paso por rci'crcncia requiere que el cotripilador calcule la direcciún del argumento (y debe tener tal dirección), la cu;il se üIm;tcena entonces en el registro de activ:tci<jn local.

Page 388: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

El compilador también debe convertir los accesos locales a un piuimetro de referen accesos indirectos, ya que el "valor" local es realmente la dirección en cualquier sitio ambiente.

En 1engua.jes como FORTRAN77, donde sólo se dispone del paso por referencia, p regular se ofrece un acuerdo para argumentos que sean valores sin direcciones. En Iug hacer una llamada como

ilegal en FORTRAN77, un compilador debe "inventar" una dirección para la expre 2 + 3, calcular el valor en esta dirección, y posteriormente pasar la dirección a la llam Por lo general esto se realiza creando una localidad temporal en el registro de activ del elemento que llama (en FORTRAN77, éste será estático). Un ejemplo de esto se cuentra en el ejemplo 7.1 (pigina 350). donde el valor 3 se pasa como un argumento c do una localidad de memoria para el mismo en el registro de activación del procedi principal.

Un aspecto del paso por referencia es que no requiere que se haga una copia del pasado, a diferencia del paso por valor. Esto en ocasiones puede ser importante cuan valor a ser copiado es una estructura grande (o un arreglo en otro lenguaje aparte d C+ +). En tal caso puede ser importante poder pasar un argumento por referencia, per hibir que se hagan cambios al valor del argumento, de esta manera se consigue el pa valor sin el gasto de copiar el valor. Una opción así es proporcionada por C+ +, dond puede escribir una llamada tal como

void f ( conat MuchData & x )

donde EluchData es un tipo de datos con una estructura grande. Esto todavía es paso por referencia, pero el compilador también debe realiar una verificación estática de que x nun- ca va a aparecer a la izquierda de una asignación o, de otro modo, puede ser cambiada.14

1.53 Paso por valor-resultado Este mecanismo consigue un resultado similar al paso por referencia, sólo que no se esta blece un alias real: el valor del argumento es copiado y utilizado en el procedimiento, en- tonces el valor final del parámetro es copiado de vuelta a la ubicación del argumento cuan- do el procedimiento sale de escena. Así, este método se denomina a veces como de copia de entrada, copia de salida o copia de recuperación. Éste es el mecanismo del parámetr out en Ada. (Ada también tiene simplemente un parámetm out, que no tiene valor i pasado; éste podría ser llamado paso por resultado.)

El paso por valor-resultado sólo es distinguible del paso por referencia en presencia de alias. Por ejemplo, en el código siguiente (en sintaxis de C),

void p(int x , int y)

( ++x;

++y;

1

14. E m no \lernpre puede h.rcer\e de manera cornpletmente wgura

Page 389: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Mecanismos de paso de parámetros

mainí )

f. int a = 1;

piara): return O;

a tiene el baior 3 después de que p es llamado si se utiliza el paso por rei'erencra, mientras que a tiene el valor 2 si x emplea el paso por valor-resultado.

Cuestiones que se dejan sin especificar por este mecanismo, y que posiblemente difie- ran en lenguajes o implementaciones diferentes, son el orden en el que los resultados con copiados de vuelta a los argumentos y si las ubicaciones de los argumentos se calculan só- lo a la entrada y se almacenan o si vuelven a calcularse a la salida.

Ada tiene otra peculiaridad: su definición establece que los parámetros i n out pueden de hecho ser implementados como paso por referencia, y cualquier cálculo que fuera dife- rente bajo los dos mecanismos (lo que involucraría un alias) sería un error.

Desde el punto de vista del escritor de compiladores, el paso por valor-resultado requie- re varias modificaciones a la estructura básica de la pila de ejecución y a la secuencia de Ila- mada. En primer lugar, el registro de activación no puede ser liberado por el elemento llamado, puesto que los valores (locales) que s e r h copiados deben continuar disponibles para el elemento que Ilma. En segundo lugar, el elemento que llama debe insertar las di- recciones de los argumentos como temporales en la pila antes de comenzar la construc- ción del nuevo registro de activación, o bien, debe volver a calcular estas direcciones al regreso del procedimiento llamado.

7.54 Paso por nombre Éste es el más complejo de los mecanismos de paso de parámetros. También se le conoce como de evaluiición diferida, puesto que la idea del paso por nombre es que el argumento no sea evaluado hasta su uso real (como un parámetro) en el programa llamado. De este mo- do, el nombre del argumento, o su representación textual en el punto de llamada, reempla- La al nombre del parhmetro que le corresponde. Como ejemplo, en el código

void p(int X)

f ++x; 1

si se hace una llamada tal como p (a [i 1 ) , el efecto es el de evaluar ++ (a [ i ] ) . De esta forma, si i fuera a cambiar antes del uso de x dentro de g, el resultado sería diferente, ya \ea del que se obtuviera mediante el paso por referencia o del que se obtuviera por medio del paso por valor-resultado. Por ejemplo, en el código (con sintaxis de C),

int i;

int, a[101;

void p(int X)

( ++i;

+ + X t

1

Page 390: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 7 1 AMBIENTES DE EJECUCIÓN

main ( ) i = l ;

a [ l l = 1; a[21 = 2;

g i a i i l ) t re turn O;

el resultado de la llamada a p es que a 121 se establece a 3 y a [ 11 se deja sin c m La interpretación del paso por nombre es como sigue. El texto de un argumen

e1 punto de llamada es contemplado como una función por derecho propio, lacual e hada cada vez que el nombre de parámetro correspondiente se alcanza en el códi procedimiento llamado. Sin embargo, el argumento siempre será evaluado en el a te del elemento que llama, mientras que el procedimiento será ejecutado en su am de definición.

El paso por nombre se ofreció como ud mecanismo de paso del parámetro (junt paso por valor) en el lenguaje AlgolóO, pero perdió popularidad por tres razones. mera es que puede dar resultados sorprendentes y contrarios a la intuición en la pres de efectos colaterales (como lo muestra el ejemplo anterior). La segun& es que es difi . .

implementar, puesto que cada argumento debe ser convertido en lo~que es esencialmente un. 4 ,,. procedimiento (en ocasiones conocido como suspensión o interrupcih [<'thunk"l) que., 2 debe ser llamado siempre que el argumento es evaluado. La tercera es que es inefi puesto que no sólo convierte una simple evaluación de argumento en una llamada de cedimiento, sino que también puede provocar que ocurran evaluaciones múltiples. variación de este mecanismo denominada evaluación diferida que se ha vuelto popul los lenguajes puramente funcianales, consiste en evitar la reevaluación memorizan suspensión con el valor calculado la primera vez que se llama. La evaluación diferida d cho puede producir una implementación rnds eficiente, puesto que un argumento que n utiliza nunca tampoco se evalúa nunca. Los lenguajes que ofrecen la evaluación diferida mo un niecanismo de paso de parámetros son Miranda y Haskell. Remitimos al lector a I sección de notas y referencias para más inforn~ación al respecto.

7.6 UN AMBIENTE DE EJECUCION PARA EL LENGUAJE TINY

En esta sección final del capítulo, describiremos la estructura de un ambiente de ejecu para el lenguaje TINY, nuestro ejemplo ejecutable de un lenguaje pequeño y simple para c pilación. Haremos esto aquí en una forma independiente de máquina y remitiremo al siguiente capítulo para un ejemplo de una implementación en una máquina espec

El ambiente que TINY necesita es mucho más simple que cualquiera de los a comentados en este capítulo. En realidad, TINY no tiene procedimientos, y todas bles son globales, de manera que no hay necesidad de una pila o registros de activación, y el único almacenamiento d inhico necesario es el de temporales durante la evaluación de la expresión (éste incluso podría ser estático como en FOR'TRAN77: véanse los ejercicios).

IJn esquema simple para un ambiente TINY es colocar las vtiriables en direcciones ab- solutas en el extremo inferior de la memoria del programa, y asignar la pila temporal en el cvtremo superior. Así, <lado u n programa que utilice, digamos, cuatro variiibles x, y, z y w, estas variables obtendrían las direcciones absolutas del O basta el 3 en la ptirte inferior de la ~iicinoria, y en un punto (iurante la ejecuci611 donde se estén almacenando los tres tenipora- les, el amhicnte de qjecucicín se vcría c«nio sigue:

Page 391: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Un ambiente de ejecucion para el lenguaje TINY

parte superior o tope de memor,ia

1 tew3 1 c- parte superior o tope

parte inferior de la memoria

Dependiendo de la arquitectura, podemos necesitar establecer algunos registros de adminis- traci6n para apuntar a la parte inferior ylo superior de la memoria, para entonces utilizar las direcciones "absolutas" de las variables como desplazamientos desde d apuntador de la par- te inferior, y emplear el apuntador de la parte superior de la memoria como el "tope (parte superior) de la pila oral", o bien, calcular los desplazamientos para los temporales desde un apuntador fijo en la parte superior. Naturalmente, también se podría utilizar la pila del pro- cesador como la pila temporal, si está disponible.

Para implernentar este ambiente de ejecución 1~ tabla de símbolos en el compilador de TlNY debe, como se describió en el último capítulo, mantener las direcciones de las varia- bles en memoria. Se hace esto proporcionando un parámetro de ubicación en la función st-insert e incluyendo una función st-lookug que recupere la ubicación de una variable (apéndice B, lineas 1166 y 1 171):

void st-insert( char * name, int lineno, int loc ) ;

int st-lookup ( char * name );

El analizador semántica debe, a su vez, asignar direcciones a las variables cuando se hayan encontrado la primera vez. Esto se hace manteniendo un contador de ubicación de memoria estático que es inicializado para la primera dirección (apéndice B. línea 1413):

static int location = 0;

Entonces, en cualquier momento que se encuentre una variable (en una sentencia de lectu- ra, una sentencia de asignación o una expresión de itlentiticador), el analizador sernántico ejecuta el cítdigo (apkndice B, línea 1454):

C'uarido st-lookug dcvuelvc - 1. 13 variable ntín no está cri 13 tabla. En cse caso, s i q i s t r n una nucv:i iihic:icií,n, y ci contador de uhic:ici(in se incrcrnenta. De otro riiodo. 13

Page 392: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 7 1 AMBIENTES DE EJECUCI~N

variable ya esta en la tabla, en cuyo caso la tabla de símbolos ignora el parámetro de u cibn (y se escribe O como una ubicación ficticia).

Esto maneja la asignación de las variables nombradas en un programa TINY; la así c i h de las variables temporales en la parte superior de la memoria, y las operaciones ne sarias para mantener esta asignación, serán responsabilidad del generador de código, e1 se analizará en el siguiente capítulo.

DERCfCfOS 7.1 Dibuje una posible organización pan e, ambiente de ejecución del siguiente progrma e FORTRAN77, similar a la de la tigura 7.2 (página 352). Asegúrese de incluir los apunta de memoria como se presentarían durante la llamada a AVE.

REAL A(S1ZE) ,AVE

INTEGER N,I

10 READ *, N

IF (N.LE.O.OR.N.GT.SIZE) GOTO 99

READ *, (A(I),I=l,N) PRINT ", 'AVE = ',AVE(A,N)

GOTO 10

99 CONTINUE

END

REAL FUNCTION AVE(B,N)

INTEGER 1.N

REAL B(N),SUM

SUM = 0.0

DO 20 I=1,N

20 SUM=SUM+B ( 1)

AVE = SUM/N

END

7.2 Dibuje una posible organización para el ambiente de ejecucicin del siguiente programa en C, similar a la de la figura 7.4 (página 354). a. Después de la entrada en el bloque A en la función f. 6. Después de la entrada en el bloque B en la función g.

int a1101;

char * s = "hallo";

int f(int i, int b [ l )

C int j=i;

A:{ int i-j;

char c = b[il;

... 1

return O;

1

Page 393: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejercicios

void g(char * e )

( c h a r c = e[Ol;

B:í int at51;

main ( )

( int x=l;

x = f ( x , a ) ;

g ( a ) ;

return O;

1

Dibuje una posible organizaciún para el ambiente de ejecución del programa en C de la figura 4.1 (página 138) después de la segunda llamada a factor, dada la cadena de entrada ( 2 ) . Dibuje la pila de registros de activación para el siguiente programa en Pascal, que muestre los vínculos de control y de acceso, después de la segunda llamada al procedimiento c. Describa cómo se tiene acceso a la vaiable x desde el interior de c.

program env;

grocedure a;

var x: integer;

grocedure b;

grocedure c;

begin

x := 2;

b;

end;

begin (* b *)

c; end;

begin ( * a *)

b;

end;

begin ( * main * )

a ;

end.

7.5, Dibuje la pila de registros de activación para el siguiente prograrna en Pascal a. Despuks de la 1l;irn;ida a a en la primera llamada de P.

Page 394: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 7 1 AflBIENTES DE EJECUCIÓN

b. Después de la llamada a 'a en la segunda llama& de p. c. ¿,Qué imprime el programa y por qué?

proqram closureEx(output~;

var x: integer;

procedure one;

begin

writeln(x);

end;

procedure g(procedure a);

begin

a;

endi

grocedure q;

var x : integer;

grocedure two;

begin

writeln(x);

end;

beqin

x := a; p(one);

P(tw0);

end; ( * q * )

beqin ( * main * )

x := 1;

'2;

end.

7.6 Considere el siguiente programa en Pascal. Suponga una entrada de usuario compuesta de los tres números 1, 2, O y dibuje la pila de' registrosde aetivaciún cuando el número 1 es impreso la primera vez. incluya todos los vínculos de control y de acceso, además de todos los pntá- metros y variables glob~les, y suponga que todos los procedimientos son :tlmacenados en el ambiente como ccmduras.

proqram procenv(input,output);

procedure dolist (grocedure print);

var x: integer;

procedtire newprint;

beqin

print ;

writeln(x);

und ;

Page 395: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejercicios

begin (* dolist *)

readln(x);

if x = O then begin

print;

print;

end

else dolist(newprint);

end; ( * doliat *)

grocedure null;

begin

end;

begin ( * main * )

dolist(nul1);

end.

7.7 Para efectuar asignación completamente estática, un compilador FOKTRAN 77 necesita cons- tituir una estimación del número mtiximo de temporales requeridos para cualquier cilculo de expresión en un programa. Conciba un método para estimar el número de temporales requeri- dos para calcular una expresión realizando un recorrido del árbol de expresihn. Suponga que las expresiones son evaluadas de izquierda a derecha y que cada subexpresicín i~quierda se debe gnardar en un teinpral.

7.8 En lenguajes que permiten números variables de argumentos en 1l:madas de procedimiento, una manera de encontrar el primer argumento es calcular los argumentos en orden inverso, como se describió en la sección 7.3.1, página 361. a. Una alternativa para calcular los argumentos en reversa sería reorganizar el registro de ac-

tivación para tener disponible el primer argumento incluso en la presencia de argumentos variables. Describa tal organización de registros de activación y la secuencia de llamada que necesitaría.

b. Otra alternativa para calcular los argumentos en reversa es emplear un tercer apuntador (aparte del sp y el fp), el cual por lo regular se denomina ap ("argument pointer", apunta- dor de argumento). Describa una estructura registro de activación que utilice un ap para hallar el primer argumento y la secuencia de llamada que necesitaría.

7.9 El texto describe cómo manejar los parámetros de longitud variable (tales como arreglos abiertos) que son pasados por valor (véase el ejemplo 7.6. página 362) y esrablece que una solución semejante funciona para variables locales de longitud variable. Sin embargo, existe un problema cuando se tienen presentes tanto parámetros de longitud variable como variables lo- cales. Describa el problema y una solución utilizando el siguiente procedimiento de Ada como ejemplo:

t m e IntAr is ArrayfInteger range c > ) of Integer;

. . . procedure f(x: IntAr; n:Integer) is

y: Array(1. .n) o£ Integer;

i: Integer;

begin

... end f ;

Page 396: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 7 1 AMBIENTES DE EJECUCI~N

7. I D Una alternativa al encadenamiento de accesv en un lenguaje con procedimientos locales mantener vínculos de acceso en un arreglo exterior de la pila, indicado por el nivel de a ción. Este arreglo se conoce como despliegue ("display"). Por ejemplo. la pila de ejecuc la figura 7.12 (pitgina 369) se vería como sigue con un despliegue

display [l]

display [al Registro de activación

de la llamada a q

Registro de activación de la llamada a r

mzentras que la pila de ejecucidn de la figura 7.13 (página 370) tendría el aspecto que sigue:

Registro de activación display [l] del programa principal

display [a] Registro de activación

a. Describa cúmo un despliegue puede mejor= la eficiencia de referencias no locales de pro- cedimientos profundamente anidados.

b. Vuelva a hacer el ejercicio 7.4 utilizando un despliegue. c. Describa la secuencia de llamada necesaria para implementar un despliegue. d. Existe un problema al utilizar un despliegue en un lenguaje con paránietros de procedi-

miento. Describa el problenia utilizando el ejercicio 7.5.

Page 397: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejercicios

7.1 1 Considere el siguiente procedimiento en sintaxis de C:

void f( char c. char sL101, double r )

( int * x; int yf51;

a. Con las convenciones de paso de parimetros estándaes de C, y suponiendo que los tamaños de datos son prua enteros = 2 bytes, char = 1 byte, douhle = 8 bytes, dirección = 4 bytes, determine los desplazümientos desde el fp de los siguientes, utilizando la estructura de re-' gistras de activacih descrita en este capítulo: 1) c, 2) 8 [7 1, 3) y 12 1 .

b. Repim el inciso a suponiendo que todos los pruámetros son pasados por valor (incluyendo aneglosf.

c. Repita el inciso a suponiendo que todos los par,hetros son pasados por referencia. 7.12 Ejecute el siguiente programa en C y explique su salida en términos del ambiente de ejecución:

void gívoid)

í (int x; printf ( "%d\nn, x) ;

x = 3 ; )

(int Y;

printf("%i\nW.y);)

1

int* f(void)

i int x; printf ("%d\n",x) ;

return kx;

1

void main( )

int *p;

p = f0; *p = 1;

f 0 ;

9 0 ;

> 7.13 Dibuje el diseño de la memoria de los objetos de las siguientes clases de C - t +, junto con las

tablas de función virtual como se describieron en la sección 7.4.2:

class A

( gublic:

int a;

virtual void f0; virtual void g 0 ;

1:

Page 398: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 7 1 AMBIENTES DE E J ~ C U C I ~ H

c laaa B : public A

( public:

i n t b;

v i r t u a l void f 0 ;

void h 0 ;

1;

c l a s a C : public B

( public:

i n t c;

v i r t u a l void g 0 ;

1

7.14 Una tabla de función virtud en un lenguaje orientado a objetos ahorra recorrer la gráfica de herencia en la blísqueda de un mktodo, pero tiene un costo. Explique cuál es este costo.

7.15 I'mporcitine la salid^ del siguiente programa (escrito con sintaxis de C ) utilizando los cuatro inEtodos de paso de p;whetros comentados cii la sección 7.5:

#include <stdio.h>

i n t i = 0 ;

void p ( i n t x, i n t y )

( x t= 1;

i t= 1;

y +' 1;

)

rnain ( )

( i n t a[21={1,1);

p(aCi1 , a i i l );

pr in t f ( "%d %d\nn,a[O1.a[ll);

r e tu rn O;

f

7.16 Propnrcione la wlida del siguiente programa (con wtaxis de C ) ut~li~ando lo^ cuatro métodos de paso de parámetros comentados en La ~ección 7.5:

#include <stdio.h>

i n t i = 0 ;

void swap(int x , i n t y )

t x = x t y ;

y = x - y ;

x = x - y ;

1

Page 399: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejercicios de programación

EJERCICIOS DE 7.19 Como le describió en la sección 7.5, el paso por nombre, o evaluación retardada, puede ser

PROGRAMaCIdN visto como el empacamiento de un argumento en un cuerpo de función (o suspensión), el cual es Ilamado cada vez que el parámetro aparece en el código. Vuelva a escribir el código en C del ejercicio 7.16 para implementar los parámetros de la función swap de esta manera, y veti- fique que el resultado es en realidad equivalente al paso por nombre.

7.20 a. Como se describió en la sección 7.5.4, $e puede conseguir un mejoramiento en la eíicien- cia del paso por nombre al memorbar el valor de un argumento la primera cez que er evaluado. Vuelva a eicribir su código del ejercicio anterior para implementar dicha mne- tnorización, y compare los resultados con los de ese ejercicio.

b. La memorización puede provocar diferentes resultados del paso por nombre. Explique c6mo puede ocurrir esto.

7.21 La cornpresidn (secci611 7.4.4) puede hacerse en un paso separado de la recolección de basura y puede ser realilatl~ mediante malloc si una petici6n de memoria falla debido a la c;ireticia de un bloque suticictitcmente largo. a. Vuelva a cscrihir el procedimiento malloc ik la sección 7.4.3 para incluir un piso i&

7.17 Suponga que la subrutina P de FORTRAN77 estj declarada corno sigue

SUBROUTINE P (A )

INTEQER A

PRINT *, A

A . A + 1

RETURN

ENn

y que es Ilamada desde el pmgrama principal de la manera siguiente:

En algunos sistemas FORTRAN77, esto ocasionará un error de ejecucih En otros, no ocurri- rá un error de ejecución, pero si la subrutina se llama de nuevo con un 1 como SU argumento, puede imprimir el valor 2. Explique cómo ambos comportamientos pueden ocurrir en términos del ambiente de ejecuci6n.

7.18 Una variación del paso por nombre es el paso por texto, en el cual los argumentos son eva- luados en modo retardado, justo como en el paso por nombre, pero cada argumento es evalua- do en el 'unbiente del procedimiento llamado en vez de hacerlo en el ambiente que llama. a. Muestre que el paso por texto puede tener diferentes resultados que el paso por nombre. 6. Describa una organi~ación de ambiente de ejecución y secuencia de llamada que podrían

utilizarse para implementar el paso por texto.

Page 400: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 1 1 AMBIENTES Df EJECUCI~N

b. La compresión requiere que la ubicaci6n del espacio previamente asignado wmb' significa que un programa debe informarse acerca del cambio. Describa cómo uti tabla de apuntadores a bloques de memoria para resolver ese problema, y vuelva a su código del inciso a para incluirla.

NOTAS Y EI ambiente completamente estático de FORTRAN77 (y versiones más anti

REFERENCIAS FORTRAN) representa un enfoque directo y natural para el diseño de ambientes y lar a los ambientes de ensamblador. Los ambientes basados en pila se hicieron popula la inclusi6n de la recursividad en lenguajes tales como Algo160 ((Naur [1963]). R Russell 119641 describen un temprano ambiente basado en pila de Algo160 con La organización del registro de activación y la secuencia de llamada para algun piladores de C se describe en Johnson y Ritchie 119811. El uso de un despliegue en I cadenas de acceso (ejercicio 7.10) se describe con detalle en Fischer y LeBlanc incluyendo los problemas al utilizarlo en un lenguaje con parámetros de procedimi

La administración de memoria dinümiea se analiza en muchos libros acerca de turas de datos, tal como Aho, Hopcroft y Ullman 119831. Una útil perspectiva ge ciente se ofrece en Drozdek y Simon [1995]. El código para implementación de ma f ree que es semejante, aunque ligeramente menos sofisticado, al código dado en la se 7.4.3, aparece en Kernighan y Ritchie 119881. El diseño de una estructura de apilami para su uso en la compilación se comenta en Fraser y Hanson 119951.

Un panorama general de la colección de basura se presenta en Wilson 119921 o Co 1198 11. Un colector de basura generacional y ambiente de tiempo de corrida para el ML del 1 guaje funcional se describe en Appel[1992]. El compilador de lenguaje funcional Gofer (Jon 119841) contiene tanto el colector de basun de marcar y barrer como el de dos espacios.

Budd [1987] describe un ambiente completamente dinámico para un pequeño sistema Smalltalk, incluyendo el uso de la grafica de herencia, y un recolector de basura con con- teos de referencia. El uso de una tabla de función virtual en C++ se describe en Ellis y Stroustrup [1990], junto con extensiones para manipular herencia múltiple.

Más ejemplos de técnicas de paso de parámetros pueden hallarse en Louden [1993], donde también se puede encontrar una descripción de la evaluación diferida. Las técnicas de implementación piira la evaluación diferida pueden estudiarse en Peyton Jones [1987].

Page 401: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generación de código

Código intermedio y estructu- ras de datos para generación de código

Técnicas básicas de generación de código

Generación de código de refe- rencias de estructuras de datos

Generación de código de sen- tencias de control y expresio- nes lógicas

Generación de código de llamadas de procedimientos y funciones

Generación de código en compiladores comerciales: dos casos de estudio

TM: una máquina objetivo simple

Un generador de código para el lenguaje TlNY

Una visión general de las técnicas de optimización de código

Optimizaciones simples para el generador de código de TlNY

En este capitulo volveremos a la tarea final de un compilador. la de generar código ejecutable para una máquina objetivo que sea una fiel representación de la semántica del código fuente. La generación de código es la fase más compleja de un compilador, puesto que no sólo depende de las características del lenguaje fuente sino también de contar con información detallada acerca de la arquitectura objetivo, la estructura del ambiente de ejecución y el sistema operativo que esté corriendo en la máquina objetivo. La generación de código por lo regular implica también algún intento por optimizar, o mejorar, la velocidad y10 el tamaño del código objetivo recolectando más información acerca del programa fuente y adecuando el código generado para sacar ventaja de las características especiales de la máquina objetivo, tales como registros, modos de direccionamiento, distribución y memoria caché.

Debido a la complejidad de la generación del código, un compilador por lo regular divide esta fase en varios pasos. los cuales involucran varias estructuras de datos inter- medias, y a menudo incluyen alguna forma de código abstracto denominada código intermedio. Un compilador también puede detener en breve la generación de código ejecutable real pero, en vez de esto genera alguna forma de código ensamblador que debe ser procesado adicionalmente por un ensamblador, un ligador y un cargador, los cuales pueden ser proporcionados por el sistema operativo o compactados con el compilador. En este capítulo nos concentraremos únicamente en los fundamentos de la generación del código intermedio y el código ensamblador, los cuales tienen muchas características en común. Ignoraremos el problema del procesamiento adicional del código ensamblador

Page 402: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 GENERACI~N DE CÓDIGO 4

en cbdigo ejecutable, el cual puede ser controlado más adecuadamente mediante un lenguaje ensamblador o sistemas de texto de programación.

En la primera sección de este capítulo analizaremos dos formas populares de có intermedio, el código de tres direcciones y el código P, y comentaremos algunas de propiedades. En la segunda sección describiremos los algoritmos básicos que permit generar código intermedio o ensamblador. En secciones posteriores se analizarán téc de generación de código para varias características del lenguaje, incluyendo expresi sentencias de asignación y sentencias de flujo de control, tales como sentencias if y s cias while y llamadas a procedimientolfunción. A estas secciones les seguirán estu de caso del código producido para estas características mediante dos compiladores merciales: el compilador C de Borland para la arquitectura 80x86 y el compilador C Sun para la arquitectura RlSC de Sparc.

En una sección posterior aplicaremos las técnicas que permiten desarrollar un nerador de código ensamblador para el lenguaje TINY estudiadas hasta aquí. Como generación de código en este nivel de detalle requiere una máquina objetivo real, pri analizaremos una arquitectura objetivo simple y un simulador de máquina denominado TM, para el cual se proporciona un listado fuente en el aphdice C. A continuac' describiremos el generador de código complejo para TINY. Finalmente, daremos visión general de las técnicas para el mejoramiento u optimización del código estánd y describiremos cómo algunas de las técnicas recién adquiridas pueden incorporarse el generador de código de TINY.

CÓDIGO INTERMEDIO Y ESTRUCTURAS DE DATOS PARA GENERACIÓN DE CÓDIGO

'< Una estructura de datos que representa el programa fuente durante la traducción se denomi- : na representación intermedia, o IR (por las siglas del término en inglés) para abreviar.

En este texto hasta ahora hemos usado un árbol sintáctico abstracto como el IR principal. Además del IR, la principal estructura de datos utilizada durante la traducción es la tabla de símbolos, la cual se estudió en el capítulo 6.

Aunque un árbol sintktico abstracto es una representación adecuada del código fuente, incluso para la generación de código (como veremos en una sección posterior), no se pare- ce n i remotamente al código objetivo, en particular en su representación de constnicciones de flujo de control, donde el código objetivo, como el código de máquina o código ensam- blador, emplean saltos mas que consírucciones de alto nivel, como las sentencias if y while. Por lo tanto, un escritor de compiladores puede desear generar una nueva forma de repre- sentaciición intermedia del árbol sintiictico que se parezca más al código objetivo o reemplace del todo al árbol sintáctico mediante una representación intermedia de esa clase, y entonces genere código objetivo de esta nueva representación. Una representación intermedia de esta naturaleza que se parece al c6digo objetivo se denomina código intermedio.

E l código intermedio puede tomar muchas formas: existen casi tantos estilos de código intermedio como compiladores. Sin embargo, todos representan alguna forma de linealización del árbol sintáctico, es decir, una representación del árbol sintáctico en forma secueircial. E l &digo intermedio puede ser de muy alto nivel, representar todas las operaciones de ruane- ra casi tan abstracta como un árbol sintjctico, o parecerse niucho ti1 código objetivo. Puede o no utilizar informiición detallada acerca de la máquina objetivo y el ambiente de cjccu- sicíri, corno 10s tamaños de los tipos de datos, las ubicaciones de Lis variables y la disponi- bilidad de los registros. Puede i) no incorporar toda la inforrnaciiín contenida en la tnbl;i

Page 403: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Código intermedio y estructuras de datos para generación de codigo 399

de sínibolos, tül como los ámbitos, los niveles de anidación y los desplazamientos de las variables. Si io hace, entonces la generación de código objetivo puede basarse sólo en el có- digo intermedio; si no, el compilador debe retener la tabla de símbolos para la generación del código objetivo.

El código intermedio es particularmente útil cuando el objetivo del compilador es pro- ducir código muy eficiente, ya que para hacerlo así se requiere una cantidad importante del análisis de las propiedades del código objetivo, y esto se facilita mediante el uso del códi- go intermedio. En particular, las estructuras de datos adicionales que incorporan informa- ción de un detallado análisis posterior al análisis sintáctico se pueden generar fácilmente a partir del código intermedio, aunque no es iiiiposible hacerlo directamente desde el &bol sintáctico.

Ei código intermedio también puede ser útil al hacer que un conipilador sea más fácil- mente redirigible: si el cóúigo intermedio es hasta cierto punto independiente de la niáquina objetivo, entonces generar código para una máquina objetivo diferente sólo requiere volver a escribir el traductor de código intermedio a código objetivo, y por lo regular esto es más fácil que vdver a escribir todo un generador de código.

En esta sección estudiaremos dos formas populares de código intermedio: el código de tres direcciones y el código P. Ambos se presentan en muchas formas diferentes, y nues- tro estudio aquí se enfocará sólo en las características generales, en lugar de presentar una descripción detallada de una versión de cada uno. Tales descripciones se pueden encontrar en la literatura que se describe en la sección de notas y referencias al final del capítulo.

8.1.1 Codigo de tres direcciones La instrucción básica del código de tres direcciones está diseñada para representar la eva- luación de expresiones aritméticas y tiene la siguiente forma general:

Esta instrucción expreía la aplicación del operador op a los valores de y y z, y la asignación de este valor para que sea el nuevo valor de x. Aquí r,p puede Ter un operador aritméticó como f o - o algún otro operador que pueda actuar 5ohre los valore^ de y y z.

El nombre "código de tres direcciones" viene de esta forma de instrucción, ya que por lo general cada uno de los nombres x, y y z representan una dimcción de la memoria. Sin embargo, observe que el uso de la dirección de x difiere del uso de las direcciones de y y z, y que tanto y como z (pero no x) pueden representar constantes o valores de literales sin direcciones de ejecución.

Para ver cómo las secuencias de código de tres direcciones de esta forma pueden repre- sentar el cálculo de una expresión, considere la expresión aritmética

con árbol sintáctico

Page 404: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 G E N E R A C I ~ N DE C ~ D I G O

El código de tres direcciones correspondiente es

t l = 2 * a

t 2 = b - 3

t 3 = tl + t 2

El código de tres direcciones requiere que el conipilador genere nombres para elem temporales, a los que en este ejemplo hemos llamado tl, t2 y t3. Estos elementos te rales corresponden a los nodos interiores del árbol sintáctico y representan sus v calculados, con el último elemento temporal (t3, en este ejemplo) representando el v l a raíz.' La manera en que estos elementos temporales finalmente son asignados en rnoria no es especificada por este código; por lo regular serán asignados a registr también se pueden conservar en registros de activación (véase el análisis de la pila te ral en el capítulo anterior).

El código de tres direcciones que se acaba de dar representa una linealizaci6n de i da a derecha del árbol sinttIetico, debido a que el código correspondiente a la evaluact subárbol izquierdo de la raíz se lista primero. Es posible que un compilador desee un orden diferente en ciertas circunstancias. Aquí simplemente advertimos que pue ber otro orden para este código de tres direcciones, a saber (con un significado dife para los elementos temporales),

Evidentemente. la forma del código de ires direcciones que hemos mostrado no basta para representar todas las características ni siquiera del lenguaje de programación más pe- queño. Por ejemplo, l o ~ operadores unitarios, como la negacidn, requieren de una variación del citdigo de tres direcciones que contenga bolo dos direcciones, tal como

t 2 5 - tl

Si se desea tener capacidad para todas las construcciones de un lenguaje de programaci estándar, ser6 necesado variar la forma del código de tres direcciones en cada constmcci Si un lenguaje contiene características poco habituales, puede ser necesario incluso inve nuevas formas del código de tres direcciones para expresarlas. Ésta es una de las razt por las que no existe una forma estríndar para el código de tres direcciones (del mismo mo- do que no existe una forma estándar para árboles ~intácticos).

En las siguientes secciones de este capítulo trataremos algunas construcciones comune de lenguaje de programación de manera individual y mostraremos cómo estas construc ciones son traducidas por lo común como código de tres direcciones. Sin embargo, p ~costumbrantos a lo que nos espera presentamos aquí un ejemplo completo en el que se uti- liza el lenguaje TINY presentado anteriormente.

Considere el programa de muestra TINY de la sección 1.7 (capítulo 1) que calcula el máximo común divisor de dos enteros, el cual repetimos en la Figura 8.1. El código de tres direcciones de muestra para este programa se ofrece en la figura 8.2. Este código contiene

1. Las nomhres tl, t2, y así sucesivamente están scilo destinados U ser representativos del estilo neneral de tal código. De hccho, los nonibres temporales en código de tres direcciories &ben ser (lis- - tintos de cualquier notnbre que pudiera ser utilizaJo en cl ccidigo fuente wal, s i e van a inezclar los uonrhres de ccidigo fuente, a m o ocurre aquí.

Page 405: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Código intermedio y estrutturas de datos para generati911 de código

( Programa de muestra

en lenguaje TINY--

calcula el factorial

)

read x; introducir un entero )

if O < x then ( no calcular si x <= O f

fact := 1;

repeat

fact := fact * x; x : * x - 1

until x = O;

write fact ( salida del factorial de x )

end

read x

t l = x > O

if-falsa tl goto L1

fact = 1

label L2

t2 = fact * x fact = t2

t 3 = x - 1

x t3

t4 = x == O

if-false t4 goto L2

write fact

label L1

halt

varias formas diferentes de código de tres direcciones. En primer lugar, las operaciones in- tegradas de entrada y salida read y write se tradujeron directamente en instrucciones de una dirección. En segundo lugar, existe una instrucción de salto condicional if-falae que se utiliza para traducir tanto sentencias if como sentencias repeat y que contiene dos di- recciones: el valor condicional que se probará y la dirección de código a la que se saltará. Las posiciones de las direcciones de salto también se indican mediante instrucciones (de una dirección) label. Estas instrucciones label pueden ser innecesarias, dependiendo de las estructuras de datos empleadas para implementar el código de tres direcciones. En tercer lugar, una instrucción halt (sin direcciones) sirve para marcar el final del código.

Finalmente, observamos que las asignaciones en el código fuente producen la genera- ción de instrucciones de copia de la forma

Por ejemplo, la sentencia de programa de muestra

fact := fact * x;

Page 406: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 G E N E M C ~ ~ N DI CÓDIGO

se traduce en las dos instrucciones del código de tres direcciones

t 2 = f a c t * x

f a c t = t 2

aunque sería suficiente una instrucción de tres direcciones. Esto sucede por razones cas que se explicarán en la sección 8.2.

8.12 Estructuras de datos para la irnpiernentación del código de tres direcciones

El código de tres direcciones por lo regular no está implementado en forma textual como lo hemos escrito (aunque podría estarlo). En vez de eso, cada instrucción de tres direcciones - .

está implenientada como una estructura de registro que contiene varibs campos, y la s cia completa de instnicciones de tres direcciones está implementada como un arreg ligada, la cual se puede conservar en la memoria o escrita para (y leída desde) arch porales cuando sea necesario.

La implementación más común consiste en implementar el código de tres dire fundamentalmente como se muestra, lo que significa que son necesarios cuatro camp para la operación y tres para las direcciones. Para las instrucciones que necesitan de tres direcciones, uno o más de los campos de dirección proporcionan un v "vacío"; la selección de los canipos depende de la implementación. Como son nec tro campos, una representación de código de tres direcciones de esta naturaie~a se den iia cuádruple. Una posible implementación cuádruple del código de tres direcciones d figura 8.2 se proporciona en la figura 8.3, donde Te eicribieron los cuádrnples en nota matemática de "tuplas".

Los typedef posibles de C para implementar los cuádruples mostrados ra 8.3 se ofrecen en la figura 8.4. En estas definiciones permitimos que una di d o una constante entera o una cadena (que representa el nombre de un elemento tempo o una variable). También, puesto que se utilizan nombres, éstos deben introducirse en un tabla de símbolos, y re necesitnrá realizar búsquedas durante procesamient Una alternativa para conservar los nombres en los cuádruples es mantener npunt cia entradas de la tabla de símbolos. Esto evita tenecque reaiizar búsquedas e& particularmente ventajoso en un lenguaje con ámbitos anidados, donde se necesit información de ámbito que aólo el nombre para realizar una búsqueda. Si también troducen constantes en la tabla de símbolos, entonces no es necesaria una unión en de datos Address.

Una implementación diferente del código de tres direcciones consiste en utilizar las i trucciones mismas para representar los elementos temporales. Esto reduce la ne campos de dirección de tres a dos, puesto que en una instrucción de tres direcciones que contiene la totalidad de las tres direcciones, la dirección objetivo es siempre un elemento temporal.z Una implementación del código de tres direcciones de esta clase se denomi- na triple y requiere que cada instrucción de tres direcciones sea referenciable, ya sea como

2. Esto tia es una verdad inherente acerca del cúdigo de tres direcciones, pero pttcde asegurarse nietli;tntc la iinplementnciún. Por cjeniplo, es veriladero en el c6digo de la figura 8.2 (véase también la figura 8.5).

Page 407: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

f igura 8.4 COdigo C que define estructuras de datos posibles para los cuádruples de la Sgura 8.3

(Sdigo intermedio y estructuras de datas pata generauan de código

t ~ e d e f enum (rd.gt, if-f ,asn, lab,mul,

sub, eg,wri,halt, . . . > OgKind; tygedef enum Wmpty,IntConst,String) AddrKind;

tygedef struct

{ AddrKind kind;

union

{ int val;

char * name; ) contents;

). Address;

tygedef struct

( OpKind og;

Address addrl,addr2,addr3;

) Quad;

un índice en un arreglo o como un apuntador en una lista ligada. Por ejemplo, tina represen- tación abstracta de una implementación del código de tres direcciones de la figura 8.2 como triples se proporciona en la figura 8.5 (página 404). En esa figura utilizamos un sistema de numeraciGu que correspondería a índices de arreglo para representar los triples. Las referen- cias de triple también se distinguen de las constantes poniéndolas entre paréntesis en los mismos triples. Adicionalmente, en la figura 8.5 eliminamos las instrucciones label y las reemplazamos con referencias a los índices de triple mibmos.

Los triples son una manera eficiente para representar el código de tres direcciones, ya que la cantidad de espacio se reduce y el compilador no necesita generar nombres para ele- mentos temporales. Sin embargo, los triples tienen una importante desvenlaja, y 6sta consis- te en que, si se los representa mediante índices de arreglo, entonces cualquier movimiento ríe sus posiciones se vuelve difícil. Por otra pnrte, una representación de lista ligada no su- fre de esta deficiencia. Problemas adicionales que involucrcin triples, y el código C apropiado para la rlcfinici6n <le 10s mismos, se dejan coino ejercicios.

Page 408: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

404 CAP. 8 1 GENEMCI~N DE CÓDIGO

Firiura 8.5 (0) (rd,x,-)

Una representacion del (1) (gt,x,O)

código de tres direcciones ( 2 ) (if-f, (l), (11))

de la figura 8.2 como triples (3) (asn,l,fact)

81.3 Código P El código P comenzó como un código ensamblador objetivo estándar producido por v compiladores Pascal en la década de 1970 y principios de la de 1980. Fue diseñado par el código real de una máquina de pila hipotética, denominada máquina P, para la fue escrito un intérprete en varias máquinas reales. La idea era hacer que los compilado de Pascal se transportaran fkilmente requiriendo sólo que se volviera a escribir el intérpre la máquina P para una nueva plataforma. El código P también ha probado ser útil com digo intermedio, y se han utilizado varias extensiones y modificaciones del mismo en di sos compiladores de código nativo, la mayor parte para lenguajes tipo Pascal.

Como el código P fue disefiado para ser directamente ejecutable, contiene una descn ción implícita de un ambiente de ejecución particular que incluye tamaños de datos. adem de mucha información específica para la máquina P, que se debe conocer si se desea que programa de código P sea comprensible. Para evitar este detalle describiremos aquí una v sión simplificada y abstracta de código P apropiada para la exposición. Las descripciones diversas versiones de código P real podrán encontrarse en varias referencias enumeradas final del capítulo.

Para nuestros propósitos la máquina P está compuesta por una memoria de código, un memoria de datos no especificada para variables nombradas y una pila para datos tem- porales, junto con cualquier registro que sea necesario para mantener la pila y apoyar la ejecución.

Como un primer ejemplo de código P, considere la expresión

2*a+(b-3)

empleada en la sección 8.1.1, cuyo árbol sintáctico aparece en la página 399. Nuestra versión de código P para esta expresión es la que se muestra en seguida:

ldc 2 ; carga la constante 2

lod a ; carga el valor de la variable a

m ~ i ; multiplicación entera

lod b ; carga el valor de la variable b

ldc 3 ; carga la constante 3

sbi ; sustracción o resta entera

adi ; adición de enteros

Page 409: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

COdigo intermedio y estmcturas de datos para generación de código 405

Estas instrucciones se ven como si representaran las siguientes operaciones en una mi- quina P. En primer lugar, ldc 2 irisertia el valor 2 en la pila temporal. Luego, lod a inser- ta el valor de la variable a en la pila. La instrucción mgi extrae estos dos valores de la pila, los multiplica (en orden inverso) e inserta el resultado en la pila. Las siguientes dos instrucciones (lod b y ldc 3) insertan el valor de b y la constante 3 en la pila (ahora tenemos tres valores en la pila). Posteriormente, la instrucción sbi extrae los dos valo- res superiores de la pila, resta el primero del segundo, e inserta el resultado. Finalmen- te, la instmcción adi extrae los dos valores restantes de la pila, los suma e inserta el re- sultado. El código finaliza con un solo valor en la pila, que representa el resultado del cálculo.

Como un segundo ejemplo introductorio, considere la sentencia de asignación

Esto corresponde a las siguientes instrucciones en código P:

Ida x r carga dirección de x

lod y ; carga valor de y

ldc 1 ; carga constante 1

adi ; suma

ato ; almacena tope a dirección

; debajo del tope y extrae ambas

Advierta cómo este código calcula primero la dirección de x, luego el valor de la expresión que será asignada a x, y finalmente ejecuta un comando ato, el cual requiere que se en- cuentren dos valores en la parte superior de la pila temporal: el valor que será almacenado y, debajo de éste, la dirección en la memoria variable en la cual se almacenará. La instruc- ción sto también extrae estos dos valores (dejando la pila vacía en este ejemplo). De este modo el código P hace una distinción entre direcciones de carga (Ida) y valores ordinarios (loa), que corresponden a la diferencia entre el uso de x en el lado izquierdo y el uso de y en el lado derecho de la asignación x: =y+l.

Como último ejemplo de código P en esta sección damos una traducción de código P en la figura 8.6 para el programa TINY de la figura 8.1, junto con comentarios que describen cada operación.

El código P de la tigura 8.6 (página 406) contiene varias instrucciones nuevas de código P. En primer lugar, las instrucciones rdi y wri (sin parámetros) implementan las senten- cias enteras read y write construidas en TINY. La instrucción de código P rdi requie- re que la dirección de la variable cuyo valor va a leerse se encuentre en la parte superior de la pila, y esta dirección es extraída como parte de la instrucción. La instrucción wri requiere que el valor que será escrito esté en la pate superior de la pila, y este valor se extrae como parte de la instrucción. Otras instrucciones de código P que aparecen en la figura 8.6 que aún no se han comentado son: la instrucción lab, que define la posición de un nombre de etiqueta; la instmcción f jp ("false jump"), que requiere un valor booleano en el tope o par- te superior de la pila (el cual es extraído); la instmcción sbi (de "sustracción entera", en inglés), cuya operación es semejante a la de otras instrucciones aritméticas; y las operaciones de comparación grt (de "greater than") y equ ("equal to"), que requieren dos valores en- teros en el tope de la pila (los cuales son extraídos), y que insertan sus resultados booieanos. Finalmente, la instmcción stg ("stop") correqmndiente a la instmcción halt del anterior c6digo de tres direcciones.

Page 410: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 8.6 Código P pan el programa TlNY de la figura 8.1

CAP. 8 1 GENERACIÓN DE C~DIGO

Ida x

r di

lod x

ldc O

grt

fjp L1

Ida fact

ldc 1

Sto

lab L2

Ida fact

lod fact

lod x

mpi

Sto

Ida x

lod x

ldc 1

sbi

Sto

lod x

ldc O

equ

fjp L2

lod fact

wri

lab L1

StP

; carga dirección de x

; lee un entero, almacena a la

; dirección en el tope de la pila ( y la extrae)

; carga el valor de x

; carga la constante O

; extrae y compara dos valores del tope

; inserta resultado Booleano

; extrae resultado Booleano, salta a L1 si es falso

; carga dirección de fact

; carga la constante 1

; extrae dos valores, almacenando el primero en la

; dirección representada por el segundo

; definición de etiqueta L2

; carga dirección de fact

; carga valor de fact

; carga valor de x

; multiplica

; almacena el tope a dirección del segundo y extrae

; carga dirección de x

; carga valor de x

; carga constante 1

; resta

; almacena (como antes)

; carga valor de x

; carga constante O

; prueba de igualdad

; salto a L2 si es falso

; carga valor de fact

; escribe tope de pila y extrae

; definición de etiqueta L1

Comparación del código P con el código de tres direcciones El código P en muchos aspectos es- tá más cercano al código de miquina real que al código de tres direcciones. Las instruccio- nes en código P también requieren menos direcciones: todas las instrucciones que hemos visto son instrucciones de "una dirección" o "cero direcciones". Por otra parte, el código P es menos compacto que el código de tres direcciones en términos de números de iristruccio- nes. y e1 código P no está "autocontenido" en el sentido que Las instrucciones funcionen im- plícitamente en una pila (y las localidades de pila implícitas son de hecho las direcciones "perdidas"). La ventaja respecto a la pila es que contiene todos los valores temporales ne- cesarios en cada punto del código. y el compilacior no necesita asignar noinbres a ninguno de ellos, conio en el código de tres direcciones.

Page 411: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Técnicas básicas de generación de cbdigo 407

/mplementación del código P Históricamente, el código P ha sido en su mayor parte generado como un archivo del texto, pero las descripciones anteriores de las implementaciones de cs- tructura de datos internas para el código de tres direcciones (cuádruples y triples) también funcionarán con una modificación apropiada para el código P.

En esta sección comentaremos los enfoques básicos para la generación de código en general, mientras que en secciones posteriores abordaremos la generación de código para construc- ciones de lenguaje individuales por separado.

.. . ,.. , 0' ... ... . .,. . i<. . . ., 8.2.1 Código intermedio o código objetivo como un atributo sintetizado

La generación de código intermedio (o generación de código objetivo directa sin código in- termedio) se puede ver como un cálculo de atributo similar a muchos de los problemas de atributo estudiados en el capítulo 6. En realidad, si el código generado se ve como un atribu- to de cadena (con instrucciones separadas por caracteres de retorno de línea), entonces este código se convierte en un atributo sintetizado que se puede definir utilizando una gramática con atributos, y generado directamente durante el análisis sintáctico o mediante un recorri- do postorden del árbol sintáctico.

Para ver cómo el código de tres direcciones, o bien, el código P se pueden definir como un atributo sinteti~ado, considere la siguiente gramática que representa un pequeño subcoti- junto de expresiones en C:

exp +id = exp / aexp aexp -+ aexp +factor /factor factor -+ ( exp ) 1 num 1 id

Esta grwit ica sólo contiene dos operaciones, la asignación (con el símbolo =) y la adición (con el símbolo +l.' El token i d representa un identificador simple, y el token num simboli- . - za una secuencia simple de dígitos que rcpresenta un entero. Se supone que ambos tokens tienen un atributo strval previamente calculado, que es el valor de la cadena, o lexema, del token (por ejemplo, "42" para un num o "xtemp" para un id).

Código P Consideraremos el caso de la generación del código P en primer lugar, ya que la gramática con atributos es más simple debido a que no es necesario generar nombres para elementos temporales. Sin embargo, la existencia de asignaciones incrustadas es un factor que implica complicaciones. En esta situación deseamos mantener el valor almacenado co- mo el valor resultante de una expresión de asignación, ya que la instrucción de código P ei- tándar a t o es destructiva, pues el valor asignado se piede. (Con esto el código P muestra sus orígenes de Pascal, en el cual no existen las asignaciones incrustadas.) Resolvemos es- te problema introduciendo una instrucción de almacenamiento no destructiva s t n en nuestro código P, la que como la instrucción ato, supone que se encuentra un valor en la parte superior o tope de la pila y una dirección debajo de la misma; s t n almacena el valor en la direccibn pero deja el valor en el tope de la pila, mientras que descarta la direcciítn. Con esta nueva instrucción, una gramática con atributos para un atnbuto de cadena de código

3. L3 asignación en este ejemplo tiene la sciriántica siguiente: x = e almacena el valor de e en x 4 tiene el mismo valor resultante que e.

Page 412: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tabla 8.1 Gramática con atributos de código P tomo un atributo de cadena sintetizado

CAP. 8 1 GENEMCI~N DE C~DIGO

P se proporciona en la tiabla 8.1. En esa figura utilizamos el nombre de atributo pcod la cadena de código P. Tambith empleamos dos notacione,~ diferentes para la concatenac de cadena: + + cuando las instrucciones van a ser concatenadas con retornos de línea i tados entre ellas y / / cuando se está construyendo una instrucción simple y se va a ins un espacio.

Dejamos al lector describir el cálculo del atributo pcode en ejemplos individua mostrar que, por ejemplo, la expresión (x=x+3 ) +4 tiene ese atributo

Ida x

lod x

ldc 3

adi

stn

ldc 4

adi

Regla gramatical Reglas semánticas

exp, + id = e.rpz exp, .pcode = "lda" / / id .stWal + + expz p o d e + + "stn"

ern + amn ex^ acode = aexn mode aexp, + ue.rpi + factor uexp, .pcode = ue.rp2 .pCodr

++factor .pcode ++ "adi"

aexp +factor uap .pode =factor .pcode

factor -+ ( exp ) jizctor .pcode = exp .p.pode

factor + num fucror .pccnie - "ldc" 11 num ..~trval factor -+ id factor .pcode = "lod" / / id ..rlnxzl

Código de trer direcciones Una gramática con atributos para código de tres direcciones de 1 gramática de expresión simple anterior se proporciona en la tabla 8.2. En esa tabla utiliza- mos el atributo de código que se llama tacode (para codigo de tres direcciones), y como en la tabla 8.1, + + para concatenación de cadena con un retorno de Iínea y / / para concatenación de cadena con un espacio. A diferencia del código P, el código de tres direcciones requiere que se generen nombres temporales para resultados intermedios en las expresiones, y esto requiere que la gramática con atributos incluya un nueva atributo name para cada n Este atributo también es sintetizado, pero para asignar nombres temporales recsn genera a nodos interiores utilizamos una función newremp( ) que se supone genera una secuencia de nombres temporales tl, t2, t3,. . . (se devuelve uno nuevo cada vez que se llama a newtemp( )). En este ejemplo simple sólo los nodos correspondientes al operador + necesi- tan nuevos nombres temporales; la operación de asignación simplemente utiliza el nombre de la expresión en el lado derecho.

Advierta en la tabla 8.2 queo en el caso de las producciones unitarias exp -+ (re.r(l y aexp -+ fnctor, el atributo turne, además del atributo tacode, se transportan tle hijos a padres y que, en e"Icaso de Los riodos interiores del operador, se generan nuevos atributos nnttte an- tes del tc~cocle ~isociado. Observe también que, en las producciones cle Iiojaj¿~c~or num y í;rctor -+ id , cl valor de cadena del tokcn sc utiliza c«nto&~cror.~rtrrnr, y que (a diferenci:~

Page 413: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

TPcnicas básicas de generación de código

...

,;:f&la 8.2 , . . ~ ... ..., . , . ... ,., , :-:: ~'matica con atributos pan Regla gramatical Reglas semánticas

/irctor .tacode = exp .tacode

factor + ntm fuctor .nutne = num .srn>ul factor . tacode = "

factor * id factor .turme = i d .sfrval flrctor .tac»de = " "

e.rp -+ uexp exp .nume = aexp .narnc exp Sacode = uexp ..tacode

ue.xpl -+ <iexp2 + factor ae.rp, .nume - twwtenipf ) aexpl .tacode =

aexp2 .tacode + + factor. t~icude ++ uexp, .ndme / / "=" / / aexp2 .name

/ j "+" /lfactor .natne

uexp -+factor uecp .nnme =factor .nome aexp .tucocla =factor .iacode

fucfar -+ f exp 1 factor .name = exp .nome

del código Pj no se genera ningún código de tres direcciones en tales nodos (utilizamos " " para representar la cadena vacía).

De nueva cuenta, dejamos al lector mostrar que, dadas Ias ecuaciones de atributo de la tabla 8.2, la expresión (x=x+3) +4 tiene el atributo tacode

(Esto supone que newtemp( j es llamado en postorden y genera nombres temporales co- menzando con tl.) Advierta cómo la asignación x=x+3 genera dos instrucciones de tres direcciones utilizando un elemento temporal. Esto es una consecuencia del hecho de que la evaluación de atributos siempre crea un elemento temporal para &da subexpresión, inclu- yendo los lados derechos de las asignaciones.

Visualizar la generación de código como el cálculo de un atributo de cadena sintetíza- do es útil para niostrar claramente las relaciones entre las secuencias de código de las di- ferentes partes del árbol sintáctico y para comparar los diferentes m6todos de generación de código, pero es poco practico como técnica para generación de código real, por varias razones. En primer lugar, el uso de la concatenación de cadena causa que se desperdicie una cantidad desmesurada de copiado de cadenas y memoria a menos que los operadores de la concatenación sean muy complejos. En segundo, por lo regular es mucho niás desea- ble generar pequeños segmentos de código a medida que continúa la generación de cótligo y escribir estos segmentos en un archivo o hien insertarlos en una estructura de datos (tal como un arreglo de cuádruples), lo que requiere acciones semáriticas que no se iitlhie- ren a la-síntesis postorden estándar de atributos. Finalmente. aunque es út i l visualizar cl có- digo corno puramente sintetizado, la generacidn de c6tligo en general depende mucho de ;atributos hcrcdados, y csto complica cn gran medida las grarn5tic;is con atributos. Es por

Page 414: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

. . .. CAP. 8 1 GENEMCI~N DE E ~ D I G O

,.. . . . . .. . .

esto que no nos preocupamos aquí por escribir ningún código (incluso pseudocódigo ra implementar las gramáticas con atributos de los ejemplos anteriores (pero vea los ej cicios). En vez de eso, en la siguiente subsección regresaremos a técnicas de generació código más directas.

822 Generación de código práctica Las técnicas estándar de generación de código involucran modificaciones de los recomd postorden del irbol sintáctico implicado por las gramáticas con atributos de los ejem cedentes o, si no se genera un árbol sintáctico de manera explícita, acciones equival rante un análisis sintáctico. El algoritmo básico puede ser descrito com o el siguient cedimiento recursivo (para nodos de árbol con dos hijos como máximo, pero ficilment extensibles a más):

procedure genCode ( T: treenode ); begin

if T no es nil then genere co'digo para preparar en el caso del código del hijo izquierdo de T ; genCode(1aijo izquierdo de T) ; genere código para preparar en el caso del cídigo del hija derecho de T ; genCode(h¿jo derecho de T) ; genere cddigo para implernentar la acción de T ;

end;

Observe que este procedimiento recursivo no sólo tiene un componente postorden (qu genera código parti implementar la acción de T ) sino también un componente de preorde y un componente enorden (que genera el código de preparación para los hijos izquierdo derecho de Tf . En general, cada acción que representa T requerirá una versión un poc diferente del código de preparación preorden y enorden.

Para ver de manera detallada cómo se puede construir el procedimiento gencode en u ejemplo especifico considere la gramitica para expresiones aritméticas simples que hem estado utili~ando en esta sección (véase la gramática en la página 407). Las definiciones e C para un árbol sintáctico abstracto de esta gramática se pueden proporcionar de La inaner siguiente (compare con las de la página 11 1 del capítulo 3):

typedef enum tPlus,Aasignf üptype;

typedef enum {OpKind,ConstKind,IaKindf NodeKind;

typedef stmct streenode

t NodeKind kinds üptype og; / * usado con OpKind */ struct streenode *lchild,*rchild;

int val; / * usado con ConstKind * / char strval;

/ * usado para identificadores y números * / 1 STreeNode;

typedef STreeNode *SyntaxTree;

Con estas definiciones se puede dar un árbol sintictic« para la expresión (x=x+3 1 +4 como se ve en seguida:

Page 415: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Tkcnicas bkicas de generación de código

Advierta que el nodo designación contiene el identificador que se está asignando (en ei campo s t r v a l ) , así que un nodo designación tiene solamente un hijo (la expresión que se a~ igna) .~

Basados en esta estructura para un árbol sintáctico podemos escribir un procedimiento genCode para generar código P como se da en la figura 8.7. En esa figura haremos los co- mentarios siguientes acerca del código. En primer Lugar, el código utiliza la función están- dar de C s p r i n t f para concatenar las cadenas en el elemento temporal local codestr. En segundo lugar se llama el procedimiento a m i tcode para generar una línea simple de código P. ya sea en una estructura de datos o en un archivo de salida; sus detalles no se - muestran. Finalmente, los dos casos de operador ( P l u s y Asaign) requieren dos órdenes diferentes de recorridos: P l u s necesita sólo procesamiento de postorden, mientras que A s a i g n requiere cierto procesamiento tanto preorden como postorden. De este modo, las llamadas recursivas pueden no escribirse del mismo modo para todos los casos.

Para mostrar que la generación de código, incluso con la variación necesaria en orden de recorrido, todavía se puede realizar durante un análisis sintáctico (sin la generación de un árbol sintáctico), mostramos un archivo de especificación Yacc en la figura 8.8 que corres- ponde directamente al código de la figura 8.7. (Observe cómo el procesamiento combinado preorden y postorden de las asignaciones se traduce en secciones de acción dividida en la especificación Yacc.)

Dejamos al lector escribir un procedimiento genCode y especificación Yacc para ge- nerar el código de tres direcciones como se especificó en la gramática con atributos de la tabla 8.2.

Figura 8.1 void gencode( SyntaxTree t)

lmplementación de un pro- f char codestrtCODESIZE1 i

tedimiento de generacien /* CODESIZE longitud máxima de 1 línea de código P */ de código pan código P if (t 1.: NULL)

cornspondiente a la gram6- f switch (t->kind)

tica con atributos de la { case GpKind: tabla 8.1 switch (t->op)

f case Plus:

genCode(t->lchild);

genCode(t->rchild);

emitcode ( "adi") ;

break;

J. También almacenmos números como cadenas en el campo strval en este ejemplo.

Page 416: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 GENERACI~N DE C~DIGO

case Assign:

sprintf (codestr, "%S %S",

"Ida", t->strval);

emitCode(code8tr);

genCode(t->lchild);

ernitCode("stnm);

break;

default:

emitCode ("Error") ;

break;

1 break;

case Constkind:

sgrintf(codestr, "%S %S", "ldc", t->strval) ;

emitCode(codestr);

break;

case Idñind:

sprintf(codestr, "%a %a", "lod", t->strval) ;

emitCode(code8tr);

break;

default :

emitCode ( ''Error" ) ;

break;

)

1 1

generación de código P de / * hace que Yacc utilice cadenas como valores * /

acuerdo ton la gramática

ton atnbutor de la / * otro código de inclusión ... */ tabla 8.1 %)

Page 417: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Técnicas bkicar de generación de código

aexp : aexp ' + ' factor íemitcode ( "adi" ) ; 1

I factor

;

factor : ' ( ' exp ' ) ' I m f

I ID í

;

%%

sprintf (codestr, '%S %S", "ldc", $1) ;

emitCode(c0destr); 1 sprint£ (codestr, "8s %S", "lod", $1) ;

emitCode(codestr); 1

/ * funciones de utileria ... */

8.2.3 Generación de código objetivo a partir del código intermedio

Si un compilador genera código intermedio, ya sea directamente durante un análisis sintác- tico o desde un &bol sintáctico, entonces debe efectuarse otro paso en el código intermedio para generar el código objetivo final (por lo regular después de algún procesamiento adicio- nal del código intermedio). Este paso puede ser bastante complejo por sí mismo, en particii- lar si el código intermedio es muy simbólico y contiene poca o ninguna información acerca de la máquina objetivo o el ambiente de ejecución. En este caso, el paso de la generación de código final debe suministrar todas las ubicaciones reales de variables y temporales, más el código necesario para mantener el ambiente de ejecución. Una cuestión particularmente importante es la asignación apropiada de los registros y el mantenimiento de la información .*obre el uso de los mismos (es decir, cuáles registros se encuentran disponibles y cuáles con- tienen valores conocidos). Aplazaremos un análisis detallado de tales cuestiones de asigna- ción hasta más adelante en este capítulo. Por ahora comentaremos sólo técnicas generales para este proceso.

Por lo regular la generación de código a partir del código intermedio involucra alguna o ambas de las dos técnicas estándar: expansión de macro y simulación estática. La expan- sión de macro involucra el reemplazo de cada clase de instrucción del código intermedio con una secuencia equivalente de instrucciones de código objetivo. Esto requiere que el compilador se mantenga al tanto de las decisiones acerca de ubicaciones e idiomas de códi- go en estructuras de datos separadas y que los procedimientos del macro varíen la secuen- cia de código como se requiera mediante las clases particulares de datos in~olucradas en la - instrucción de código intermedio. De este modo, cada paso puede ser mucho más complejo que las formas simples de expansión de macro disponibles del preprocesador de C o cn- sanibladores de macro. La simulación estatiea involucra una simulación en línea recta de los efectos del código intermedio y generación tle código objetivo para igualar estos efectos. Esto tltinbih requiere de más estructuras de datos, y puede variar de formas muy simples de seguimiento cntplcadas cn conjunto con la expansión de macro, hasta la nltarncrite

Page 418: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

sofisticada interpretación abstracta (que mantiene los valores algebraicamente a medida que son calculados).

Podemos obtener alguna idea de los detalles de estas técnicas con~iderando el problema de traducir desde el código P al código de tres direcciones y viceversa. Consideremos la pe- queña gramática de expresión que hemos estado utilizando como ejemplo de ejecución en esta sección, y consideremos la expresión (x=x+3) +4, cuyas traducciones en cbdigo P código de tres direcciones se proporcionaron en las páginas 4.08 y 309, respectivamente. Consideremos primero la traducción del código P para esta expresión:

Ida x lod x ldc 3

adi

etn

1dc 4

adi

en su correspondiente código de tres direcciones:

Esto requiere que realicemos una simulación estatica de la pila de m iina P para encontrar equivalentes de tres direcciones del código dado. Hacemos esto con una estructura de datos de pila real durante la traducción. Después de las primeras tres instrucciones de código P, no se han generado todavía instrucciones de tres direcciones, pero la pila de máquina P se ha modificado para reflejar las cargas, y la pila tiene el aspecto siguiente:

+--tope de la pila

Ahora, cuando se procesa fa operación adi, se genera la instrucción de tres direcciones

t l = x + 3

y la pila se cambia a

+--tope de la pila t 1

La instmcción s t n provoca entonces que se genere la instrucción de tres direcciones

x 3 tl

Page 419: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Técnicas básicas de generacián de &digo

y la pila se cambie a

-tope de la pila

La instrucción siguiente inserta la constante 4 en la pila:

-tope de la pila

Finalmente, la instrucción adi provoca que se genere la instrucción de tres direcciones

y la pila se cambie a

/-tope de la pila

Esto completa la simulación estática y la traducción. Ahora consideraremos el caso de traducir del código de tres direcciones al código P. Si

ignoramos la complicación agregada de los nombres temporales, esto puede hacerse me- diante expansión simple de macro. Por consiguiente, una instrucción de tres direcciones

siempre se puede traducir en la secuencia de código P

Ida a lod b ; o ldc b si b es una constante lod c ; o ldc c si c ea una constante adi

Sto

Esto resulta en la siguiente traducción (algo insatisfactoria) del anterior código de tres di- recciones en código P:

Ida el

lod x ldc 3

adi

Sto

Ida x

lod tl

Sto

Ida t2 (continúu)

Page 420: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 I GENERACI~N DE C ~ D I G O

loa tl

ldc 4

adi

Sto

Si queremos eliminar los elementos temporales extra, entonces debemos utilizar un e ma más sofisticado que la expansión de macro pura. Una posibilidad es generar un n árbol a partir del código de tres direcciones, indicando el efecto que el código tiene al quetar los nodos del árbol tanto con el operador de cada instrucción como con el nombre se le asigna. Esto puede visuaíizarse como una forma de simulación estática, y el árbol tante para el anterior código de tres direcciones es

+ \ 4

/ + \ X 3

Advierta cómo la instrucción de tres direcciones

x = tl

no provoca que se creen nodos extra en este árbol, pero si que e1 nodo con nombre t l quiera el nombre adicional x. Este árbol es similar, pero no idéntico, al árbol sintictico la expresión original (véase la ptigina 41 1)."1 código P se puede generar a partir de est árbol de manera muy semejante a como se genera el código P a partu de un árbol sintáctic de la manera antes descrita, pero con elementos temporales eliminados al hacer asignacio sólo a nombres permanentes de nodos interiores. De este modo, en el árbol de muestra, s se asigna x, los nombres t 1 y t 2 nunca se emplean en el código P generado, y el valor c mespondiente al nodo raíz (con nombre t2) se deja en la pila de la máquina P. Esto prod ce exactamente la misma generación de código P que antes, aunque se utiliza s t n en lu de s t o siempre que se realiza un almacenamiento. Alentamos al lector a escribir ps código o código en C para llevar a cabo este proceso.

GENERACIÓN DE CODIGO DE REFERENCIAS DE ESTRUCTURAS DE DATOS

8.3.1 Cálculos de direcciones En la oección anterior vimos cómo se puede generar el código intermedio para asignaciones y expre5iones aritméticas simples. En estos ejemplos todos los valores básicos eran constan- tes, o bien, variables simples (ya sea variables de programa tales como x, o temporales como ti.). Las variables wnples eran identificadas sólo por nombre: la traducción a código

5. Este :irbol es un caso especial de una constmcci<in rnrís general denominada D.\C de un bloque Básico, la cual re describe en Iü seccih 4.93.

Page 421: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generación de código de referencias de estructuras de datos 417

objetivo requiere que estos nombres secan reemplazados por direcciones reales, las cuales podrían ser registros, direcciones de memoria absoluta (para globales), o desplazamientos de registro de activación (para locales, que posiblemente incluyan un nivel de anidación). Estas direcciones se pueden insertar en el momento en que es generado el código intermedio o aplazar la inserción hasta que se genere el código real (con la tabla de símbolos mante- niendo las direcciones).

Sin embargo, existen muchas situaciones que requieren que se realicen los cálculos de las direcciones para localizar la dirección real en cuestión, y estos cálculos deben ser expre- sados directamente, incluso en código intermedio. Tales cálculos se presentan en subíndice de arreglo, campo de registro y referencias de apuntador. Comentaremos cada uno de estos casos por turno. Pero debemos comenzar por describir extensiones para el código de tres di- recciones y el código P que nos permitan expresar tales cáiculos de direcciones.

Código de tres direcciones para cálculos de direcciones En el código de tres direcciones, lo que \e necesita no es tanto nuevas operaciones (las operaciones aritméticas habituales \e pueden utilizar para calcular direcciones) sino maneras para indicar los modos de direccionamiento "dirección de" e "indirecto". En nuestra versión del código en tres direcciones utilizaremos la notación equivalente en C "&" y "*" para indicar estos modos de direccionamiento. Por ejemplo, supongamos que deseamos almacenar el valor constante 2 en la dirección de la variable x más 10 bytes. Expresaríamos esto en código de tres direcciones de la manera siguiente:

La implementación de estos nuevos modos de direccionamiento requiere que la ebtruc- tura de datos para el código de tres direcciones contenga un nuevo campo o campos. Por ejemplo, la estructura de datos cuádruple de la figura 8.4 (página 403) se puede aumentar mediante un campo enumerado AddrMode con posibles valores None, Addresa e Indirect.

código P para cáku/os de direcciones En código P es común introducir nuevas instrucciones para expresar nuevos modos de direccionamiento (puesto que hay pocas direcciones explí- citas a las cuales asignar modos de direccionamiento). Las dos instrucciones que introduci- remos para este propósito son las siguientes:

1. ind ("indirect load), que toma como parámetro un desplazamiento entero, supone que hay una dirección en la pnrte superior o tope de la pila, agrega el desplaamien- to a la dirección y reemplaza a esta última en la pila con el valor de la ubicación resultante:

Pila antes Pila después

2. ixa ("indexed address"), la cual toma como parámetro un factor de escala entero, supone que se tiene un desplazamiento en la parte superior o tope de la pila y una di- rección base debajo del mismo, multiplica el desplazamiento por e1 factor de escala, agrega la dirección base, extrae tanto el desplazamiento como la hase de la pila e inserta la dirección resultante:

Page 422: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 / GENEMCI~N DE Cb0lGO

Pila antes Pila después

+parte superior o tope

Estas dos instmcciones de código P. junto con la instmcción Ida (load address) que se sentó anteriormente, permitirán realizar los mismos cálculos de dirección y las refere a los modos de direccionamiento que para el código de tres direccione~.~ Por ejem problema de muestra anterior (almacenar el valor constante 2 en la dirección de la v x más 10 bytes) ahora se puede resolver en código P de la manera que se presenta a c

i

a

nuación:

ira S ___*

ida x ldc 10

ixa 1 ldc 2

SCO

Ahora volveremos a un analisis de arreglos, registros y apuntadores, seguido por un análi- sis de generación de código objetivo y un ejemplo extendido.

8.3.2 Referencias de arreglo Una referencia de arreglo involucra la subindización de una variable de arreglo mediante una expresión para obtener una referencia o valor de un simple elemento del arreglo, como en el código en C

int atSIZE1; int i, j;

... a [ i + l l = a [ j * 2 ] + 3;

En esta asignación la subindización de a mediante la expresión i + l produce una dxecció (el objetivo de la asignación), mientras que la subindización de a mediante la expresión j *2 produce el valor de la dirección calculada del tipo de elemento de a (a saber, i n t ) . Como los arreglos son almacenados de manera secuencial en la memoria, cada dirección debe calculada desde la dirección base de a (su dirección de inicio en memoria) y un despl miento que depende linealmente del valor del subíndice. Cuando se deseara obtene valor más que la dirección, se debe generar un paso de indirección extra para obtener el valor de la dirección calculada.

El desplazamiento es calculado a partir del valor de subíndice de la manera siguiente. En primer lugar, debe hacerse un ajuste al valor del subíndice si el intervalo de subíndice no comienza en O (esto podría ocurrir en lenguajes como Pascal y Ada, pero no en C). En se- gundo lugar, el valor del subíndice ajustado debe ser multiplicado por un factor de escala

6. De hecho, 1s instrucción ixa podría ser simulada mediante operaciones aritméticas, 5610 que en el código P estas operaciones son tipeadas (adi = multiplicaci6n entera d o ) y de este modo no se pueden aplicar a direcciones. No insistimos en las limitantes de tipo del código P porque involucra parhetras extra que hemos suprimido por simplicidad.

Page 423: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generacibn de cbdigo de referencias de estructuras de datos 419

que es igual al tamaño de cada uno de 10s elementos del arreglo en la memona. Finalmen- te, el subíndice escalado resultante se agrega a la dirección base para obtener la direcci& fi- nal del elemento del arreglo.

Por ejemplo, la dirección de la referencia de arreglo en C a I i + 11 es7

De manera más general, la dirección de un elemento de arreglo a [tl en cualquier lenguaje es

dirección-base(a) + (t - limite-inferiotfa)) * tamañoelernento(a)

Ahora volveremos a las maneras de expresar este cálculo de dirección en el código de tres direcciones y en el código P. Para hacer esto en una notación independiente de la má- quina objetivo supondremos que la "dirección" de una variable de arreglo es su dirección base. De este modo, si a es una variable de arreglo, &a en código de tres direcciones es Lo mismo que dirección-base(a), y en código P

carga la dirección base de a en la pila de máquina P. También, puesto que un cálculo de referencia de arreglo depende del tamaño del tipo de datos del elemento en la máquina ob- jetivo, utilizaremos la expresión e l - s i z e (a) para e1 tamaño de elemento del arreglo a en la máquina objetivo." Como ésta es una cantidad estática (suponiendo tipeado estático), esta expresión será reemplazada por una constante en tiempo de compilación.

Código de tres direcciones para las referencias de arreglo Una manera en que se pueden expresar referencias de arreglo en código de tres direcciones es introducir dos nuevas operaciones, una que obtenga el valor de un elemento de arreglo

y otra que asigne la dirección de un elemento de arreglo

(esto podría ser dado por los símbolos = [ 1 y [ ] =). Si se utiliza esta terminología no es ne- cesario expresar el cálculo de dirección real (y las dependencias de máquina tales como el tamaño del elemento desaparecen de esta notación).. Por ejemplo, la sentencia de código fuente

se traduciría en las instrucciones de tres direcciones

7. En C. el noinhre de un ;irreglo (tal como a en esta expresih) representa su propia dirección base.

8. Esto podría ser dc Iiechu una I'uncih suministrada por la tabla de hnholt~s.

Page 424: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 G E N E R A C I ~ H DE C ~ D I G O

Sin embargo, todavía es necesario introducir modos de direccionamiento como los ante mente descritos cuando tratamos con referencias de apuntador y campo de registro, de do que tenga sentido tratar todos esos cálculos de dirección de manera uniforme. De manera, tambien podemos escribir los cálculos de direcciones de un elemento de m e rectamente en código de tres direcciones. Por ejemplo, la asignacidn

tambiiin puede escribirse como (utilizando elementos temporales adicionales t 3 y t

y la asignación

a [ t 2 1 = t l

puede escribirse como

t 3 = t 2 * e l e m - o i z e ( a )

t 4 = &a t t 3

* t 4 = t l

Finalmente, como un ejemplo más complejo, la sentencia de código fuente

a [ i t l l = a [ j * 2 ] + 3;

se traduce en las instrucciones de tres direcciones

t l = j * 2

t 2 3 t l * e l e m . s i z e ( a )

t3 = &a + t 2

t 4 = * t 3

t 5 = t 4 + 3

t 6 ~ i t l

t 7 = t 6 * e l e m - s i z e ( a )

t 8 = &a + t 7

* t 8 = t 5

Código P para referencia de arreglo Como se describió anteriormente, utilizaremos las nuevas instrucciones de tlirección ind e ixa. La instrucción ixa de hecho fue construida precisa- mente con los crilculos de dirección de arreglo en mente, mientras que la instrucción ind se utiliza para cargar 21 valor de una dirección calculada con ;interioridad (es decir, para irnplementar una carga indirecta). La referencia de arreglo

Page 425: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generación de código de referencias de estructuras de datos

se escribe en código P corno

Ida t2

Ida a

lod tl

ixa elem-size(a)

ind O

Sto

y la asignación de arreglo

se escribe en código P como

ida a

lod t2

ixa elem-siize(a)

lod tl

Sto

Finalmente, el e~emplo anterior más complejo

se traduce en las siguientes instrucciones de c6digo P:

Ida a

lod i

ldc 1

adi

ixa elem-size(a)

Ida a

lod j

ldc 2

mpf ixa elem-size(a)

ind O

ldc 3

adi

Sto

Un procedimiento para la generación de código con referencias de arreglo M«strarcincis aqttí cótiio las referencias de arreglo se pueden generar mediante un procedimieiito tie generxi<jli de c.í,dig«. Utilizarcrnos el ejziiipio del stibconjunto de expresiones C Je la secci<iri aiitcrior (véase la gramática de 13 página 407). :itiiiicnt:ido iiizdiante una opcr~cii,n de suhítidicc. L.:i ritieva gramática que utili~i~rcrnos cs la sipientc:

Page 426: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 GENERACI~N DE C~DIGO

exp -+ suhs = exp / aexp aexp -+ ae.xp +factor 1 factor factor -+ ( enp ) 1 num 1 subs subs -+ i d / id [ exp 1

Advierta que el objetivo de una asignación ahora puede ser una variable simple o una v ble subindizada (anibas incluidas en el no terminal subs). Utilizamos la misma estructur datos para el árbol sintáctico que antes (véanse las declaraciones en la página 410), sólo ahora existe una operación adicional Subs para la subindización:

typedef enum fPlus,Assign,Subs) Ogtype;

/ * otras declaraciones como antes * /

Además, como ahora puede estar una expresión de subíndice a la izquierda de una asign no es posible almacenar el nombre de la variable objetivo en el mismo nodo de asignación que puede no haber tal nombre). En vez de eso, los nodos de asignación ahora tienen dos como nodos extra: el hijo izquierdo debe ser un identificador o una expresión de subí Los subíndices mismos sólo pueden ser aplicados a identificadores, de modo que alma mas el nombre de la variable de arreglo en nodos subíndice. Así, el árbol sintáctico par expresión

Un procedimiento de generación de código que produce código P para tales árboles sin- táctikos se proporciona en la figura 8.9 (compare con la figura 8.7, página 41 1). La dife- rencia principal entre este código y el de la figura 8.7 es que éste necesita tener un atributo heredado isAddr que distinga un identificador o expresión con subíndice a la izquierda de una asignación a partir de uno a la derecha. Si isAddr se establece a TRUE, entonces la direccih de la expresión debe ser devuelta; de otro modo, se devuelve el valor. Dejamos al lector verificar que este procedimiento genera cl siguiente código P para la expresión ( a [ i + l ] = Z ) + a [ j ] :

Ida a

lod i

Page 427: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 8.9 lmplementacián de un procedimiento de generación de código para el código P correspondiente a la grama- iica de expresión de la pagina 422

Generación de código de referencias de estructuras de datos

ldc 1

adi

ixa elem-size(a) ldc 2

stn

Ida a

lod j ixa elem-size(a)

ind O adi

También dejamos ai lector la construcción de un generador de código de tres direcciones para esta gramática (véanse los ejercicios).

void genCode( SyntaxTree t. int isAddr)

( char codestr[CODESIZEl;

/ * CODESIZE = longitud máxima de 1 línea de código P * / if (t != NULL)

{ switch (t->kind)

{ case OpKind:

switch (t->op)

{ case Plua:

if (isAddr) emitCode ( "Error" ) ;

else ( genCode(t->lchild,FALSE);

genCode(t->rchild,PALSE);

emitCode ( "adi" ) ; )

break;

case Assign:

genCode(t->lchild,TRUE);

gencodeft->rchild,FALSE);

emitCode("stnM);

break;

case Suba:

sgrintf (codestr, "36s %S', "Ida", t->stsval) ;

emitCode(codestr);

genCode(t-zlchild,FALSE);

agrintf (codestr, "%a%s%a" , "ixa elem-size(",t->strval,")");

emitCode(codestr);

if ( ! isAddr) emitcode ( "ind 0 " ) ;

break;

default:

emitcode ( "Error" ) ;

break;

1 break;

case ConstKind:

Page 428: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

424

Figura 8.9 Conlinuatión if (isAddr) emitCodef"Errorq)i

else

{ sprintf(codestr,"%s %s","ldc",t->strval);

emitCode(codestr);

1 break;

case IdKind:

if (isAddr)

aprintf (codestr,"%s %e", "lda", t->strval);

else

agrintf (codestr, "%a %S", "lod", t->strval) ;

smitCode(codestr);

break;

default:

emitCode ( "Error") ;

break;

)

Arregos multidimensionales Un factor que complica el cilculo de las direcciones de arreglo la existencia, en la niayona de los lenguajei, de arreglos en varias dimensiones. Por eje plo, en C un arreglo de dos dimensiones (con diferentes tamaños de índice) puede decl se como

int a[151 [lo];

Tales arreglos pueden estar parcialmente subindimdos, produciendo un arreglo de me- nos diniensiones, o subindizados por completo, produciendo un valor del tipo elemento del arreglo. Por ejemplo, dada la declaración anterior de a en C, la expresión a [ i 1 implica su- bindización de manera parcial de a, produciendo un arreglo unidiniensional de enteros, mientras que la expresión a [i] [ j] implicala subindización completa de a y produce un valor de tipo entero. La dirección de una variable de arreglo subindizada de manera parcia o complzta se puede calcular mediante la aplicación recursiva de las técnicas recién descri tas para el caso unidimensional.

833 Estructura de registro y referencias de apuntador El cálculo de la dirección de un campo de estructura o regibtro presenta un prublema similar al de calcular una dirección de arreglo subindizada. En primer lugar se calcula la dirección ha% de la variable de estructura; luego se encuentra el desplazamiento (generalmente fijo) del campo nombrado, y se suman los dos para obtener la dirección resultante. Considere, por ejemplo, las dcclaciciones en C

typedef struct rec

{ int i;

char c;

int j;

1 Rec; . ~. Rec x;

Page 429: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generacion de cód~go de rderencias de estructurar de datos 425

I'm lo general, Iti v:triahle x est i nsigirada en la nietriori:~ c o ~ i i o lo ~tniestra la siguiciite ilus- t ixicin. cori cada C;IIII~O I i, c y j) teriicndo un <Icspl:i~aiiiicr,to desde la dil-cccicín base de x i l a c ~ t d puede ser en s í rnisnia un i lespla~anricir~o en un registro de ael ivacih):

memoria asignada

a x

x . j x . j 1- x .c f Desplazamiento de

/ (otra memoria)

l

Advierta que lo? campos están asignados linc;ilr~reiitc (por lo regulor desde la d i recc ih rnás h:$i a la ~i iásalta), que el despla~:iniicnto de cada campo es una coustante, y que el priiirer campo (x. i) tictic tlesplazainiento cero. Advierta tambien que los t1cspl:izaniientos de los c : im posdependen del taiuario de los diversos tipos de datos en la riiiquiria objetivo. pero que n o se encuentra involucrado un I'actor de escala corno con los iu~cglos.

Para escribir ck l i go iiitcrniedio independiente del objetivo p x t i cále~ilos de direccitin de cairipo de estructura de registro. deberrios iittroducir una uuevn funciítn que devuelva el (les- plalantiento en campo, dada una variable de cstructurü y el noilibre tiei campo. Llainaremos n esta funcióii f ield-.of fset y la cscribirerrtos con dos parátnetros. el primero será el nombre de la vari:ible y e l segundo el riotnhre del campo. De este niodo. f ield-of f set (x, j ) devuelve el desplazainieirt» de x. j. Como con otras funcioi~es sirnil:ires. esla fun- ci6n puede ser sutiiini~ti.:~da mediante la tabla de símbolos. E n ctt:ilquicr citso, Csta es uiia cantidad en tiempo de cotnpilacii>n, de rnodo que el ctítiigo intermedio real generado teridrá casos (le llarnadas a f ield-of f set reeirrplazados por coi~statttes.

L.as estructuras tle registro yeneralinente son utilizadas junto con apuntadores y asigna- cihti de memoria d in imica para iniplerncntar estructuras de ihtos dináiliicas tüles como lis- tas y árhoies. Así, descrihiinos tainbikn c h r s ititeractúan los apuntadores con eálculos de clirccciítn de earupo. Uii apuritLidor, para los prop6sitos de a t a exp«sicií,n, establece simple- mente uri nivel ;~dicioti:il de iridirecci6n; pasarnos por alto las cucstiunes de :isignaciítri in- \olucradas en la c rex ihn de valores de apunt:idor (estas fueron comentadas en el capítulo üiitcrior).

Código de tres direcciones para las referencias de apuntador y estructura Corisiderernos. en primer Iugir . e l cbdigo de tres direcciones para e l c i IcuIo de d i r e ~ c i o i ~ e ~ de cainpo: para cal- cular la direccii>n (le x. j cti un elenicrito terupot-al tl i i t i l imtnos l a iiistrticcicín (le tres clirecciones

Page 430: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 GEHERACI~N DE C~DIGO

Una asignación de campo tal como la sentencia de C

x. j = x.i;

puede ser traducida en el código de tres direcciones

Ahora consideremos los apuntadores. Supongamos, por ejemplo, que x es declarada como un apuntador para un entero, por ejemplo mediante la declaración en C

int * x;

Supongamos además que i es una variable entera normal. Entonces la asignación en C

*x = i;

puede ser traducida previamente en la instrucción de tres direcciones

*x = i

y la asignación

i = *x;

en la instrucción de tres direcciones

i = *x

Para ver cómo interactúa la indirección de apuntadores con los cálculos de dirección de campo consideremos el ejemplo siguiente de una estructura de datos de árbol y declaració de variable en C:

typedef struct treeNode

f int val;

struct treeNode * lchild, * rchild; 1. TreeNode;

... TreeNode *p;

Ahora consideremos dos asignaciones típicas

Estas sentencias se producen en el código de tres direcciones

Page 431: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generación de código de referencias de estructuras de datos 427

Código P para referencia de apuntador y estructura Dada la declaración de x al principio de es- ta exposición (página 423), se puede hacer un cálculo directo de la dirección de x. j en c6- digo P de la manera siguiente:

Ida x

lod field-offset(x.j)

ixa 1

La sentencia de asignación

puede ser traducida en el código P

Ida x

lod field-offaet(x,j)

ixa 1

Ida x

ind field-offset (x, i)

sto

Advierta cómo se utiliza la instrucción ind para obtener el valor de x. i sin calcular prime- ro su dirección completa.

En el caso de los apuntadores (con x declarada como una f nt*), la asigntición

*x = i;

se traduce al código P

lod x

lod i

ato

y la asignación

se traduce al código P

Ida i lod x

ind O ato

Concluimos con el código P para las asignaciones

(vease la declaración de p en la pigina anterior). Esto se traduce ti1 siguiente d i g o P:

Page 432: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 GENERACION DE C ~ D I G O

lod. g

lod field._offset(*p,lchild)

ixa 1

lod g

Sto

Ida p

lod g

ind field-offset(*p,rchild)

Sto

De~arnos los detalles de un procedimiento de generación de código que generará es cuencias de código ya sea en cbhigo de tres direcciones o en código P para los ejerci

4 GENEMC~ON DE CODIGO DE SENTENCIAS DE CONTROL

En esta sección describiremos la generación de código para varias formas de seritenci control. Entre éstas se encuentran principalmente la sentencia if y la sentencia while e. turadas, las que veremos con detalle en la primera parte de esta sección. También inclui en esta descripción el uso de la sentencia break (como en C). pero no comentaremos el e trol de bajo nivel como el de la sentencia goto, puesto que tales sentencias se pueden ini mentar con facilidad directamente en código intermedio o código objetivo. Otras f'ormrt control estructtirado. como la sentencia repeat (o la sentencia do-while), la sentenci la sentencia case (o seutenciri switch), se dejan para los ejercicios. Otra tkcnica de i mentación útil p a a la sentencia switch, denominada tabla de salto, también se dese un ejercicio.

La generación de código intermedio para sentencias de control, tanto en código tres direcciones como en código P, involucra la generación de etiquetas de una man similar a la generación de nombres temporales en el código de tres direcciones, pero este caso representan direcciones en el código objetivo a las que se harán los saltos. las etiquetas están por eliminarse en la generación de código objetivo, entonces surg problema, ya que los saltos a localidades de código que ya no son conocidas se de reajustar o reescribir retroactivamente. Cornentarernos esto en la siguiente parte de e sección.

Las expresiones lógicas, o booleanas, que se utilizan como pruebas de control, también se pueden emplear de manera independiente como datos, se analizan a con ción, particularmente respecto a la evaluación de cortocircuito, en la cual difieren de expresiones aritméticas.

Finalmente, en esta sección presentaremos un procedimiento de generación de código de muestra en código P prm sentencias iF y while.

84.1 Generación de código para sentencias if y while Considet.;irertios las siguientes dos formns de las scntcncias if y while, que son sirnil:ires en :

rn~iclios lengii~jes diferentes (pel-o que aquí daremos en una sintaxis tipo letiguaje C):

Page 433: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generación de código de sentenciar de control y expresiones lógicas 429

F l ~ ~ r d 8 10 Arreglo de código tip~co para m a rentencia if

El problema.principal en la generación de código para tales sentencias es traducir las carac- terísticas de control estructurado en un equivalente "no estructurado" que invotucre saltos, los cuales se pueden i~riplementar direct;unente. Los conipiladores se encargan de generar código para tales sentencias en un orden estánilar que perniite el uso eficiente de un subcon- junto de los saltos posibles que una arquitectura objetivo puede permitir. Arreglos de códi- go típicos para cada una de estas sentencias se muestran en las figuras 8.10 (más abajo en esta tnisnia página) y 8.11 (página 430). (La figura 8.10 muestra una parte else (el caso FALSE). pero esto cs opcional, de acuerdo con la regla gramatical que se dio reciente- mente, y el rirreglo mostrado es fhcil de modificar para el caso de una parte else omitida.) En cada uno de esos arreglos existen sólo dos clases de saltos (saltos incondicionales y sal- tos cuando la condición es falsa) y el caso verdadero es siempre un caso "que que& en la nada" que no necesita saltos. Esto reduce el número de saltos que e1 compilador necesi- ta generar. También significa que sólo son necesarias dos instrucciones de salto en el códi- go interniedio. Los saltos Falsos ya aparicieroii en el código de tres direcciones de la figu- ra 8.2 (como una instrucción if-false. . goto) y en el ccídigo P de la figura 8.6 (como tina instrucción f jg). Resta introducir los saltos incondicionales, los cuales escribiremos

código antes de la sentencia if

cód~go para la prueba ~f

salto condieronal

' l código para el caso TRUE (verdadero)

salto incondtcional b código para el caso

FALSE

FALSE

-

/ código después de i la sentencia if i...----2

Page 434: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Fifura 8,11 Arreglo de código típico para una sentencia while

QP. 8 1 G E N E ~ ~ N DE CÓDIGO

simplemente como instrucciones goto en código de tres direcciones y como el u j p conditional jump) en código P.

código antes de la sentencia while

código para la prueba while

I

código para el cuerpo del while

código después de la sentencia while

Código de tres direcrianes para sentenciar de control Supondremos que el generador de código genera una secuencia de etiquetas con nombres tales como L1, La, y así sucesivamente. PWU la sentencia

se generar4 el siguiente patrón de código:

cede to evaluate E to tl> if-false tl goto L1

<cede for SI>

goto L2

label L1

<cede for S2>

label L2

Page 435: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generación de código de sentencias de control y expresiones lógicas

De manera similar, una sentencia while de la forma

while ( E ) S

provocaría que fuera generado el siguiente patrón de código de tres direcciones:

label L1

<coda to evaluate E to tl> if-falee tl goto La

<coda for S>

goto L1

label L2

Código P para sentencias de control Para la sentencia

se genera el siguiente patrón de código P:

<cede to evaluate E> fjp L1 <cede for SI>

ujp lab L1

<cede for SZz

lab L2

y para la sentencia

while ( E ) S

se genera el siguiente patrón de código P:

lab L1

cede to evaluate E>

fjp L2 <cede for S>

ujp L1 lab L2

Advierta que todas estas secuencias de código (tanto de código de tres direcciones como de código P) finalizan en una declaración de etiqueta, a la que pr>dríamos llamar etiqueta de salida de la sentencia de control. Muchos lenguajes proporcionan una construcción de len- guaje que permite salir de los ciclos o bucles desde ubicaciones arbitrarias dentro del cuerpo del ciclo. Por ejemplo, C proporciona la sentencia break (que también se puede uti- lizar dentro de sentencias switch). En estos lenguajes se debe disponer de la etiqueta de salida para todas las rutinas de generación de código que se pueden llamar dentro del cuerpo de un ciclo, de manera que s i se encuentra una sentencia de salida tal corno un "break", se pueda generar un salto hacia la etiqueta de salida. Esto transforma a la etiqueta de salida en

Page 436: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 G E N E R A C I ~ N DE ~ 6 0 1 ~ 0

un atributo heredado durante la generación del código, que debe almacenarse en una p bien pasarse como un par:imctro a las rutinas de generación de código apropiada. M& lante en esta sección se ofrecerán tris detalles al respecto. ademjs de ejeinplos de otras tuaciones donde puede ocurrir esto último.

8.4.2 Generación de etiquetas y reajuste Una característica de la generación de código para sentencias de control que puede pro:. vocar problemas durante la generación de código objetivo es el hecho de que, en algunos casos, dehen generarse saltos a una etiqueta antes de que ésta se haya definido. Durante la; generación de código intermedio, esto presenta pocos problemas, puesto que una rutina:: de generación de cúdigo simplemenk puede llamar al procetliniiento de generación etiqueta cuando se necesita una para generar un salto hacia delante y grabar el nombre.' de la etiqueta (de manera local o en una pila) hasta que se conozca la ubicación de la eti- queta. Durante la generación del código objetivo, las etiquetas simplemeute se pueden pasar a un ensarublador si se está generando código ensamblador, pero si se va a generar código ejecutable real, estas etiquetas dehen transformarse en ubicaciones de código abso- lutas o relativas.

Un niétodo estándar para generar tales saltos hacia delante consiste en dejar una brecha en el código donde e1 salto vaya a ocurrir, o bien, generar una instrucción de salto ficticia a una localidad falsa. Entonces, una vez que se conoce la ubicación del salto real, se utiliza para arreglar o ajustar el cíidigo omitido. Esto requiere que el código generado se manten- ga en un "buffer" o "almacenamiento temporal" en la memoria, de manera que el ajuste se pueda hacer al vuelo, o que el código se escriba en un archivo tetnporal y después se reintro- duzca y ajuste cuando sea necesario. En cualquier caso, puede ser necesario que los ajustes se almacenen de manera temporal en una pila o se mantengan localmente en procedimien- tos recursivos.

Durante el proceso de ajuste puede surgir otro problema, ya que inuclias ;irquitecturas tienen dos variedades de saltos, un salto cono o ramificación (dentro de, diganios, 128 bytes de código) y un salto largo que requiere mis espacio de ciídigo. En ese caso un generador de ccídigo puede necesitar insertar instrucciones nog cuando salte de manera breve, o hacer varios pasos para condensar el código.

8.4.3 Generación de código de expresiones lógicas Hasta ahora no hemos dicho nada acerca de la generación de código para las expresiones ló- gicas o booleanas que se utilizan corno pruebas en sentencias de control. Si el código inter- medio tiene un tipo de datos booleano, y operaciones lógicas como and y or, entonces el valor de una expresión booleana se puede calcular en cbdigo intermedio exactamente de la misma manera que una expresión aritniética. Éste es el caso para el código P, y el código intermedio se puede diseñar de manera similar. Sin embargo, incluso si éste es el caso, la traducción en código objetivo por lo regular requiere que los valores hooleanos se represen- ten aritméticamente, ya que la mayoría de las arquitecturas no tienen un tipo booleano inte- grado. La manera estándar de hacer esto es representar el valor booleano false como O y true como 1. Entonces, los operadores estándar de hit and y or se pueden utilizar para cdcular el valor de una expresión booleana en la mayor parte de las arquitecturas. Esto requiere yue el resultado de operaciones de cornparacicín corno .r se nornialice a O o 1. En algunas ;ircluitccturas esto requiere yue O o 1 se carguen de rnmera explícita, puesto que el operador de coinpar;icitln riiisrno s6lo est:iblece un ctídigo de conrlicitín. En cse caso iieccsitnn gcne- rnrse siiltt~s t~tlicion:~les p:ira cargar el v:ilor ;ipropiado.

Page 437: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generación de código de sentencias de control y expresiones lógicas 433

Si las operaciones lógicas son de cortocircuito, como en C, es necesario un uso adi- cional de los saltos. Una operación lógica es de cortocircuito si puede fallar la evaluación de su segundo argumento. Por ejemplo, si u es una expresión hooleüna que se calcula como falsa, entonces la expresión booleana (1 and b se puede dctermirtar inmediatamente co- rno falsa sin evaluar h. De manera similar, si se conoce que tz es verdadera, entonces n or b se puede determinar como verdadera sin evaluar h. Las operaciones de cortocircuito son muy útiles para el codificador, ya que la evaluación de la segunda expresión ocasionaría un error si un operador no fuera de cortocircuito. Por ejemplo, es común escribir en C

donde la evaluación de g- >val cuando g es 'nula podría causar una falla de rnemwia. Los operadores booleaiios de cortocircuito son semejantes a las sentencias if, sólo que

estos devuelven valores, y con frecuencia se definen utilizando expresiones if como

a and b = if CI then b else false

(I or b - if a then true etse b

Para generar código que asegure que la segunda siibexpresión será evaluada sólo cuan- do sea necesario, debemos emplear saltos exactamente de la misnia ntanera que en e1 c d i - go para las sentencias if. Por ejemplo, el código P de cortocircuito para I i i expresión en C (xl=O)&&(y~=x) es:

lod x ldc O

neq f j p L1

lod y

lod x

egu ujp L2

lab L1

lod FALSE

lab L2

8.11'4 Un procedimiento de generación de código de muestra para sentencias if y while

En esta sección mostr:irernos un proccdirniento tle generación de código para scntericitts de control utili7.ando la siguiente gramática sirnplilicaúa:

Page 438: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 GENERACI~N DE C~DIGO

Por simplicidaú, esta gramitica utiliza el token other para representar sentencias no i cluidas en la gramática (como la sentencia de asignación). También incluye únicamente 1 expresiones booleanas constantes true y false. Incluye una sentencia break para m trar cómo una sentencia así se puede implementar utilizando una etiqueta heredada pasa como un parámetro.

Con la siguiente declaración en C se puede implementar un árbol sintáctico abstracto p ra esta gramática:

typedef enum {ExpXind,IfXind.

WhileKind,BreakKind,OtherKind) NodeXind;

typedef struct streenode

( NodeXind kind;

struct streenode * childI31; int val; / * usado con ExpXind */

1 STreeNode; typedef STreeNode *SyntaxTree;

En esta estnictura de árbol sintáctico, un nodo puede tener hasta tres hijos (un nodo if con una parte eise), y los nodos de expresión son constantes con valor verdadero o hlso (alma- cenado en el campo val como un 1 o un O). Por ejemplo, la sentencia

if(true)while(true)if(false)break else other

tiene el árbol sintáctico

donde únicamente hemos mostrado los hijos no nulos de cada nodo." En la figura 8.12 se proporciona un procedimiento de generación de código que genera

código P utilizando los typedef dados y la correspondiente estructura de árbol sintáctico. Acerca de este código hacemos las observaciones siguientes:

9. La ambigüedad del else ambiguo en esta gramática se resuelve mediante la regla cst;índa de la "widacih más cercana", corno lo muestra el árhol sintictico.

Page 439: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 8.12 procedimiento de generación de código para sentencias de

Generacion de codigo de sentencias de control y expresiones lógicas

void genCode( SyntaxTree t, char * label) ( char codeatr[CODESIZEl; char * labl, * lab2; if (t != NULL) switch (t-zkind) { case ExgKind:

if (t->val==O) emitCode("1dc false"); else emitCode("1dc truen); break;

case IfKind: gencodett->child[Ol,label); labl = genlabelo; sgrintf (codestr, "%a %si', "f jp", labí) ; emitCode(codestr); genCode(t->child[ll,label); if (t->child[2] I = NULL) { lab2 = genlabelo; sprintf (codestr, "%S %S', "ujg" , lab2) ; emitCode(codestr);l

sgrintf(codestr, "%S %S", "lab", labl) ; emitcodefcodestr); if (t->child[Zl !E NULL) { genCode(t->child[2Irlabel); sgrintf (codestr, *%S %S". "lab". lab2 ) ; emitCode(codeatr);).

break; case WhileKind: labl = genlabelo; sgrintf (codestr, "%S %S", "lab" . labl) ; emitCode(codestr); genCode(t->child[OI,label); lab2 = genlabelo ; sgrintf (codestr, "%S %S", "fjg", lab2) ; emitCode(codestr) ; genCode(t->child[ll,lab2); sprintf (codestr, "%a %S", "ujg", labl) ; emitCode(codestr); sgrintf (codestr,"%s %S", "lab", lab2) ; emitCode(codeatr); break;

case BreakKind: sprintf (codestr, "%S %S", "ujp",label) ; emitCode(c0destr); break;

case OtherKind: emitCode("0ther"); break;

default: emitCode ( "Error" ) ; break;

1 1

Page 440: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

UP. 8 1 G E N E M c I ~ H DE C ~ D ~ G O

En primer lugar, el código supone, como antes, la existencia de un procediin' ernitcode (este procedimiento simplemente imprimiría la cadena que se le pas código también supone la existencia de un procedimiento genLabel sin parámet devuelve nombres de etiqueta en la secuencia L1, L2, L3, y así sucesivamente.

El procedimiento senCode aquí tiene un paráinetro labei extra que es necesario generar un salto absoluto en el caso de una sentencia break. Este parámetro sólo se c en la llamada recursiva que procesa el cuerpo de una sentencia while. De este modo, una s tencia break siempre provocara un salto fuera de la sentencia while anidada nrás cerc (La llanada inicial a genCode puede utilizar una cadena vacía como el parametro Labe cualquier sentencia break que se encuentre fuera de una sentencia while generará enton un salto a una etiqueta vacía y, en consecuencia, un error.)

Advierta también cómo las variables locales labl y lab2 se emplean para guar nombres de etiqueta, para los cuales.existen saltos y/o definiciones aún pendientes.

Por último. puesto que una sentencia other no corresponde a ningún código real, en te caso este procedimiento simplemente genera la instrncción en código no P "Other".

Dejamos al lector describir la operación del procedimiento de la figura 8.12 y m que para la sentencia

if(true)while(true)if(falae)break elae other

se genera la secuencia de código

ldc true

fjp L1

lab L2

ldc true

fjp L3

ldc falae

fjp L4

ujg L3

U ~ P L5

lab L4

Other

lab L5

UJP La

lab L3

lab L1

GENERACION DE CÓDIGO DE LLAMADAS DE PROCEDIMIENTOS Y FUNCIONES

Las llamadas de procedimientos y funciones son el último mecanismo de lenguaje que co- mentamos en térniinos generales en este capítulo. La descripción del código intermedio y del código objetivo para este mecanismo es, mucho más que otros mecanismos de lengua- je, complicada por e1 hecho de que diferentes máquinas objctivo utilizan mecanismos muy diferentes para realizar llamadas y por cl hecho de que Iris llamadas dependen mucho (le la t,rganii.acidn del ambiente de ejecución. Por consiguiente, es difícil obtener una representación

Page 441: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generaciin de código de llamadas de procedimientos y funciones 437

de código intermedio que sea tan general conio para emplearla en cualquier ambiente de ejecución o arquitectura objetivo.

8.5.1 Código intermedio para procedimientos y funciones Los requerimientos para las representaciones de código intermedio de Ilamndas de funcio- nes pueden describirse en términos generales como sigue. En primer lugar, existen en reali- dad dos niecanismos que necesitan descripción: la definición de función/pmcedimiento (también conocida como declaracih) y la llamada de función/procediruient~.'~' Una de- finición crea un nombre de función, parámetros y código, pero la función no se ejecuta en ese punto. Una llamada crea valores reales para los parámetros (o argumentos para la Lla- mada) y realiza un salto hacia el código de la función, el cual se ejecuta entonces y regresa. El ambiente de ejecución en el que ésta tiene lugar no se conoce cuando se'crea el código p a a la función, excepto en su estructura general. Este ambiente de ejecución es construido en parte por el elemento que llama, y en parte por el cúdigo de la función llamada; esta divi- sión de la responsabilidad forma parte de la secuencia de llamada estudiada en el capítulo anterior.

El código intermedio para una definición debe incluir una instrucción que marque el inicio, o punto de entrada, del código para La función, y una instrucción que marque el fi- nal, o punto de retorno, de la función. De manera esquemática podemos escribir esto de la manera siguiente:

Instrucción de entrada

<código para el cuerpo de la función>

Instrucción de retorno

De la misma manera, una llamada de función debe tener una instrucción que indique el prin- cipio del cálculo de los argumentos (en preparación para la llamada) y luego una initrucción de llamada red que indique el punto en que los argumentos han sido construidos y el salto real hacia el código de la función puede tener lugar:

Instrucción de comienzo de cálculo de argumento

<código para calcular los argumentos>

Instrucción de llamada

Diferentes versiones del código intermedio tienen versiones muy diferentes de estas cuatro instrucciones agrupadas, en particular respecto a la cantidad de información acerca del ambiente, los parámetros y la función misma que es parte de cada instrucción. Ejemplos típicos de tal información incluyen el número, tamaño y ubicación de los parámetros; el ta- maño del marco de pila; el tamaño de las variables locales y el espacio temporal, así como diversas indicaciones del uso del registro por la función llamada. Como es habitual, pre- sentaremos código intermedio que contiene una mínima cantidad de información en las inscntcciones mismas, con la idea de que cualquier información necesaria puede mantenerse \eparadamente en una entrada de la tabla de símbolos para el procedimiento.

iO. A lo largo de este texto adoptamos el criterio de qne las funciones y procedimientos wpresen- tan en esencia e1 mismo rnecanisrno y, sin abundar en el tema, consideraremos que son lo rnisnio para los propósitos de esta sección. La diferencia es. por supuesto, que una funci0n dshe tener disponible un valor devuelto para cl clernento que llama criando salga y el clernento que Ilaina debe saber ddnde encontrarlo.

Page 442: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 / GENERACI~N DE C~DIGO

Códko de tres direcciones para procedimientos y funciones En código de tres direcciones, I trucción de entrada necesita dar un nombre al punto de entrada de procedimiento, se te a la instrucción label; de este modo, es una instrucción de una dirección a la que maremos simplemente entry. De manera similar, llamaremos return a la instrucción retorno. Esta instrucción también es una instrucción de una dirección: debe dar el no del valor devuelto, si existe uno.

Por ejemplo, considere la definición de la función en C

in t f ( in t x , int y)

( return x+y+l; 1

Esto se traducirá al siguiente código de tres direcciones:

entry f

t l = x + y

t 2 = tl + 1

return t 2

En el caso de una llamada, de hecho necesitamos tres diferentes instrucciones de tres d ime nes: una para señalar el inicio del cálculo del argumento, a la que llamaremos begin-ar (y la cual es una instrucción de dirección cero); una instrucción que se utiliza de manera petida para especificar los nombres de los valores de argumento, a la que denominarem arg (y que debe incluir la dirección o nombre del valor del argumento); y finalmente, instrucción de llamada real, que denotaremos simplemente como call, la cual también una instrucción de una dirección (debe proporcionarse el nombre o punto de entrada de función que se está llamando).

Por ejemplo, supongamos que la función f se definió en C como en el ejeniplo anterior. Entonces, la Llamada

f (2+3,4)

se traduce al código de tres direcciones

begi-rga

t l = 2 + 3

arg tl

arg 4

c a l l f

Aquí enumeramos lor argumentos en orden de izquierda a derecha. El orden podría, por su- puesto, ser diferente. (Véase la sección 7.3.1, página 361.)

Código P para procedimientor y funciones [,a instrucción de entrada en código P es ent, y la iiistrucción de retorno es ret. De esta manera, la definición anterior de la Liirtción f de C se traduce al código P

ent f

lod x

Page 443: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generación de codigo de llamadas de procedimientos y funciones

lod y

adi

ldc 1

adi

ret

Advierta que la instrucción ret no necesita un paráinetro para indicar cuál es el valor devuelto: se supone que el valor devuelto está en el tope de la pila de la niríquina P al re- torno.

Las instrucciones en código P para una llamada son la instrucción rnst y la instrucción cup. La instrucción mst viene de "mark stack" y corresponde a la instrucción en código de tres direcciones begin-args. La razón por la que se denomina "mark s tack es que el código objetivo generado de una instrucción de esta naturaleza está involucrado con el es- tablecinliento del registro de activación para la nueva llamada en la pila, es decir, los prime- ros pasos en la secuencia de llamada. Esto generalmente significa, entre otras cosas, que el espacio debe ser asignado o "marcado" en ta pila para elementos tales como los argu- mentos. La instrucción cup en código P es la instrucción de "procedimiento de llamada de usuario'' y corresponde directamente a la instrucción cal1 del código de tres direcciones. La razón de que reciba este nombre es que el código P distingue dos clases de llamaóas: cug y cap, o "call standard procedure". Un procedimiento estándar es un procedimiento "inte- gr.3do" requerido por la definición del lenguaje, como los procedimientos sin o abs de Pascal (C no tiene procedimientos integrados de los cuales hablar). Las llamadas a procedi- mientos integrados pueden emplear conocimiento específico acerca de su funcionamiento para mejorar la eficiencia de la llamada (o inclusive eliminarla). No consideríiremos más la operación cap.

Advierta que no introducimos una instrucción en código P equivalente a la instrucción arg en código de tres direcciones. En vez de eso se supone que todos los valores de argurnen- tos aparecen en la pila (en el orden apropiado) cuando se encuentra la instrucción cup. Esto puede producir un orden un poco diferente para la secuencia de llamada que el correspondien- te id código de tres direcciones (véanse los ejercicios).

Nuestro ejemplo de una llamada en C (la llamada f (2+3,4) para la función f descrita anteriorniente) se traduce ahora en el siguiente código P:

ms t

ldc 2

Idc 3

adi

ldc 4

cug f

(De nueva cuenta, calculamos los argumentos de izquierda a derecha.)

8.5.2 Un procedimiento de generación de codigo para definición de función y llamada

Corno cn secciones anteriores, deseamos mostrar un procedimiento de gcncración dc &di- go I):I~U una grainitica de iiiuestra con definiciones de funciún y Ilarriiidas. La gcunática que utiliz:~renios es la que se muestra a con~inuacicin:

Page 444: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. U 1 GENERACIÓN DE CÓDIGO

program -+ decl-lis? exp decl-list -+ ciecl-lis? decl / e decl -+ fn id ( param-list ) = exp param-list 4 param-lis? , id / id exp -+ exp + exp / llamada / num / id llamada -+ id ( arg-lis? ) arg-lis? -, arg-lis? , exp / exp

Esta gramática define un programa como una secuencia de declaraciones de funciones, guidas por una expresión simple. No hay variables o asignaciones en esta gramática, s parámetros, funciones y expresiones, las cuales pueden incluir llamadas de función. Tod los valores son enteros, todas las funciones devuelven enteros y todas las funciones ben tener por lo menos un parámetro. Existe sólo una operación numérica (aparte de mada de función): la adición entera. Un ejemplo de un programa definido median gramática es

Este programa contiene dos definiciones de funciones seguidas por una expresión que es una llamada a g. También hay una llamada a f en el cuerpo de g.

Queremos definir una estructura de árbol sintáctico para esta gramática. Lo haremos uti- lizando las siguientes declaraciones en C:

typedef enum

~PrgX,FnX,ParamK,PlusX,CallX,ConstX,IdX)

NodeKind;

typedef struct streenode

( NodeXind kind;

atruct streenode *lchild,*rchild, *sibling;

char * name; / * usado con FnX,ParamK, CallK, IdX */

int val; / * usado con ConstK * / 1 STreeNode;

typedef STreeNode "SyntaxTree;

Tenemos 7 diferentes clases de nodos en esta estructura de árbol. Cada irbol sintác tiene una raíz que es q nodo PrgK. Este nodo es utilizado simplemente para agrupar declaraciones y la expresión del programa en conjunto. Un árbol sintáctico contiene exac mente un nodo así. El hijo izquierdo de este nodo es una lista de hermanos de nodos FnK; hijo derecho es la expresión del programa asociado. Cada nodo FnK tiene un hijo izquierd que es una lista de hemanos de nodos Par-. Estos nodos definen los nombres de los pa- rámetros. El cuerpo de cada función es el hijo derecho de su nodo FnK. Los nodos de ex- preiiún se representan de la manera habitual, sólo que un nodo CallK contiene el nombre de la función llamada v tiene un hiio derecho que es una lista de hermanos de las expresio- nes de argumento. Por ejemplo, el programa de muestra anterior tiene el khol sintáctico que se da en la figura 8.13. Por claridad incluimos en esa tigura la clase de nodo correspondiente - a cada nodo, junto con algunos atnhutos nonibre/ialor. Los h~jos y hermanos se distinguen por la direcci6ii (hermanos a la derecha, hijos debajo).

Page 445: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

f igura 8.13 Arbol sintáctico abstracto para el programa de mues- tra en la página 440

Figura 8.14 Procedimiento de generacion de código p a n definición de función y llamada

Generación de código de llamadas de procedimientos y funciones

ParamK

"\ Par- Par- P\ ConstK COnStK

X X - 3 - 4

ConstK IdK a X

Dada esta estructura de árbol sintáctico, en la figura 8.14 se proporciona un procedi- miento de generación de código que produce código P. Acerca de éste código hacemos las siguientes observaciones. En primer lugar, el código para un nodo PrgK simplemente reco- rre el resto del árbol que tiene por debajo. El código para un IdK, ConstK o plus^ es casi idéntico al de los ejemplos anteriores.

void genCode( SyntaxTree t) ( char codestr[CODESIZEl;

SyntaxTree p; if (t 1=NuLL)

switch (t->kind) ( case PrgK:

p = t->lchild; while (p 1 = NuLL)

I genCode íp) ; p = p->sibling; >

genCode(t->rchild);

break; case FnK:

sprintf (codestr,*%s %S", "entU,t->name) ;

emitCode (codestr) ;

genCode(t->rchild); emitCode("ret"); break;

case Par&: / * sin acciones */ break:

case ConstK:

sprintf (codestr, "%S %dl', "ldc", t->val) ; emitCode(codestr);

break; case PlusK:

genCode(t->lchild);

genCode(t->rchild); emitCode("adi");

break:

Page 446: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

442 CAP. 8 1 G E N E R A E I ~ N DE C ~ D I G O

Figura 8.14 Conlinuation case I ~ K : sprintf(codestr,"%s %s","lodW,t->name);

break;

case CallK:

emitCode("mst");

p = t->rchild;

while (p!=NULL)

I gencode (P) ;

sgrintf (codestr, "%S %S", "cup", t-zname) ;

emitCode(codestr);

break;

default :

emitcode ("Error") ;

break;

Restan los casos de FnK, Par- y CallK. El código para un nodo FnK simple te rodea el código para el cuerpo (le la función (el hijo derecho) con ent y ret; los par metros de la función nunca son visitados, En realidad, los nodos de parámetro nunca provo- can que se genere &digo: los argumentos ya han sido calculados por el elemento que llama.'' Esto explica también por qué no hay acciones para un nodo Par- en la figura 8.14; en efecto, debido al cornportiuniento del código para FnK, no deberían ser alcanzados nodos ParamK en el recorrido del árbol, de modo que este caso en realidad podría ser desechado.

El caso final es Callk. El código para este caso resulta de una instrucción mst, que procede a generar el código par;i cada argumento, y finalmente de una instrucción cug.

Dejamos al lector demostrar que el procedimiento de gener:ición de código de la figura 8.14 produciría el siguiente cOdigo P, dado el programa cuyo árbol sintáctico $e muestra en la figura 8.13:

ent f

IdC 2

lod x

aüi

ret

ent g

mst

11. Sin cnib~rgo. los notlos de parámccro tienen un importante p:ipel de aJminictracii>n cn cl scn- tido que tktertnittün I:is ~iosiciones rel;itiv:is, (1 deipluzarnientos. donde sc piteden encontrar los parAmeirus eit el rcgisiro ilc .ictivacitiii. Siipondreinns que esto es ni;iiiejü(lo en otra parte.

Page 447: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generación de codigo en compiladores comerciales: dos casos de estudio

lod x

cup f

lod y

adi

ret

ms t

ldc 3

ldc 4

CUP

8.6 GENERACIÓN DE CÓDIGO EN COMPIMDORES COMERCIALES: DOS CASOS DE ESTUDIO

En esta sección examinaremos la salida de código ensamblador producida por dos diferen- - tes compiladores comerciales para diferentes procesadores. El primero es el compilador pa- ra C de Borland versión 3.0 para procesadores lntel 80x86. El segundo es el compilador para C de Sun versión 2.0 para sparcstations. Mostraremos la salida de ensamblidor de estos compiladores para los mismos ejemplos de código que utilizamos para ilustrar el código de tres direcciones y e1 código P." Esto debería mejorar la comprensión de las técnicas de generación de código, así como de la conversión de código intermedio a código - objetivo. También debería suministrar una comparación útil para el código de máquini pro- ducido por el compilador 'TINY, el cual se comentar5 en secciones posteriores.

8.6.1 El compilador C de Borland 3.0 para la arquitectura 80 X 86 Comenzamos nuestros ejemplos de la salida de este compilador con la asignación utilizada en la sección 8.2.1:

Supondremos que la varkable x en esta expresión está almacenada localmente en el marco de pila.

El código ensamblado para esta expresicín tal como se produce mediante el compilador Borland versrón 3.0 para la arquitectura Intel 80x86 es como se niuestra a continuación:

mov ax,word ptr [bp-21

add ax, 3

mov word ptr [bg-21,ax

ndd ax, 4

En este código el registro de acumulador ax se utiliza como la ubicación temporal principal para el cálculo. La ubicación de la variable local x es bp-2, lo que refleja el uso del regis- tro bp (base pointer ) como el apuntador de marco y el hecho de que las variables enteras ocupan dos bytes en esta máquina.

La primera instrucción mueve el valor de x hacia ax (los corchetes en 13 dirección [bp-21 indican una carga indirecta más que inmediata). Ida segunda instrucción agrega la

12. Para los prop6sitos de Aste y la inayoría de otros ejemplos, las c~ptimilaciones que estos c m piladores re~lizan cstiii drsac1ivdd;is.

Page 448: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

UP. 8 1 G E N E R A C I ~ N DE C ~ D I G O

constante 3 a este registro. La tercera instrucción mueve entonces este valor a la ubica de x. Finalmente, la cuarta instmcción agrega 4 a ax, de manera que el valor final de 1 presión se deja en este registro, donde se puede utilizar para ciílculos adicionales.

Observe que la dirección de x para la asignación en la tercera instnicción no está cal lada previamente (como una instrucción Ida de código P podría sugerir). Una simulac estática del código intermedio, junto con el conocimiento de los modos de direccionami to disponibles, pueden aplazar el cálculo de la dirección de x hasta este punto.

Relerenciar de arreglo Utiiizamos como ejemplo la expresión en C

(a[i+lI=2)+a[jl

(véase el ejemplo de la generación del código intermedio en la página 422). También supo- nemos que i, j y a son variables locales declaradas como

int i , j ;

int a[lOl;

El compilador C de Borland genera el siguiente código ensamblado para la expresión (con el fin de facilitar la referencia numeramos las líneas del código):

(1) mov

(2) shl

(3) lea

(4) add

( 5 ) mov

(6) mov

(7) mov

( 8 ) shl

(9) lea

(10) add

(11) add

bx,word ptr [bp-21

bX, 1

ax,word ptr [bp-221

bx, ax

ax, 2

word ptr [bxl , ax

bx,word ptr [bp-41

bx, 1

dx,word ptr [bp-241

bx,dx

ax,word ptr [bxl

Puesto que los enteros tienen un tamaño de 2 en esta arquitectura, bp-2 es la ubicación de i en el registro de activación local, bp-4 es la ubicación de j, y la dirección base de a es bp-24 (24 = tamaño de índice de arreglo de 10 X tamaño de entero de 2 bytes + espacio de 4 bytes para i y j). De este modo, la instrucción 1 carga el valor de i en bx, y la instruc- ción 2 multiplica ese valor por 2 (un desplazamiento a la izquierda de 1 bit). La instrucción 3 carga la dirección base de a en ax ( l e a = load effective address), pero ya ajustada al agregar 2 bytes para la constante 1 en la expresión ~ubindizada itl. En otras palabras, el compilador ha aplicado el hecho algebraico de que

La instrucción 4 calcula entonces la dirección resultante de a [ it 11 en bx. La instmcción 5 mueve la constante 2 al registro ax, y la instrucción 6 almacena esto para la dirección de a [i+l]. La instrucción 7 carga entonces el valor de j en bx, la instrucción 8 multiplica este valor por 2, la instrucción 9 carga la dirección base de a en el registro dx, la instruc- ción 10 calcula la dircccicin de.a [ j ] en bx y la instrucción 11 agrega el valor almacenado

Page 449: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generación de chdigo en compiladores comerciales: dos casos de estudio 445

en esta dirección al contenido de ax (a saber, 2). El valor resultante de la expresión se de- ja en el registro ax.

Referencia de apuntador y campo Tomaremos las declaraciones de ejemplos anteriores:

typedef struct rec

( int i;

char c;

int j;

1 Rec;

typedef stmct treeNode

{ int val;

stmct treeNode * lchild, * rchild; 1 TreeNode;

... Rec x; TreeNode *p;

También supondremos que x y p se declaran como variables locales y que se hizo la asig- nación apropiada de apuntadores.

Consideremos en primer lugar los tamaños de los tipos de datos involucrados. En la arquitectura 80x86 las variables enteras tienen un tamaño de 2 bytes, las variables carácter tienen un tamaño de 1 byte y los apuntadores (a menos que se declare de otra manera) son los denominados apuntadores "cercanos" con un tamaño de 2 bytes.I3 De este modo, la va- riable x tiene un tamaño de 5 bytes y la variable p de 2 bytes. Con estas dos variables de- claradas localmente como las únicas variables, x se asigna en el registro de activación en la localidad bp-6 (las variables locales están asignadas sólo en límites pares de bytes, una res- tricción típica, de modo que el byte extra queda sin utilizar), y p se asigna al registro si. Adicionalmente, en la estructura en Rec, i tiene desplazamiento O, c tiene desplazamiento 2 y j tiene desplazamiento 3, mientras que en la estructura TreeNode, val tiene despla- zamiento O, lchild tiene desplazamiento 2 y rchild tiene un desplazamiento de 4.

El código generado para la sentencia

mov ax,word ptr Ibp-61

mov word ptr [bp-31,ax

La primera instrucción carga x . i en ax, y la segunda almacena este valor para x. j. Ad- vierta cómo el cálculo del desplazamiento para j (-6 + 3 = -3) se realiza estáticamente por el compilador.

El cOdigo generado para la sentencia

13. La arquitectura 80x86 tiene una clase i n h general de apuntadores denominados apuntadore\ "tar" ("lejano\") con un tamaño de cuatro bytec.

Page 450: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 GENERRCIÓN DE CÓDIGO

es

m o v word ptr [si+2], si

Advierta cómo la indirección y el cálculo del desplazamiento se combinan en u instrucción.

Finalmente, el código generado para la sentencia

p = p->rchild;

es

m o v si,word ptr [si+4]

fentenciaj if y while Mostraremos aquí el código generado mediante el compilad Borland para sentencias de control típicas. Las sentencias que utilizaremos son

if (x>y) y++;else x--;

while (x<y) y -= x;

En ambos casos, tanto x como y son variables enteras locales. El conipilador de Borland genera el siguiente código 80x86 para la sentencia if

donde x se encuentra localizada en el registro bx, mientras que y está localizada en registro dx:

C ~ R bx, dx j le short 61986 inc dx

j m ~ short 919114 61686 :

dec bx

619114 :

Este código utiliza la misma organización secuencial de la figura 8.10, pero advierta que te código no calcula el valor lógico real de la expresión x<y sino que simplemente empl de manera directa el código de condición.

El código generado mediante el compilador Borland para la sentencia while es el que muestra a continuación:

jmp short 618170 Ol(P142 :

sub dx, bx

619170 :

C ~ P bx, dx

j 1 short 616142

Esto utili~a una organi~ación secuencia1 diferente a la de la figura 8.1 1, en el \entido que la prueba ?e ubica dl final, y se hace un salto incondicional inicial para c\ta prueba.

Page 451: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generación de código en compiladores comerciales: dos casos de estudio 447

Deíinicion de funciones y llamadas Los ejemplos que utiliraremos son la definición de la fun- ción en C

int f (int x, int y)

f return x+y+l; f

y una llamada correspondiente

- f proc near

push bp

mov bpt SR

mov ax,word ptr [bp+41

add ax,word ptr [bp+61

inc ax

jmp short @1@58 @1@58 :

POR bp ret

- f endp

(estos ejemplos fueron empleados en la sección 8.5.1). Consideremos, en primer lugar, el código del compilador de Borland para la llamada

f (2+3,8):

mov ax, 4

push ax

mov ax, 5

push ax

call near ptr -f

POR cx

POR CX

Advierta cómo los argumentos se insertan en la pila en orden inverso: primeroel 4, luego el 5. (El valor 5 = 2 + 3 es calculado previamente por el compilador, ya que es una expresión constante.) Advierta también que el elemento que llama es responsable de eliminar los argumentos de la pila después de la llamada; ésta es la razón para las dos instrucciones gog después de la llamada. (El registro objetivo cx se emplea como un receptáculo de "basura": los valores extrjídos nunca se utilizan.) La llamada misma debería ser relativamen- te autoexplicativa: el nombre -f tiene una subraya colocada antes del nombre fuente, lo que es una convención típica para compiladores C, y la declaración near gtr significa que la función está en el mismo segmento (y de ese modo sólo necesita una dirección de dos bytes). Por último observamos que la instmcción cal l en la arquitectura 80x86 inserta automáti- camente la dirección de retorno en la pila (y una instrucción correspondiente ret, que se ejecute mediante la función llamada, la extraerá).

Ahora consideremos el código generado por el compilador de Borland para la definición de f:

Gran parte de este cbdigo debería ser claro. Corno el elemento que llama no construye el rc- gistro de activaci6n excepto para calcular e insertar los argumentos, este cbdigo debe tcrminar

Page 452: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 GENEMCI~W DE C~DIGO

su construcción antes de que el código del cuerpo de la función sea ejecutado. Ésta es rea de la segunda y tercera instrucciones, que graban el vínculo de control (bp) en la pi entonces establecen el bp al sp actual (el cual se conmuta a la activación actual). Desp de estas operaciones la pila tiene el aspecto que se muestra a continuación:

Registro de activación

vínculo de control de la llamada a f

SR bP

Dirección de crecimiento de la pila

La dirección de retorno se encuentra en la pila entre el vínculo de control (el bp antiguo) y los argumentos como un resultado de la ejecución de una instrucción cal1 por el elemen- to que llama. De este modo, el bp antiguo está en el tope de la pila, la dirección de retorho se encuentra en la localidad bp+2 (las direcciones son de dos bytes en este ejemplo), parámetro x está en la localidad bp+4 y el parámetro y en la localidad bp+6. El cuerpo d f corresponde entonces al código que viene a continuación

mov ax, word gtr [bp+4] eidd ax,word ptr [bp+61 inc ax

que carga x en ax, agrega y al mismo, y después lo incrementa en uno. Finalmente, el código ejecuta un salto (que aquí es innecesario, pero de todas formas se

genera porque las sentencias return incrustadas en el código de la función podrían necesi- tarlo), recupera el bp antiguo de la pila y regresa al elemento que llama. El valor devuelto se deja en el registro ax, donde estará disponible para el elemento que llama.

8.6.2 El compilador C de Sun 2.0 para las Sun SparcStations Repetiremos los ejemplos de código utilizados en las secciones anteriores para demostrar la salida de este compilador. Del mismo modo que antes, comenzamos con la asignación

y suponemos que la variable x en esta exprebión es almacenada localmente en el marco de la pila.

El compilador C de Sun produce código ensamblado que es muy similar al del código

Page 453: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generación de código en compiiadorer comercia~er: dos caros de estudio

ld [%fp+-Oxd],%ol

add %01,0x3, %o1

st %Ole [%fp+-0x41

ld [%fp+-Ox41, %o2

add %02,0x4, %o3

En este código, los nombres de registro comienzan con el símbolo de porcentaje, y las constantes comienzan con los caracteres Ox (x = hexadecimal), de manera que, por ejem- plo, 0x4 es el hexadecimal4 (lo mismo que el decimal 4). La primera instrucción mueve el valor de x (en la localidad f p - 4 , porque los enteros son-de cuatro hytes de longitud) al registro 01.'' (Observe que las localidades fuente estiin a la izquierda y las ubicaciones objetivo a la derecha, de modo opuesto a la convención de la arquitectura Intel80X86.) La segunda instrucción agrega la constante 3 a 01, mientras que la tercera almacena o1 en la localidad de x. Finalmente, se vuelve a cargar el valor de x , esta vez en el registro 02,'' y se le agrega 4, colocando el resultado en el registro 03 , donde se deja como el valor final de la expresión.

Referencias de arreglo La expresión

con todas las variables locales se traduce al siguiente código ensamblado mediante el com- pilador de Sun (con las instrucciones numeradas para facilitar la referencia):

(1) add

(2) Id

(3) sil

(4) mov

(5) st

(6) add

(7) ld

( 8 ) sll

(9) id

(10) mov

(11) add

Los cáiculos realizados por este código son afectados por el hecho de que los enteros son de cuatro bytes en esta arquitectura. De esta manera, la localidad de i es f p - 4 (escrito como %fp+-0x4 en este código), la localidad de j es fp- 8 (%f p+- 0x8) y la dirección base de a es f g - 4 8 (escrita %fp+-0x30, puesto que el 48 decimal es igual al 30 hexadecimal).

14. En la SparcStation los registros se indican mediante una letra niinúscula seguida por un núme- ro. Diferentes letcis se refieren a diferentes secciones de una "ventana de registro" que puede cambiar con el contexto y que corresponde aproximadamente a usos diferentes de los registros. Las distincio- nes entre las diferentes letras son irrelevantes para todos los ejemplos en este capítulo, excepto para el que involucre llamadas de Función (vCase la sección 8.5).

15. Este paso no aparece en el código de Borland y es ficilmente optimizado. Vhse 13 sec- ción 8.9.

Page 454: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 GENERACIÓN DE ~ 6 0 1 ~ 0

La instrucción 1 carga al registro o1 con la dirección base de a, modificada (como e código Borland) para restar 4 bytes a la constante I en el subíndice i t l ( 2 c hex = 44 - 4). La instrucción 2 carga el valor de i en el registro 02. La instrucción 3 multiplica o por 4 [un desplazamiento a la izquierda de 2 bits) y pone el resultado en 03. La instrucción 4 carga el regiytro 04 con la constante 2, y la instrucción 5 la almacena en ia dirección de a [ i + l ] . LU instrucción 6 calcula la dirección base de a en 05, la instrucción 7 carga el va- lor de j en 07 , y la instrucción 8 lo multiplica por 4, colocando el resultado en el registro 10. Finalmente, la instrncción 9 carga el valor de a [ j 1 en 11 , la instrucción 10 vuel cargar la constante 2 en 12 , y la instrucción 11 las suma, colocando el resultado (y el v Final de la expresión) en el registro 13.

Referencias de apuntador y campo Consideraremos el mismo ejemplo que antes (vé página 445). En Sun, los tamaños de los tipos de datos son los siguientes: las vari enteras tienen un tamaño de 4 bytes, las variables de carácter tienen un tamaño de 1 y los apuntadores tienen un tamaño de 4 hytes. Sin embargo, todas las variables, i yendo los campos de estructura, son asignadas sólo en límites de 4 bytes. De este mo la variable x tiene un tamaño de 12 bytes, la variable p tiene 4 bytes y los desplazamie de i, c y j son 0, 4, 8, respectivamente, como lo son los desplazamientos de l ch i ld y r c h i l d . El compilador asigna tanto x como p en el registro de activaci en la localidad f p - 0 x c (hexadecimal c = 12) y g en la localidad fp -0x10 (hexade 10 = 16).

El código generado para la asignación

Este código carga el valor de x. i en el registro o1 y lo almacena en x. j (advierta de nuevo cómo el desplaramiento de x. j = - 12 + 8 = -4 se calcula estáticamente).

La asignación de apuntador

produce el código objetivo

Id [%fp+-Ox101,%02 la I % ~ ~ + - O X I O I . ~ ~

st %03, [%02+0x41

En este caso el valor de g se carga en los registros 0 2 y 03. Una de estas copias (02) re utiliza entonces como la dirección base para almacenar la otra copia en la localidad d e g - z l c h i l d .

Finalmente, la asignación

Page 455: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Generación de ccidigo en compiladores comerciales: dos casos de estudio

produce el código objetivo

En este c6digo el valor de p es c;irgado en el registro o4 y posteriormente utilizado como la dirección base para cargar el valor de p->rchild. La instrucción final almacena este valor en la localidad de p.

Sentenciaj if y while Mostramos el código generado por el compilador de C de la SparcSta- tion de Sun por las mismas sentencias de control típicas que para el compilador de Borlmd

Y

while ( x q ) y -= x;

con x y y corno variables enteras locales. El compilador de la SparcStation de Sun genera el siguiente código para la sentencia if,

donde tanto x como y se localizan en el registro de activación local en los desplazamientos

Id

Id

CmP

bg

nop

b

nog

L16:

Id

add

st

b

nop

L15:

Id

sub

st

L17 :

Aquí las instrucciones nop siguen cada instnicci6n de ramific:rcióri debido a que I:i Sparc tsrá candlizrtcla (las rmificacioiies son iipl:izadas y la siguiente instrucción siempre se cjecu- ra antes que tenga efecto IU r;irnilicacióii), Observe que la organización ssc~icricial es la opuesta a lo de la figura 8.10: e1 c;~so verdxlero se coloc:i después del c:isct kilso.

Page 456: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 GENEMCI~N DE C~DIGO

El código generado por el compilador Sun para el bucle o ciclo while es

E ~ t e código utiliza un arreglo ~imilar al del compilador de Boriand, sólo que el código de ptueba también se duplica al principio. También, al final de este código se crea una ramifica- ción "que no hace nada" (a la etiqueta L22), la cual se puede eliminar fácilmente mediante un paso de optimización.

Definición de funciones y llamadar Utilizamos la misma definición de función que antes

in t £ ( i n t x, i n t y)

í return x+y+l; 1

y la misma llamada

f (2+3,4)

El c6digo generado por el compilador Sun para'la Iiamada es

mov 0x5, %o0

mov 0X4,%0l

cal1 -f , 2

Page 457: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

TM: una'maquina objetivo simple

-f :

I#PROLOGUE# O

sethi %hi(LF62),%gl

add %gl,%lo(LF62),%gl

save %SR. %gl,%sp

l#PROLOGUE# 1

s t %iO, [%fp+Ox44]

a t %il, [%fp+Ox48]

L64:

. aeg "text"

Id [%fp+Ox441,%00

16 [%fp+Ox481 ,%o1

add %oO,%ol,%oO

add %OO. 0x1, %o0

b LE62

nop

LE62 :

mov %00,%iO

ret

reatore

No analizaremos este código con detalle, pero haremos los comentarios siguientes. En pri- mer lugar, la llamada pasa los argumentos en los registros o 0 y 01, en vez de en la pila. 1.a llamada indica con el número 2 cuántos registros se utilizan para este propósito. La Ilama- da también realiza varias funciones de administración, las cuales no describiremos, excepto para decir que los registros "o" (como o0 y 01) se convierten en los registros "i" (por ejemplo, 1 0 e i l ) después de la llamada (y los registros "i" vuelven a ser los registros "o" al retorno de la 1lamada).l6

El código para la definición de f también comienza con algunas instrucciones de admi- nistración que finalizan la secuencia de llamada (entre los comentarios ! #PROLOGUE#), las cuales tampoco describiremos. El código almacena entonces los valores de parámetro para la pila (de los registros "i", que son los mismos que los registros "o" del elemento que llama). Después que se calcula el valor devuelto, se coloca en el registro i O (donde estará disponible después del retorno como registro 00).

87 TM: UNA M&UINA OBJETIVO SIMPLE En la siguiente sección presentaremos un generador de código para el lenguaje TINY. Para hacer de ésta una tarea significativa, generamos código objetivo directamente para una má- quina muy simple que se pueda simular con facilidad. Llamaremos a esta máquina TM (por Tiny Machine) y en el apéndice C proporcionaremos un listado de programa en C de un simulador TM completo que se puede emplear para ejecutar el código producido por el generador de código TINY. En la sección actual describiremos la arquitectura TM comple- ta y el conjunto de ii~strucciones, además del uso del simulador del aphdice C. Con el fin de

16. Hohlando libremente, la "o" es para "outpul" y la "i" cs para "input". Esta contnutaci6n de registr<~s a tmvis de las llamadas se rea1ir.a mediante un mecanismo (Ic ventana de registro de la aiilui- lectura Sparc.

Page 458: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

facilitar la cotnprensión emplearemos fragmentos de código en C para ayudar a esta dese ción, y las mismas instrucciones TM siempre se darán en formato de código ensamblado vez de hacerlo en códigos binario o hexadecim~l. (En cualquier caso el simulador sólo lee código ensamblado.)

8.11 Arquitectura básica de la máquina TINY TM se compone de una memoria de instrucción sólo de lectura, un& memoria de datos, y un conjunto de ocho registros de propósito general. Todos éstos utilizan direcciones enteras no negativas que comienzan en O. El registro 7 es el contador del programa y es el único re- gistro especial, como se describe a continuación. Las declaraciones en C

#define IADDR-SIZE . . . / * tamaño de la memoria de la instrucción */

#define DADDR-SIZE ... / * tamafio de la memoria de datos * /

#define NO-REGS 8 / * número de registros * / #define PC-REG 7

Instruction iMam[IADDR-SIZE];

int dMem1DADDR-SIZEI ;

int reg [NO-REGSI ;

se utilizarán en las descripciones que siguen. La TM realiza un ciclo convencional de obtención-e.jecución:

do

/ * obtención */ currentInstruction 3 iMemtreg[pcRegNol++l ;

/* ejecuta la instrucción actual * / ...

while ( ! (haltl lerror)) ;

Al eomienm, la máquina TINY establece todos los registros y memoria de datos a O, lue- go carga el valor de la dirección legal más alta (a saber, DADDR-SIZE - 1) en dMem [ O l . (Esro permite que la memoria se agregue fácilmente a la TM, puesto que los programas pueden averiguar durante la ejecución de cuánta memoria se dispone.) La TM comienza :

entonces a ejecutar las instrucciones comenzando en iMem [ O 1 . La máquina se detiene cuando se ejecuta una instrucción HALT. Las condiciones de error posibles incluyen a IMEM-ERR, que se presenta si reg [PCREGI .: o s i reg [PCREGI 2 IADDR-SIZE enei paso de obtención anterior, y las dos condiciones DMEM-ERR y ZERO-DIV, Las cua- les se presentan durante la ejecución de instrucciones como se describió anteriormente.

El conjunto de instrucciones de la TM se proporciona en la figura 8.15, junto con una breve descripción del efecto de cada instrucción. Existen dos formaros de iustniccih hási- cos: sólo de registro, o instrucciones R O y de memoria de registro. o instiucciones RM. Una instrucción s6lo de registro tiene el fornlato

Page 459: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Fiyura 8.15 Conjunto completo de inrtmccioner de la rniquina TlNY

TM: una máquina objetivo simple 455

lnstrucciones RO

Formato: opcode r, S, t

Opcode Efecto

HALT detener ejecucidn (operandos ignorados) IN res [rl t lectura de valor entero desde la entrada estindar

(S y t ignorados) OUT reg [rl -+ la salida estiindar (a y t ignorados) ADD regtrl = reg[sl + reg[tl SUB reg[rl = regrs] - reg[tl MUL regir1 = regis1 * reg[tl DIV res [rl = res [ sl / reg [ t 1 (puede generar ZERO-DIV)

lnstrucciones RM

Formato: opcode r, d f s)

(a = d t reg [ SI ; cualquier referencia a &em [al genera DME-ERR bi a C O o a 2 DADDR-SIZE)

Opcode Efecto

LD reg [rl = dMem[a] (carga rcon valor de la memoria en a) LDA reg Ir] = a (carga dirección a directamente en r) LDC reg [ rl = d (carga constante d directamente en r-s es ignorada) ST &em[a] S reg [r] (almacena valor en r a localidad de memoria a) JLT if (reg[rl<O) regEPCREG1 = a

( salta a instmcción a si r e s negativa, de manera similar para el siguiente) JLE if freg[r]í=O) reg[PC-REG] = a JGE if (regfr] >=O) reg[PC-RRGI S a JGT if (reg[r]zO) reg[PC-REO1 = a JEQ if (reg[rls=O) reg[PC-REGI = a JNE if (reg [rl !=O) reg [PC-RED] = a

donde los operandos r, S, t son registros legales (verificados en tiempo de carga). De este modo, tales instrucciones son tres direcciones, y las tres direcciones deben ser registros. Todas las instrucciones aritméticas se encuentran limitadas a este formato, como lo están las dos instrucciones primitivas de entrada/\alida.

Una instrucción de memoria de registro tiene el formato

opcode r, d (S)

En este código r y s deben ser registros legales (verificados en tiempo de carga), y d e s un entero positivo o negativo que representa un desplazamiento. Esta instrucción es una ins- trucción de dos direcciones, donde la primera dirección es siempre un registro y la segunda direccibn es una dirección de memoria a dada por a = d + reg [r] , donde a debe ser una dirección legal (0 5 a < DADDR,-SIZE). Si a esta fuera del intervalo legal, entonces se genera DMEM-ERR durante la ejecución.

Page 460: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 GENEMCI~N DE C~DIGO

.,:, : Las instrucciones RM incluyen tres instrucciones de carga diferentes que corresponde&,::' : I

a los tres modos de direccionamiento "carga constante" (LDC), '<dirección de carga" (LDA$!: I y "memoria de carga" (LD). Además, existe una instrucción de almacenamiento y s trucciones de salto condicional.

Tanto en las instrucciones RO como en las RM, los tres operandos deben estar pre tes, aun cuando algunos de ellos pueden ser ignorados. Esto se debe a la naturaleza si del cargador, el cual sólo distingue las dos clases de instrucciones (RO y RM) y no pe te diferentes fonnatos de instrucción dentro de cada c ~ a s e . ' ~

La figura 8.15 y el anáiisis de la TM hasta este punto representan la arquitectura pleta TM. En particular, no hay pila de hardware u otras facilidades de ninguna clase. registro, excepto el pc, es especial en modo alguno (no hay sp o fp). Un compilador TM debe, por lo tanto, mantener alguna organización del ambiente de ejecución de enteramente manual. Aunque esto puede ser algo poco realista, tiene la ventaja de qu las operaciones se deben generar explicitamente a medida que sean necesarias.

Debido a que el conjunto & instrucciones es mínimo, algunos comentarios est puestos acerca de cómo se puede utilizar para conseguir casi todas las operaciones es de un lenguaje de programación (en realidad ésta es una máquina objetivo adecuado, es que hasta cómodo incluso para lenguajes muy sofisticados).

1. El registro objetivo en las operaciones aritméticas, IN, y de carga viene en prim gar, y el (los) registro(s) fuente vienen en segundo, de manera similar a la arq tura 80x86 y diferente a la de la SparcStation de Sun. No hay restricción so uso de los registros para fuentes y objetivos; en particular, los registros fuente y ob- jetivo pueden ser los mismos.

2. Todas las operaciones aritméticas están restringidas a registros. Ninguna operaci (excepto las operaciones de carga y almacenamiento) actúa directamente en la moria. En esto, la TM se parece a las máquinas RISC, tales como la SparcStatio Sun. Por otra parte, la TM tiene sólo 8 registros, mientras que la mayoría de los pro- cesadores RISC tienen por lo menos 32."

3. No hay operaciones de punto flotante o registros de punto flotante. Aunque no se muy difícil agregar un coprocesador a la TM con registros y operaciones de pun flotante, la traducción de los valores de punto flotante hacia y desde los registros r gulares y la memoria requerirían de alguna precaución. Remitiremos al lector a I ejercicios.

4. No existen modos de direccionamiento especificables en los operandos como algún código ensamblado (tal como LD #1 para modo inmediato, o LD @a p indirecto). En vez de eso existen diferentes instrucciones para cada modo: LD es in- directo, LDA es directo y LDC es inmediato. En realidad, la TM tiene muy poc opciones de direccionamiento.

5. No existe restricción para el uso del pc en ninguna de las instmcciones. Efecti mente, puesto que no hay instrucción de salto incondicional, ésta debe ser simula mediante el uso del pc como el registro objetivo en una instrucción LDA:

Esta instrucción tiene el efecto de saltar a la localidad a = d + reg [a].

17. También facilita el trabajo de un generador de código, ya que sólo serSn necesarias dos mti- nas por separado para las dos clases de instrucciones.

18. Aunque sería fácil incrementar el número de registros TM, aquí no lo haremos, ya que no se necesitan en la generación de código básico. Véanse los ejercicios al final del capítulo.

Page 461: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

TH: una máquina objetivo simple - 457

6. Tampoco hay instrucción de salto indirecto, pero también se puede imitar, si es necesario, mediante el uso de una instrucción LD. Por ejemplo,

salta a la instmcción cuya dirección está almacenada en la memoria en la localidad a la que apunta el registro 1.

7. Las instrucciones de salto condicional (JLT, etc.) pueden hacerse relativas a la posi- ción actual en el programa mediante el uso del pc como el segundo registro. Por ejemplo,

provoca que la TM salte cinco instrucciones hacia delante en el código si el registro O es O. Un salto incondicional también puede hacerse relativo al pc utilizando dos veces el pc en una instrucción LDA. De este modo,

LDA 7 , -4(7)

realiza un salto incondicionat de tres instrucciones hacia atrás. 8. No existe llamada de procedimiento o instrucción JSvB. En su lugar debemos escribir

que tiene el efecto de saltar al procedimiento cuya dirección de entrada es dMem- [d+reg [a l 1. Naturalmente, debemos recordar grabar la dirección de retorno eje- cutando primero algo como

que coloca e1 valor actual del pc más uno en reg [ 0 1 (que es a donde queremos regresar, suponiendo que la instrucción siguiente es el salto real al procedimiento).

8.7.2 El simulador de TM El simulador de máquina acepta archivos de texto que contienen instrucciones de TM de la manera en que se describieron anteriormente, con las convenciones siguientes:

l. Una Iínea completamente en blanco se ignora. 2. Una línea que comienza con un asterisco se considera un comentario y se ignora. 3. Cualquier otra línea debe contener una localidad de instrucción entera seguida por un

signo de dos puntos seguido por una instrucción legal. Cualquier texto que se presen- te después de la instntcci6n se considera un comentario y se ignora.

El simulador de TM no contiene otras características, en particular no hay etiquetas sim- bólicas ni facilidades de macro. En la figura 8.16 se proporciona un programa de muestra TM escrito manualmente que corresponde al programa TINY de la figura 8.1.

Estrictamente hablando, la instmcción HALT al final del código de la figura 8.16 no es necesaria, puesto que el ~imulador de TM establece todas las localidades de instrucción a HALT antes de cargar el programa. Sin embargo, es útil conservarla como un recordatorio, y como un objetivo para saltos que deseen salir del programa.

Page 462: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

458 CAP. 8 1 GENEMION DE ~ 6 ~ 1 6 0

Figura 8.16 Un programa TH que muestra las ionventiones de formato

* Este programa introduce un entero, calcula * su factorial si es positivo, * e imprime el resultado

o: IN 0,0,0

1: JLE 0,6(7)

2: LM: 1,l.O

3: LDC 2,1,0

4: MWL 1,l.O

5: SUB 0,0,2

6: J'NE 0,-3(7)

7: OUT 1,0,0

8: HALT 0,0,0

* fin del programa

rO = read

if O < rO then

rl = 1

r2 = 1

* repite rl = rl * rO rO = rO - r2

until rO == O

write rl

halt

'Tampoco es necesario que aparezcan localidades en secuencia ascendente como hicimos en la figura 8.16. Cada línea de entrada es efectivamente una directiva del tipo "almacena esta instrucción en esta localidad": si un programa TM fuera perforado en tarjetas, sería aceptable tirarlas y que se desordenaran en el piso antes de leerlas en la TM. Aunque esta propiedad del simulador de TM podría causar alguna conFusión al lector de un programa, facilita ajustar saltos cuando no hay etiquetas simbólicas, ya que el código se puede ajustar sin apoyarse en el archivo del código. Por ejemplo, es probable que un generador de códi- go genere el código de la figura 8.16 en la secuencia que se muestra a continuación:

O: IN 0,0,0

2: LDC 1.1.0

3: LM: 2,1,0

4: MUL 1,1,0

5: SUB 0,0,2

6: J'NE 0,-3(7)

7: OUT 1,O.O

1: JLE 0,6(7)

8: HALTO,0,0

Esto se debe a que el salto hacia adelante en la instrucción 1 no se puede generar hasta que se conozca la ubicación después del cuerpo de la sentencia if.

Si el programa de la figura 8.16 está en el archivo fact . t m , entonces este archivo se puede cargar y ejecutar como en la siguiente sesión de muestra (el simulador de TM automáticamente presupone una extensión de archivo . t m si no se proporciona alguna otra):

tm fact

TM simulation (enter h for help) . . . Enter command: g

Enter value for IN instruction: 7

OUT instruction prints: 5040

Page 463: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Un generador de código pan el lenguaje TlNY

HALT: 0.0,O

Halted Enter command: q Simulation done.

El coinando g representa "ir a", lo que significa que el progrania se ejecuta comenzando en el contenido actual del pc (que es O justo después de 13 carga), hasta que se detecta una ins- trucción HALT. La lista completa de comandos del sirnulador se puede obtener mediante el uso del comando h, el cual imprinie la lista que se muestra a continuación:

Los comandos son:* s(teg <n> Execute n (default 1) TM instructions

~ ( 0 Execute TM instructions until HALT r (egs Print the contents of the registers i(Mem <b a>> Print n iMem locations starting at b d(Mem <b <n>> Print n dXem locations starting at b t ( race Toggle fnstruction trace ~(rint Toggle grint of total instructions executed

('go' only) c (lear Reset simulator £01 new execution o£ grogram

h(elg Cause this list o£ counnands to be grinted q(uit Terminate the simulation

El paréntesis izquierdo en cada cornando indica el mnemónico a partir del cual se deriva la letra del comando (también es aceptable utilizar más de una letra, pero el simulador sído examinará la primera). Los picoparéntesis c > señalun los parámetros opcionales.

8.8 UN GENERADOR DE CODIGO PARA EL LENGUAJE TINY Ahora deseamos describir un generador de código para el lenguaje TINY. Suponemos que el lector está familiarizado c m los pasos previos del cornpilador 'TINY, en particular con la estnictura de árbol sintáctico generada por el analizador sintáctico como se describió en el capítulo 3: la construcción de la tabla tle símbolos como fue descrita en el capítulo 6 y el ambiente de ejecución descrito en el capítulo 7.

En esta sección describiremos primero la interfaz del generador de código TINY para la TM. junto con las funciones utiiitarias necesarias para la generación del código. Poste- riormente describiremos los pasos del generador de código apropiado. En tercer lugar des- cribirenlos el uso del compilador TINY en combinación con el sirnulatlor TM. Finalmente, analizaremos el archivo del código objetivo para el programa TINY de rnuesrra que se ha estado utilizando en este libro.

8.81 l a interfaz TM del generador de cbdigo TINY Encapsulamos algo de la información que el generador de código necesita conocer acerca de la TM en los archivos code. h y code. c, los cuales se listan en el apéndice B, líneas 1600-1685 y 1700- 1796, respectivamente. También ponemos el código que emite funcio- nes en estos archivos. Naturalmente, el generador de código todavía necesit~rá conocer los nombres de instrucción de TM. pero estos archivos separan los dctrilles del formateo de

:$ rr. ,i 4 . I I L C I ~ I I . por línea: Ejecuta n (por oniisión 1) instrucciones TM. Ejecuta las insirucciones TM hasta que encuentra a tl,\I-T. Inrpriiiie el conlenido de lo5 registros. Imprime n I~xxnli&aies de i%lciii iiiizianilo eii b. inrprimc n localid;idcs de ilhfcni iniciando en h. Fija 13 instntcciiln de ts:iz:ido. Activa I:i iiiipresih del tor:rl de hs iristnrcci~ries c,jecutn<ias (sillo coi1 'gil'). Kcinicia C I siniulador p:ls;r i ~ n a iioc\a cjwuci6n i k l progr;irn:i. I'rc.wiia esta liski de cori~an~l(?s. Tcrn~ina I;r siirinlncirin.

Page 464: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 GENERACI~N DE ( 6 ~ 1 ~ 0

instrucción, la ubicación del archivo del código objetivo y el uso de registros particulares el ambiente de ejecución. De manera importante, el archivo code. c encapsula complet mente la secuencia de instrucciones en localidades iMem particulares, así que el generado de c6digo no tiene que mantenerse al tanto de este detalle. Si se fuera a mejorar el carg dor de TM, para permitir etiquetas simbólicas y para eliminar la necesidad de números d localidad por ejemplo, entonces sería fácil incorporar la generación de etiquetas y cambios de formato en el archivo code . c.

Revismmos aquí algunas de las características de las definiciones de función y cons tantes en el archivo code. h. En primer lugar consideraremos las declaraciones de lo valores de registro (líneas 161 2, 5617, 1623, 1626 y 1629). Evidentemente, el pc debe se conocido por el generador de código y las utilidades de emisión de código. Más allá de est el ambiente de ejecución para el lenguaje TINY, como se describió en el capítulo ante asigna localidades en la parte superior de la memoria de datos para elementos tempor (de una manera tipo pila) y las localidades en la parte inferior de la memoria de d las variables. Como no hay registros de activación (y, por 10 tanto, tampoco fp) en (debido a que no hay ámbitos o llamadas de procedimiento), las localidades para vari y elementos temporales se pueden visuaiizar como direcciones absolutas. Sin embar operación LD de la máquina TM no permite el modo de direccionamiento absoluto, si debe tener un valor base de registro en su calculo de la dirección para una carga de me ria. Así, asignamos dos registros, a los que denominamos el mp (memory pointer) y gp ( bal pointer), para apuntar a la parte superior o tope de la memoria y a la parte infe ésta, respectivamente. El mp será utilizado para acceso de la memoria a los elementos tem- porales y siempre contendrá la localidad más alta de memoria legal, mientras que el gp se- rá utilizado para todos los accesos de memoria variable nombrada y siempre contendrá O, de manera que las direcciones absolutas calculadas mediante la tabla de .símbolos se pueden emplear como desplazamientos relativos al gp. Por ejemplo, si un programa tiene dos varia- bles x y y, y se tienen dos valores tenlporales actualmente almacenados en memoria, enton- ces &en tendría un aspecto como el siguiente:

En esta ilustración t 1 tiene dirección O (mp), t 2 tiene dirección - I f mp), x tiene dirección O (gp) y la y tiene dirección 1 (gp). En esta implementación gp es el registro 5 y mp es el registro 6.

Los otros dos registros que serán utilizados por el generador de código son los registros O y 1, los cuales son llamados "acumuladores" y a los que se les asignan los nombres ac y acl. Éstos ~e emplearán como registros abiertos. En particular, los resultados de lo5 c6lculos siempre se dejarán en ac. Advierta que los registros 2, 3 y 4 no estUn non~brados (;y nunca se rh utili~ados!).

Page 465: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]
Page 466: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 G E N E R A C ~ ~ N DE CODIGO

m& alta (que el simulador TM ha colocado en 'la localidad cero al inicio), mientras que segunda borra la localidad O. (El gp no necesita ser establecido a 0, puesto que todos 1 registros se establecen a O en el inicio.)

La función cGen (líneas 2070-2084) es responsable de l1ev:ir a cabo el recyrrid árbol sintáctico de TINY que genera código en postorden modificado, de la manera des tu en secciones anteriores. Recuerde que un árbol sintáctico de TINY tiene una formad por las declaraciones

typedef enum {StmtK,ExpK) NodeKind;

typedef enum {IfK,RepeatK,AssignK,ReadK,WriteK) StmtKind;

typedef enum (OpK,ConstK,IdK) ExpKind;

#define MAXCHXLDREN 3

tygedef struct treeNode

f struct treeNode * child[MAXCHILDRENI; struct treeNode * sibling; int linenoi

NodeKind nodekind;

union ( StmtKind stmt; ExpKind exp;) Rind;

union Tokenwe op;

int val;

char * name; ) attr;

ExpType type;

1 TreeNode;

Existen dos clases de nodos de árbol: los nodos de sentencia y los nodos de expresión. Si un nodo es un nodo de sentencia, entonces representa una de las cinco clases diferentes de sen- tencias de TINY (la condicional if, la de ciclo repeat, la de asignación assign, la de lectura read o la de escritura wnte), y si un nodo es un nodo de expresión, entonces representa una de las tres clases de expresiones (un identtficador, una constante entera o un operador). La función cGen prueba sólo si un nodo es un nodo de sentencia o de expresión (o nulo), Ila- mando a la función apropiada genStmt o genExp, y llamándose más adelante a sí misma de manera recursiva en los nodos hermanos (de manera que las Ilstab hermanas tendrán código generado en orden de i~quierda a derecha).

La función genstmt (Iíneas 1924-1994) contiene una gran sentencia switch que distingue entre las cinco clases de sentencias, generando el código apropiado y llamadas recuriivas a cGen en cada caso, y de manera semejante para la función genExp (líneas 1997-2065). En todos los casos se supone que e l código para una rubexpresión deja un valor en el ac, donde se puede tener acceso al mismo mediante un código posterior. En los casos en que es necesario el acceso a una variable (sentencias de asignación y lec- tura y expresiones de identificador), 5e tiene acceso a la tabla de símbolos mediante la operación

loc = lookug(tree->attr.name);

El valor de loc es la dirección de la variable en cuestión y se utiliza como el desplamnien- to con el registro gp para cargar o almacenar su valor.

El otro caso en el cual son necesarios los ziccesos a rnenioria es cti el c;ilculo del rcsul- r;itlo de una expresión de operador, ilonde cl opcrando a La izquierda dche ser al~iiacenailo

Page 467: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Un generador de código para el lenguaje TlNY 463

en una localidad temporal antes de calcular el operando de la derecha. De este modo, el código para una expresión de operador incluye la siguiente secuencia de generación de có- digo antes que se pueda aplicar el operador (líneas 2021 -2027):

cGen(p1); / * pl = hijo izquierdo * / emitRM("STW,ac, tmpoffset--,mp, I'op: push left") ;

cGen(p2); / * p2 = hijo derecho */ emitRM("LD*,acl,++tmpOffset,mp,"og: load left");

Aquí t m ~ O f f set es una variable estática que es inicializada a O y empleada como el desplazamiento de la siguiente localidad temporal disponible desde la parte superior o tope de la ~nemoria (a la que se apunta mediante el registro mp). Advierta cómo t m p O f fset se decreinenta después de cada ;ilmacenamieiito y se incrementa después de cada carga. De esta forma, t m p o f fset se puede ver como un apuntador hacia el tope de la "pila tem- poral", y las llamadas a la función e m i t m corresponden a una inserción y extracción de esta pila. Esto protege cada elemento temporal mientras se mantiene en La tnemoria. Des- piiis de que el código generado por las acciones anteriores es ejecutado, el operando del lado izquierdo estará en el registro 1 (acl) y el operando del lado derecho en el registro O (ac). Puede entonces generarse la operación apropiada RO, en el caso de las operaciones aritméticas.

El caso de los operadores de comparaci6n es un poco diferente. La sernintica del len- guaje TlNY (como se implenientó mediante el analizador semántica; véase el capítulo m - terior) permite a los operadores de comparación sólo en las expresiones de prueba de las sentencias if y las sentencias while. No hay variables booleanas u otros valores además.de en tales pruebas, y los operadores de coinparación se podrían abortlar con ekts en la gene- ración de código de esas sentencias. No obstante, aquí utilizxemos un enfoque mis general, el cual es más anlpliarnente aplicable a lenguajes con operaciones lógicas y/o valores booleanos, e impletnentaremos el resultado de una prueba como un 0 (para hlso) o 1 (para verdadero), corno en C. Esto requiere que la constante O o 1 sea cargada de manera explícita en el ac, lo que se consigue haciendo saltos para ejecutar la carga correcta. Por ejemplo, en el caso del operador "menor que" se genera el código siguiente, una vez que se ha genera- do el código para calcular el operand~ del lado izquierdo en el registro 1 y el operando del lado derecho en el registro O:

SUB 0,1,0

JLT 0,2(7)

LDC O,O(O)

LOA 7,1(7)

LDC 0,1(0)

La primera instrucción resta el operando derecho del izquierdo, y deja al resultado en el registro 0. Si c es verdadero, este resultado será negativo, y la instrucción JLT 0 , 2 ( 7 ) provocará u11 salto sobre dos instritcciones hasta 13 Últinm instrucción, la cual carga el valor 1 en el ac. Si < es falso, eritoiices la tercera y la cuarta instrrrccio~ies se +mitan, lo que carga O en el :ic y entonces salta sobre la Últinia instrucción (recuerde de la des- cripci61i de la 'TM que cuando LDA utiliza el pc como arnbos registros provoca un sallo incortdicional),

Finalizirnos la ciescripcih del generador de ctjcligo TINY con u n aodisis de !a senteit- cia ii' ílíncas 1930- i95.4). Los c;ihos restantes sc dtj:ir> :iI Icctor.

Page 468: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 GENERAC~N DE E ~ D I G O

La primera acción del generador de código para una sentencia if es generar código la expresión de prueba. El código para la prueba, como acaba de describirse, deja el O ac en el caso falso y 1 en el caso verdadero. El generador de código debe generar a co nuación una JEQ para el código para la parte else de la sentencia if. Sin embargo, la ub ción de este código aún no se conoce, puesto que el código para la parte "then" todavía be ser generado. Por lo tanto, el generador de código utiliza la utilidad e m i t S k i p p saltar la sentencia siguiente y grabar su ubicación para ajuste posterior:

savedLocl = emitSkip(1) ;

La generación de cúdigo continúa con la generación del c6digo para la parte then de la s tencia if. Después de ello, se debe genera un salto incondicional sobre el código de la te else, pero nucvnmente esa ubicación no se conoce, de modo que la localidad para este sa to también se debe omitir y su ubicación se debe grabar:

Ahora, sin embargo, el siguiente paso es generar código para la parte else, de modo que ubicación del código actual es el objetivo conecto para el salto en el caso falso, el cual ra se debe ajustar a la ubicación e a v e d l o c l , que es lo que realiza el código siguient

currenttoc = emitSkip(0) ; emitBackupfsavedLoc1) ;

emitw-~ba(~~~~~",ac,currentLoc, "if: jmp to else") ;

emitRestore í ) ;

Advierta cómo la llamada emitskip ( O ) se utiliza para obtener la ubicación de la ins- trucción actual, y cómo el procedimiento emitRtú-Abs se emplea para convertir el salto esta localidad absoluta en un salto relativo al pc, el cual es requerido por la instrucci JEQ. Después de esto, finalmente se puede generar el código para la parte eise, y en ces se ajusta un salto absoluto (LDA) a eavedLoc2 mediante una secuencia de código semejante.

8.83 Generacidn y uso de archivos de código TM con el compilador de TlNY

El generador de código TINY es afinado para trabajar limpiamente con el simulador de TM. Cuando todas las marcas NO-PARSE, NO-ANALYZE y NO-CODE se establecen como falsas en el programa principal, el compilador crea un archivo de código con el sufijo o extensión . t m (suponiendo que no hay errores en el código fuente) y escribe las instiuc- ciones de TM en este archivo en el formato requerido de simulador de TM. Por ejemplo, para compilar y ejecutar el programa eample. t n y , sólo se necesita emitir los comandos siguientes:

einy sample

<listado producido en la salida eatándarz

tm sample

<ejecución del simulador tm>

Page 469: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Un generador de código para el lenguaje TlWY 465

Para propósitos de rastreo existe una marca TraceCode declarada en globals .h y definida en maf n. c. Si se establece como TRUE, la información de rastreo se genera me- diante el generador de código, la cual aparece como comentarios en el archivo de código, indicando dónde se generó cada instrucción o secuencia de instmcciones en el generador de código, y por qué razón.

8.8.4 Un archivo de código TM de muestra generado por el compilador de TlNY

Para mostrar con mayor detalle cómo funciona el generador de código, presentamos en la figura 8.17 el archivo de código realizado por el generador de código TINY para el progra- ma de muestra de la figura 8.1, con TraceCode a TRUE, de manera que también sean generados comentarios de código. Este archivo de código tiene 42 instrucciones, inclu- yendo las dos instrucciones del preludio estándar. Al comparar esto con las nueve instmc- ciones del programa escrito a mano de la figura 8.16, podemos ver desde luego varias ineficiencias. En particular, el programa de la figura 8.16 utiliza los registros de manera muy eficiente: no se utiliza en absoluto ninguna otra memoria más que la de los registros. Por otra parte, el código de la figura 8.17 (páginas 466-467). nunca emplea más de dos re- gistros y realiza muchos almacenamientos y cargas que son innecesarios. Particularmente tofpe es la manera en que trata los valores de las variables, los que son cargados sólo para ser almacenados otra vez como elementos temporales, como ocurre en el código

16: LD 0 , 1 ( 5 ) load i d v a l u e

17: ST 0 , 0 ( 6 ) op: push l e f t 18: LD 0 , 0 ( 5 ) load i d value 19: LD 1 , 0 ( 6 ) op: l o a d l e f t

el cual se puede reemplazar mediante las dos instrucciones

LD l , l ( 5 ) load i d value LD 0 , 0 ( 5 ) load i d v a l u e

que tienen exactamente el mismo efecto. Otras ineficiencias sustanciales se provocan mediante el código generado para las prue-

bas y saltos. Un ejemplo particularmente ridículo es ta instrucción

que es un NOP elaborado (una instrucción de "no operación"). No obstante, el código de la figura 8.17 tiene una propiedad importante: es correcto. En

la prisa por mejorar la eficiencia del código generado, los escritores de compiladores en ocasiones olvidan la necesidad de esta propiedad y permiten que se genere código que, aunque eficiente, no siempre se ejecuta de manera correcta. Tal comportamiento, si no se encuentra bien documentado y es predecible (y a veces incluso así), puede conducir al desastre.

Aunque estudiar todas las maneras en que se puede mejorar el código producido por un compilador rebasa el alcance de este texto, en las dos secciones finales de este capítulo examinaremos las áreas principales en las que se pueden hacer tales mejoras, además de las tbcnicas que se pueden utilizar para implementarlas, y describiremos brevemente cómo \e pueden aplicar algunas de ellas al generador de código TINY con el fin de mejorar el có- digo que genera.

Page 470: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Figura 8.17 * Compilación TINY para código TM Salida de código TH para * Archivo: sample . tm el programa de muestra * Preludio estándar: TINY de la figura 8.1 O: LD 6,0(0) load maxaddress from location O

(págma 401) 1: ST 0,0(0) clear location O

* Fin del preludio estándar. 2: IN 0,o.o

3: ST 0,0(5)

* -> if

* -> op

* -> Const

4: LDC O,O(O)

* <- Const

5: ST 0,0(6)

* -> Id

6: LD 0,0(5)

* <- Id

7: LD 1,0(6)

8: SUB 0,1,0

9: JLT 0,2(7)

10: LDC 0,0(0)

11: LDA 7,1(7)

12: LDC 0,1(0)

* <- op

read integer value

read: store value

load const

op: gush left

load id value

op: load left

OP <

br if true

false case

unconditional jmp

true case

* if: el salto al else gertenece aquí * -> assign

* -> Const

14 : LDC 0,1(0) load const

* <- Const

15: ST 0,1(5) assign: store value

* <- assign

* -> regeat

* regeat: el salto después del cuerpo regresa aquí * -> assign

* - > op

* -> Id

16: LD 0,1(5) load id value

* <- Id

17: ST 0,0(6) op: gush left

* - > Id

18: LD 0,0(5) load id value

* <- Id

19: LD 1,0(6) . op: load left

20: PWL 0,1,0 OP *

Page 471: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Un generador de codigo para el lenguaje TiNY

* <- op

21: ST 0,1(5)

* < - assign

* -> assign

-> op

* -> Id

22: LD 0,0(5)

* <- Id

23: ST 0,0(6)

* -> Const

24: LDC 0,1(0)

* <- Const

25: LD 1.0'6)

26: SUB 0,1,0

* < - Og

27: ST 0,0(5)

* < - assign

* -> op

* -> Id

28: LDrOrO(5)

* < - Id

29: ST 0,0(6)

* -> Const

30: LDC 0.0(0)

* < - Const

31: LD 1,0(6)

32: SUB 0.1.0

33: JEQ 0,2(7)

34: LDC O,O(O)

35: LDA 7,1(7)

36: LDC 0,1(0)

* < - ap

37: JEQ 0,-22(7)

* < - repeat

* -> Id

38: LD 0,1(5)

* <- Id

39: OUT 0,O.O

* if: el salto al final pertenece aquí 13: JEQ 0,27(7) if: jmp to else

40: LDA 7,0(7) jmp t0 end

* <- if

* Fin de la ejecución. 41: HALT 0,0,0

assign: store value

load id value

og: gush left

load const

og: load left

OR -

assign: store value

load id value

op: push left

load const

op: load left

og ==

br if true

false case

unconditional jmp

true case

regeat: jmp back to body

load id value

write ac

Page 472: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

468 CAP. 8 1 GENERACI~N DE C~DIGO

8.9 UNA VISION GENERAL DE LAS TECNICAS DE OPTIMIZACIÓN DE CÓDIGO

Desde los primeros compiladores de la década de 1950, la calidad del código genera un compilador ha sido de gran importancia. Esta calidad puede ser mensurada tanto velocidad como por el tamaño del código objetivo, aunque generalmente se ha dado m portancia a la velocidad. Los compiladores modernos abordan el problema de la calidad digo al efectuar, en varios puntos durante el proceso de compilación, una serie de paso incluyen el obtener información acerca del código fuente y el uso posterior de ésta p lizar transformaciones de mejoramiento de c6digo sobre las estnicturas de datos que sentan el código. Un gran número y variedad de técnicas para mejorar la calidad del c han desarrollado a través de los años, y se han llegado a conocer como técnicas de op ción de código. Sin embargo, esta terminología es engañosa, puesto que sólo en situacio muy especiales cualquiera de estas técnicas puede producir código optimizado en el matemático. No obstante, el nombre es tan común que continuará utilizándose.

Existen tantas y diversas técnicas de optimización de código que aquí sólo pode una pequeña visión fugaz de las más importantes y las más ampliamente utilizadas, e i so para éstas no proporcionaremos los detalles de cómo implementarlas. Al final del tulo se dan referencias de fuentes de información adicional al respecto. Sin embargo, es portante enfatizar que un escritor de compiladores no puede esperar incluir cada técnic optimización simple que se haya desarrollado, pero debe considerar, para el lenguaj cuestión, cuáles técnicas tienen más probabilidad de producir una mejora de código im tante con el menor incremento de la complejidad del compilador. Se han escrito mu artículos describiendo técnicas de optimizaciónque requieren de implementaciones m complejas, aunque las mejoras, en promedio, que producen en el código objetivo son re tivamente pequeñas (digamos, una disminución de un pequeño porcentaje en el tiempo ejecución). Sin embargo, la experiencia generalmente ha demostrado que unas cuantas t nicas básicas, aun cuando se apliquen en apariencia de manera muy simple, pueden cond cir a mejoras más significativas, en ocasiones recortando el tiempo de ejecución a la mit o incluso a más de la mitad.

Al juzgar si la implementación de una técnica de optimización en particular es demasia compleja respecto a sus resultados en la mejora real del código, es importante determinar n sólo la complejidad de la irnplementación en términos de las estructuras de datos y có de compilador extra, sino también en el efecto que el paso de optimización pueda tener so la velocidad de ejecución del compilador mismo. Todas las técnicas de análisis sintáctico análisis que hemos estudiado han sido dirigidas a un tiempo de ejecución lineal para el c pilador; es decir, la velocidad de compilación es directamente proporcional al tamaño programa que se está compilando. Muchas tecnicas de optimización pueden increm el tiempo de compilación a una función cuadrática o cúbica del tamaño del programa nera que la compilación de un programa de proporciones considerables puede tomar (O en el peor de los casos, horas) o mucho tiempo con optimización completa. Esto puede vocar que los usuarios eviten ei uso de las optimizaciones (o eviten et compilador en conjun- to), y que se desperdicie gran parte del tiempo que tomó implementar las optimizaciones.

En las secciones siguientes describiremos primero las fuentes principales de optimiza- ción y posteriormente las diversas clasificaciones de optimizaciones, para después inspec- cionar de manera breve algunas de las técnicas más importantes junto con las principales estructuras de datos que se emplearon para implementarlas. Todo el tiempo proporcionaremos eiemplos simples de optimizaciones para ilustrar la exposición. En la sección siguiente da- " . " remos ejemplos más detallados de cómo se pueden aplicar algunas de las técnicas comenta- das al generador de código TINY de la sección anterior, con sugerencias para métodos de implementación.

Page 473: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Una visión general de lar técnicas de optimización de cddiga

8.9.1 Fuentes principales de optimizaciones de código En el texto que sigue describiremos algunas de las áreas en las cuales un generador de código puede fallar en producir código correcto, aproximadamente en orden decreciente de "liquidación", es decir, cuánto mejoramiento de código se puede obtener en cada área.

Asignación de regkro El buen uso de los registros es la característica más importante del có- digo eficiente. A lo largo de la historia el número de registros disponibles había estado se- veramente limitado: por lo regular a 8 o 16 registros, incluyendo los registros de propósito especial como el pc, sp y fp. Esto dificultaba la buena asignación de registros debido a que la competencia por el espacio de registros entre las variables y los elementos temporales era intensa. Esta situación persiste en algunos procesadores, particularmente en los microproce- sadores. Un método para resolver este problema ha sido incrementar el número y velocidad de las operaciones que se pueden realizar directamente en la memo&, de mancra que un compilador, una vez que ha agotado el espacio de registros, puede evitar el gasto de tener que reclamar registros almacenando valores de registro en localidades temporales y cargar nuevos valores (denominadas operaciones de desbordamiento de registro). Otro método (el denominado método RISC) consiste en disminuir el número de operaciones que se pueden realizar directamente en la memoria (a menudo a O), pero al mismo tiempo incrementar el número de registros disponibles a 32, ó4 o 128. En tales arquitecturas la asignación apropia- da de registros se vuelve aún más crítica, puesto que se podrían mantener todas, o casi todas, las variables simples de un programa entero en registros. La penalización para el fracaso al asignar registros apropiadamente en tales arquitecturas se incrementa por la necesidad de cargar y almacenar valores en forma constante, de manera que se les puedan aplicar las operaciones. Al mismo tiempo, la labor de asignar registros se vuelve m& fácil, ya que se dispone de muchos. De este modo, la buena asignación de registros debe ser la meta de cual- quier esfuerzo serio por mejorar la calidad del código.

Operaciones innecesaflas La segunda fuente principal de mejoramiento de código es el evitar la generación de código para las operaciones que sean redundantes o innecesarias. Esta clase de optimización puede variar desde una búsqueda muy simple de código localiza- do hasta el análisis complejo de las propiedades semánticas del programa completo. El número de posibilidades para la identificación de tales operaciones es grande, y existe un gran número correspondiente de técnicas empleadas para hacerlo. Un ejemplo típico de una oportunidad de optimización de esta clase es una expresión que aparece repetidamente en el código, y cuyo valor permanece igual. La evaluación repetida se puede eliminar guardando el primer valor para su uso posterior (denominada eliminación de subexpre- sión común)." Un segundo ejemplo es evitar almacenar el valor de una variable o ele- mento temporal que no sea utilizado posteriormente (esto va de la mano con la anterior optimización).

Una clase completa de oportunidades de optimización involucra la identificación del có- digo inalcanzable o código muerto. Un ejemplo típico de código inalcanzable es el uso de una marca o "bandera" constante para activar y desactivar la información de depuración:

19. Mientras que un buen programador puede evitar, hasta cierto punto, expresiones comunes en el código fuente, el lector no debería suponer que esta optimización existe sólo para ayudar a los malos programadores. Muchas subexpresiones comunesresultan de cálculos de dirección que son generados por el mismo compilador y no pueden ser eliminadas al escrihir un mejor código fuente.

Page 474: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 / G E N E M C I ~ N DE CODIGO

#define DBBUG O

Si DEBUG se establece a O (como en este código), entonces el c6digo entre llaves dentro de la sentencia if es inalcanzable, y el código objetivo no necesita generarse, ya sea para aquél o para la sentencit if encerrada. Otro ejemplo de código inalcanzable es un proc que nunca es llamado (o que sólo es llamado desde un código que él mismo hle). La eliminación del código inalcanzable por lo regular no afecta de manera im la velocidad de ejecución, pero puede reducir de manera sustancial el tamaño del có jetivo, y vale la pena, en particular si sólo se utiliza un pequeño esfuerzo extra en para identificar los casos más evidentes.

En ocasiones, al identificar operaciones innecesarias es más fácil continuar c neración de código y Inego probar la redundancia del código objetivo. Un caso en veces es más difícil probar la redundancia por adelantado se encuentra en la genera saltos pan representar sentencias de control estructuradas. Tal código puede incluir la sentencia más próxima o saltos hacia saltos. Un paso de optimización de saltos eliminar estos saltos innecesarios.

Operacione~ cortorar Un generador de código no sólo debería buscar operaciones innec rias, ~ i n o que debería tcimar ventaja de las oportunidades para reducir el costo de operaci nes que son necesarias, pero que se pueden implementar de maneras más económicas lo que puede indicar el código fuente o una implementación simple. Un ejemplo típic esto es el reemplazo de Las operaciones aritméticas por operaciones más económicas. ejemplo, la multiplicación por 2 puede implementarse como una operación de desplaz miento, y una potencia entera pequeña, tal como x3, puede implementar~e como una multt- pticación, tal como x * x * x. Esta optimización se conoce como reducción de potencia. Est puede extenderse de varias formas, como el reemplazo de multiplicaciones que involucre pequeRas constantes enteras mediante desplazamientos y sumas (por ejemplo, reemplazan- do 5*x por 2*2*x C x: dos desplazamientos y una suma).

Una optimización relacionada es emplear información acerca de constantes para elimi- nar tantas operaciones como sea posible o para precalcular tantas operaciones como sea po- 3ible. Por ejemplo, la suma de dos constantes, como 2 + 3, se puede calcular mediante el compilador y reemplazarse por el valor constante 5 (esto se conoce como incorporación d constante). En ocasiones vale la pena intentar determinar si una variable puede tener tam bién un valor constante para una parte o la totalidad de un programa, y tales transformacio- nes entonces se pueden aplicar también a expresiones que involucren a esa variable (esto se denomina propagación de constante).

Una operación que en ocasiones puede ser relativamente costosa es la llamada de proA cedimiento, donde se deben realizar muchas operaciones de secuencia de llamada. Los procesadores modernos han reducido este costo de manera sustancial, ofreciendo apoyo de hardware para secuencias de Hamada estándar, pero la eliminación de llamadas frecuentes a procedimientos pequeños aún puede producir aumentos de velocidad mensurables. Existen dos maneras estandar para eliminar llamadas de procedimiento. Una es reempluar la Ilama- da de procedimiento con cl código para el cuerpo del procedimiento (con un reemplazo ade- cuado de parámetros por argumentos). Esto se denomina descobertura de procedimiento, y en ocasiones incluso es una opción de lenguaje (como en C+ +). Otra forma de eliminar las llamadas de procedimiento es reconocer la recursividad de cola, er decir, cuando La última operación de un procedimiento es para llamarse a \í imsmo. Por ejemplo, el proce- dimiento

Page 475: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Una visión general de las técnicas de opti1niraci6n de código

int gcd( int u, int v)

{ if (v==O) return u;

else return gcd(v,u % v) ; 1

es recursivo de cola, pero el procediniicnto

int fact ( int n )

( if (n==O) return 1;

else return n * factfn-1); 1

no lo es. La recursividad de cota es equivalente a asignar los valores de los nuevos argunten- tos a los parámetros y efectuar un salto hasta el inicio del cuerpo del procedimiento. Por ejemplo, el procedimiento recursivo de cola gcd se puede volver a escribir mediante el compilador para el código equivalente

int gcdf int u, int V)

t begin: if (-?==O) return u;

(: int tí = v, t2 = u%v; u = tl; v = t2; goto begin;

1 1

(Advierta la sutil necesidad de elementos temporales en este código.) Este proceso se cono- ce como eliminación de recursividad de cola.

Una cuesticín respecto a la llatnada de procedimiento se relaciona con la asignación del registro. Debe hacerse la previsión en cada llamada de procedimiento para grabar y rrcupc- rar registros que perinanezcan en uso durante la duración de la llamada. Si se proporciona una fuerte asignación de registros, se puede incrementar el costo de las llamadas de proce- dimiento, puesto que proporcionalmente será necesario graba y recuperar más registros. En ocasiones este costo se puede reducir incluyendo las consideraciones de llamadas en el ele- mento que asigna el registro. El hardware de apoyo para llamadas con muchos registros en uso también puede reducir este costo.*" Pero éste es un caso de un fenómeno común: a ve- ces las optimizaciones pueden provocar que se invierta el efecto deseado, y deben hacerse convenios 31 respecto.

Durante la generación de código final se pueden encontrar algunas últimas oportuni- dades para reducir el costo de ciertas operaciones utilizando instrucciones especiales dis- ponibles en la máquina objetivo. Por ejemplo, muchas arquitecturas incluyen operaciones de desplazamiento de bloques que son más Apidas que la copia de elementos individuales de cadenas o arreglos. También, en ocasiones, los cálculos de direcciones pueden ser optirni- zados cuando la xquitectura permite que se combinen diversos modos de direccionamiento o cálculos de desplazamiento dentro de tka instrucción simple. De manera similar, a veces existen modos de autoincremento y autodecremento para su uso en la indizüción (la arqui- tectura VAX tiene incluso una instrucción de iiicren~ento-coií!paración-ramificación para bucles o cicl«s). Tales optimizaciones vienen bajo el encabezado selección de instrucciones o uso de idiomas de mhquina.

Page 476: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP 8 Í GEIIERACIOII DE C ~ D ~ G O

Comporiamienento de programa predictivo P:wa realizar alguitas de las cipiiiriizacioiles pr3 te dcscritas, un coinpilador debe recolectar iiiformación :rcerca de los usos de las va valores y procedirnientos en los programas: si las expresiones son rcutilizadas (y así vcrr ~!~hexpresiones cornuties), si es q~ ie las vxiables carnhim sus uaíores o pe constantes y cnándo ocurre esto: y si los procedirnientos son o no ilaniados. Cn co debe, tlerltm del alcance de sus récnic:is de citculo, hacer suposiciones basado en e los casos ;)cerca de la infoim:iciún que recolecta o arriesg:irse a generar código Inco si una variable puede o no ser constmie en un punto particular, el cornpilador debe que no es citnsiante. Esta propiedad se tleironlina estimaci6n conservadora de ción del programa. Significa cpe un co~npiiador debe hacerlo con infbrmación poc que perfecta. y con frecuencia incluso de manera rudimentaria, acerca del compor del programa. Naturalrnente. cuanto más solisticado se vuelva el aniiisis del progr ior ser& la iriforniación que un optimizador de d i g o puede uttliz:ir. No obstante, i los compiladores más avanzados de la actualidad, puede haber propiedades compro de programas que no sean descubiertas por el cornpilador.

Un rnétodo diferente es tomado por t~lgunos compiladores en que se obtiene portamiento estadístico acerca de un programa aparte de las ejecuciones reales, y po mente utilizado para predecir qué trayectorias tienen n~ás probabilidad de ser tomad procedimientos tienen más probabilidad de ser ila~nados con frecuencia y qué secci código tienen mayores probabilidades de ser ejecutadas con mis frecuencia. Esta in ción puede entonces utilizarse para ajustar la estructura de saltos, bucles y código de dimientos con el fin de minimizar la velocidad de ejecución en la mayoría de I ncs que se presentan comúnmente, Este proceso requiere, por supuesto. que un eca de rendimiento así tenga acceso a los datos apropiados y que (por 10 menos algo de) el código ejecutable contenga código de instmmentación para generarlos.

asificación de o Corno hay tantas bportunidades de optitnización y técnicas. vale la pena reducir la com jidad de su estudio :rdoptando varios esquemas de clasificación para enfatizar las calid diferentes de las optirnizaciones. Dos clasificaciones útiles son el ticrnpo durante e1 proc so de compilación en que sc puede ciplicar una optimimción.y e1 irea del programa sobre cual se aolica la ootimi~ación.

En primer lugar, considerxemos el tiempo de :tplicación durante la cctmpi1;ición. Las : iiptiniizaciones se ptleiíen realizar prácticamente en cualquier ctapa de la compilación. Por ejemplo, la incorporación de constante se puede realizar en una etapa tan ternprana cotn durante el anáiisis sintáctico (aunque por lo regular es aplazada, de manera que el corn Jor puede obtener una representación del código fuente exactamente corno fne escrito). otra parte, algunas optiinizacionrs se pueden retrasar hasta después que se haya generad código objetivo: el código objetivo es examinado y vuelto a escribir para re,flejas !a ~ación. Por ejemplo, las optimizaciones de saltos se podrían realizar de esta miiitera. ocasiones las optimimciones realizadas en $1 código objetivo se denominan uptinriraciones de mirilla, ya que el cornpilador generalmente examina pequeñas seci-iones de círdigo objetivo para descubrir estas optimizaciones.)

Por lo común, Iarriayoría de !as optimizaciones se realizan cii~ranre la geiieracih de código intermedio, justo después de la generiición de código iiitermedio, o bien, 11iir:inte ia :eneracibri cicl código objetivo. En e1 punto en que una opt in i i~ac ih !lo dcpcnde de las ~;ir:ictcrísiicas de la rnicjiiiria objctivo (clrriomin:idas opkimíc~iones a nivel de fuenlr}. perlen ~aii7::rs~ . I ~ I I C S iie i;i\: que depe:;i!cn de In ari~uiicctiira ihjctivo !trptirtr¡zacit>nes :t n iwl de ol?,jeti~,:11, A ,"cces unii op~imimcik piede tericr wito m t:t~~~yx:;it:~itc , d i~i~,:! t!c

. , fwnte C,.~II¡O !m C I ) I ~ I ~ C : I I ~ I I ~ ~ a! nivel <,?e d ~ ~ ~ c ! i ? ~ ) . ?or ejanph. cn 1:) x+!xic:~.o;i ' ~ ? c irqi\.h:s

Page 477: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Una virton general de las 16tnic.u de optim~zac~án de código 473

es coiníin contar el número de referencias ri cada variable y preferir aqiicllas variables con conteos nlás altos de referencias para la asignación a registros. Esta Larea se puede dividir en un componente al nivel de fuente, en el cual las variables son seleccionadas para ser asignadas en registros sin cortocimiento específico de cuántos registnx están disponibles. Luego, un paso de asignación de registro que cs dependiente de la ixíquina objetivo asigna registros reales a aquellas variables etiyuetadiis como asignadas a registros, o 21 localidades de memoria deiioiniiiadds pseudorregistros, si rio se dispone de registros.

Al ordenar las diversas optimizaciones es importdnte considerar e! efccto que una optiniización tiene sobre otra. Por ejemplo, tiene sentido propagar constantes antes de realizar elirniiinción de código itialcanzable. puesto que algún código se puede volver inalcanzable con base en el hecho de que se hayan ericontrado variables de prueba que sean constantes. Ocasionalmente, puede surgir un problema de fase en el que cada una de dos iiptimizacioiies puede revelar oportunidades adicionales para la otra. Por ejemplo. conside- re e1 código

x = 1;

y = O;

... if (y) x ;- 0;

En primer paso para propagaci6ri de constantes puede producir el código

. . . y = O;

. . . if (O) x S O;

Ahora, el cuerpo del primer if es código inalcaiizable: al eliminarlo obteneiiws

Page 478: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 GENERACIÓN DE C ~ D I G O

de eúdigo, es decir, secuencias de código sin saltos dentro o fuera de la secue~icia.~' secuencia mixinia de código lii~eal se cormce como bloque básico, y por definició tintizaciones locales son aquellas que se encuentran restringidas a hloqnes básic optimizaciones que se extienden rnás alla de los bloques bisicos, pero que estin co a un procedimiento individual, se denominan optimizaciones globales (aun cuando verdaderamente "glohales", puesto qnc se encuentran confinadas a un procedintien optiinizacionei que se exrienden más alla de las limitaciones de los procedimientt tod« el programa se conocen como optimizaciones interproceduralcs.

Las optimizaciones locales son relativamente fáciles de realizar, puesto que Ia I

za lineal del código permite que la inforniaci6n sea propagada de forma simple reco Iti secuencia de código. Por ejemplo, puede stiponerse que unevariable que fue cargad un registro tnediante una instrucción anterior en un bloque básico, esti rodavía en e.s rro en un punto posterior en el hloque, siempre que el registro no fuera el objetivo na carga intermedia. Esta conclusióti no scría correcta si se pudiera hacer un salto den cúdigo interniedio, conio se indica en el diagrama siguiente:

carga x dentro de r

(no hay carga <le r)

( x cít6 todavía en r)

carga x dentro de r

{no hay carga de r)

(X pue& no estar cn r)

Bloque básico Bloque no básico

\dlt« devic. otra parte (X puede notar en r)

Las optimi~aciones globales, en contraste, son rnucho más difíciles dc reítlizür: por lo general requieren de una ttcnica denominada análisis de flujo de datos, la cua colectar información a través de los límites de saltos. La optimización interprocedural es a rnás difícil, puesto que involucra posiblemente varios tnecanismos diferentes de paso de pa rametros, la posibilidad de acceso a variables no locales y la necesidad de calcular info mación simultánea en todos los procedimientos que puedan llainarse entre sí. Una con1 caci611 más para la optirnización interprocedural es la posihilidad de que se puedan muchos procedimientos de manera separada y sólo se puedan ligar entre sí en un punto pos- terior. El compilador no puede entonces efectuar ninguna optimización interproc involticraf en una forma especializada al ligador que lleva a cabo las optimizacio en inforniación que el compilador ha obtenido. Por esa razón muchos compiladores s lizan el análisis interprocedural básico, o bien no realizan ninguno.

tina cliise especial de optimizaciones globales es la que se nplica a los ciclos Como éstos por lo regular se ejecutan u n gran número de veces, es important pecial atención al código dentro de los ciclos, en particular respecto a la reduccih de complejidad de los c6lculos. Las estrategias típicas de optimización de ciclos o bucles se enfocan en ta identificación de Las v~riables que se incrementan mediante cantidades fijas con cada iteración (denominadas, por lo tanto. variables de Lduccián). Éstm incluyen

Page 479: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Una visión general de lar técnicas de optimización de iádigo 975

variables de control de ciclos, así ~01110 otras \.ariablei que dependen de las anteriores (las vari:ibles de coutt-ol de ciclos) de li>riiias I~iiis. Las ~xiables de incluccidn seleccion;i&is se pueden colocar en registros y siurplificar su crilculo. Este código vuelto a escribir puede inciuir cálculos de elirninaciht de coristaure del ciclo (denoniirlado inoviniiento de ebdigo). Eti realidad, el reaconx>do del código ta1uhi6n puede ser 6iil para rncjorar iti eficiencia dcl ciidigo dentro de los bloques básicos.

A lo largo de la historia, nna tarea adicional en la optin1i~aci6n de ciclos ha sido el pro- blema de identificar realmente los ciclos o hueles en un progranla, de tnanera que puedan ser optirnizatlos. Este descubrimiento de cielos o bucles era neces:iri» debido a la ausenci:~ clcl control esiructriratlo y el uso de las sentcncins gota para impleineritar ciclos. Aunque el descubrirriiento de ciclos permanece conio algo necesxio en algunos lenguajes cotno FORTRAN, en la mayoría de los 1cnguaji.s se pnede irtilizx l:i sintaxis rnisnia para I o c d - mr estriiciuras de ciclo significativas.

8.9.3 Estructuras de datos y técnicas de implementación para optimizaciones

Algunas optiiiiiraciones se pueden hacer n~edirinte ininsforni~ciones en el mismo :irbol \intáctico. Est:is pueden incluir tanto iiicorp<?rtición de conit:rntes corno eliniinacicín de ctídigo in;ilcanzahle, donde 10s s~~hirbolcs :ipropi:icloi son eliniinadcts o reetnplaiados por otros ntás inrples. La iriforriiaciórr ;i utilizar en oprin~i~nciones posteriores también se p e d e ihtener durante la constt-uccitin (1 recorrido dzl irbol sintáciicn. tal coino el conteo tic rekrcncia u «tr:r inforrnacidri de uso, y conserv;irl;i corno :itribriros en el árbol o introducir- lii en Ia t ~ b l a de síinbulos.

Sin embargo, pm rriuchas de las optiiniz;ieioncs niencion<iclas aiiteri«rniente, el irbnl siiitrictico es una estructura inndecu;da o difícil de inancj~tr para recolectar información y re;tli/r;r oprimizaciones. En SLI lugar. u n optiiniratior ciiie rcxli~,a optimizaciones globa1c.s construirá ü partir del código interrnetlio rle cala proccclimiento una representitcióri gráí'ic;~ tlcl ccídigo dcnorniriada gráfica de finjo. Los riodos tic tina gráfica de flujo son los hlocj~ies básicos. y los boriies son fi:rrriad»s ;i partir de los pitos condicion;iles e incondiciorialei (los que deben tener corno sus ohjctivos los inicios de otros bloqrrcs básicos}. Cada nodo de hioipc h:ísico cwtiene la sec~iencia de instrucciones de ciidigo inteírrieilic ckl bloque. Como ejcmplo. cri 1:) figura 8.18 (página 476) se muestra I U grática de tlujo cor.rcsp»ndiente 211 cóiligci i i i -

icrmcrliit de IU f igur~ 8.2 lpágina 401). (Los bloques básicos ilue se n~ucstran en esta iigiir:l csrin etiyuetirdos para f~iri~rüs referencias.)

Una grifica de flujo, junio con cada unn de ,lis bloques básicos, puede construirse iiic- iiianie un sirnple p s o sobre el código inierniedio. í'ttiia iuievi) biique básico se icIentiiic;i

,?

cic Iii nianera siguiente:--

1. [..a primcrü instrncción comienza rrri nricva hloque básico. 2. Cttda etiqireta que es cl objetivo de un s:iir:~ comicnm c m : i uwu hloytie hásico. 3. C:id;i initriicción cjue sigi~e a irn silto coinienza un rii~cvo bl~>yric h.í" < b~eo.

Page 480: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Fiq~irt 8.18 * ta griíica de Ruja del

código intermedia de la figura 8.2

l

Eact = t2 t 3 = x - 1

La gráfica de Flujo es la principal estructura de datos que necesita el análisis de flujo de datos, el cual la utiliza para acumutür información que será empleada en las optimizaciones. Dikrentes clases de irtforrnación pueden requerir diferentes clases de procesamiento de la gráfica de flujo, y la infoimación obtenida puede ser muy variada, dependiendo de las clases de optirnizaciones deseadas. Aunque no 'tenemos espacio en esta breve perspectiva general para describir la técnica de análisis de flujo de datos con detalle (vease la secci6n de notas y referencias al final del capítulo). puede valer 1% pena describir un ejemplo de la clase de datos que se pueden acumular mediante este proceso.

Un problema estándar de ariálisis de flujo de datos es calcular, para cada variab conjunto de las denominadas definiciones de alcance de esa variable al principio de bloque bJsico. ,4quí una definición es una instnicción de código intermedio que pnede tablecer el valor de la variable, tal como una asignación o una ~ectura.~"or ejemplo, las definiciones de la variable fact en la figura 8.18 son la instrucción simple del bloque básico B2 (fact=L) y la tercera instrucción del bloque B3 (£act=t2j. Llurneiiios a estas definiciones d I y (12. Se dice que una definición alcanza un bloque básico si al principio del bloque la variable todavía puede tener el valor establecido por esta definición. En la gráfi- ca [le flujo de la figura 8.1 S se puede establecer que ninguna <lefinición de f act alcanza BI o B2, que tanto d l como d2 dcanzan U3 y que sólo d2 alcanza R4 y B2. Las dcfitiiciones de alcance se puedeii utilizar rn diversas optirriizacioncs: cn la prop:igación de coiistaritz, por *jcrnplo, si las únicas definiciones que a l ca i i~~n un bloque representan u n wlor constante

Page 481: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Una visión general de las técnlcas de opt~mizacion de código 477

simple, entonces la variable se puede i-eernplaz~r por este valor (por lo rcenos basta qtte se ~tlcance otra deíinieiiin dentro del bloque).

La gráfica de flujo es útil para representar la infurrnaciún global acerca tiel código tie cada procedimiento, pero los bloques básicos toúavía son representados corno secuencias de código simples. Una vez que se realiza el análisis de flujo de datos, y que se va a generar el cúdigo pm cada bloque básico, con frecriencia se construye otra estructura de datos pa- ra cada bloque, llamada DAG de un hioque b&sico (DAG = Directed .Icyclic Graph). (Se puede construir una DAG para cada bloque básico iticluso sin constrttir la grifica de flujo.)

Uua estrnctura de datos DAG describe el cálculo y reasignaciún de valores y variables en un bloque básico de la manera siguiente. 1.0s valores que son utiliz;t~los en el bloqiie que vienen de cualquier parte se representan como nodos de hoja. Las operaciones sobre q u i - 110s y otros valores se representan rnediatite nodos interiores. La asignación de un ~ a l o r nue- vo se representa adjuntando el nombre de la variable elemento temporal objetivo. al nodo que representa'el valor asignado. (Un caso especial de esta constnicción se describió en la pJgina 4 16.)"'

Por ejemplo, el bloqiie básico 83 de la figura 8.18 se puede representar rnetliante la DAG de la figura 8.19. (Las etiquetas al principio de los bloques básicos y saltos a1 final por lo regular no se incluyen en la DAü.) Observe en esta DAG que las operaciones de copia tales como fact=t2 y x=t3 no crean iirievos nodos, sino siinplernente agregan nuevas eti- quetas a los nodos con las etiquetas t 2 y t 3 . Advierta también que el nodo de hoja etique- tado x tiene dos padres, lo que resulta del hecho que el valor de entrada de x es utilizado en dos instrucciones por separado. De este modo, el uso repetido del mismo v;rlor también es representado en la estructura DAG. Esta propiedad de una DAG permite representar el uso repetido de las subexpresiones conlunes. Por ejenipio, la asignacibri en C

se traduce en laa in\trucciones de trey direcciones

y la DAG para esta secuencia de instrucciones está dada en la fignra 8.20: que niuestra el uso repetido de la expresiún x+l.

F ~ q u r a 8 19 t4 @ La DAG del bloque b á m 83 / \

Page 482: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

478 CAP. 8 i GENERACIÓN DE CÓD~GO

Figura 8.20 La DAG de lar inrtructiones de tres direcciones correspon- dientes a la arignation en C x = (X+l)*(x+l) i i

La DAG de un bloque básico se puede construir manteniendo dos diccionarios. E) primero es una tabla que contiene nornbres de variable y c»n\tanteb, con una operación de búsqueda que devuelve el nodo DAG para el cual se encuentra acttialliiente asignad nombre de variable (la tabla de símbolos se podría utilizar como esta tabla). El segund una tabla de iioitos DAG, con una operación de búsqtieda que, dada una operación y no hijos, devuelve el ri«tlo con esa operación y los hijos o el elemento tiulo si no hay tal no Esta operacicín permite la búsqueda de valores existentes sin constmir un nuevo iiodo en la '1 DAG. Por ejemplo. una vez que se constrtlye el nodo + de la figura 8.20 con hijos x y 1, se le asigna el nombre tl (como restiltarlo de procesar la instniccióii de tres direcci tl=x+l), una búsqueda de (+. x, 1) en la segunda tabla devolverá este nodo ya cons do, y la instrucci6n de tres direccioties t2=x+l simplemente provocará que t2 tambiij-n se asignado a este 11odo. Los detalles de esta c«nstrucción se ptreden encontrar eri otra parte (véase la sección de notas y referenciasj.

El código objetivo, o una versión revisada de ccídigo intermedio, se puede generar a tir de una DAG nmliante un recorrido segun crialquiera de las posibles clases topoliíg de los nodos 110 hoja. (Una clase topológica de una DAG es un reconido tal que los hijos d nudos son visitados antes que todos los padres.) Puesto clue' en general, existen rriiicha clases topológicas, es posible generar muchüs secuencias de código diferentes a partir de una DAü. Cuáles pueden ser mejores que otras depende de varios factores, que incluyen tletalies de la arquitectura de la ~náquinri objetivo. Por ejeniplo? una de las secuencias de re- corrido lcgales de los tres nodos no hoja de la figura 8.19 daría corno resuliado la dguiente nueva secuencia de las instrticciones de tres direcciones, que podrían reeinpl:izar el bloque básico original:

t 3 = x - 1

t 2 = fact * x

x = t 3

ta = x == o fact = t2

Page 483: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Una won general de las ticntcas de optlmtzacton de todtgo 479

Un recorrido .;e~nejatite de la DAG de I:i figura 8.20 p d u c c el siguiente código de ires direcciones revisado:

Page 484: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 GEHERACIÓN DE C~DIFO

¡..a tabla cid descriptor de registro estaría vacía. y no la irriprimini-is. Ahora suponga que se genera el ccítiigo siguiente:

LD O , l ( q p ) l o a d E a c t i n t o reg O

LD l,O(gg) i o a d x á n t o reg 1

Mur, 0 , 0 , 1

Los descripttrres de tiit-eccicín ahora serían:

Var~abieiConstante Descriptores de dirección

f a c t

X

t 2

t 3

t 4

1

a

y los dcscriptores tic regis!ro serían

Registro Variables/Consrantes

f a c t , t 2

x -

Ahora, dad,, el cútligo wbs~gu~ente

LDc 2 , 1 ( 0 ) l o a d c o n s t a n t 1 i n t o reg 2

ADD 1,1,2

los ilescriptores de direccicín se convertirían en:

VarlableiConstante Descriptores de dirección

f a c t i nReg ( O

x i nReg ( 1 )

t 2 inReg ( 0 )

t 3 i n R e g ( 1 )

t 4 - L i s C o n s c ( l ) , i n R e g ( 2 )

O i s i l o n s t ( 0 )

Page 485: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Optimiracioner simples para el generador de código de i !HY

Registro VariablesiConstantes

fact, t2

x, t3

Dejamos al lector proporcionar el citdigo adecoatio para el cálcido del valor e11 el nodo res- tante DAG y describir los dc,scriptnres de registro y dirección rcsiiltantes.

Esto concluye ntiestro breve recorrido a través de las i2criicas i!c opiimiznci~ín de ciidigo.

8,10 OPTIMIZ CIONES SIMPLES P E CODIGO DE TINY

El generador de código pira el lenguüje TINY, como se proporcionit cn la sección 5.8. pro- duce un ccidigo muy ineliciente. coino se demostró mediante tina coniparaciiin de las 42 ins- trucciones de la figura 8.17 (pagina 466) respecio a las nueve instrucciones codificadas u mano del programa equivalente de la figura 8.16 (página 458). En primr lugar, i:rs iric('i- ciencias se deben a dos fuentes:

1. El generador de cóc!igo TlNY hace un uso muy pobre tic los registros de !a máquina 'TM (de hecho, nunca iililim los registros 2, 3 o 4).

2. El generador de citdigo TINY genera iilnecesari;imente los valores i0gicos O y 1 ra las pruebas, cnmdo tstüs sólo aparecen en sentencias if y sentencicts while. donde lo haría un citcligo ni:is simple.

Deseamos señalar en esta sección cómo inoiuso las técnicas relativanlente rrií!inientari&s pueden rnqjorar de manera sustanci;il e1 código generado por el conipilüdor <!e TINY, En realidad, no genera ni bloques básicos ni gráficas de flujo. pero continúa generando ccídigo directamente del árbol sintktico. La única mayuinaria riecesaria es un segnlerito adicimal de datos de atributo y algún cbdigo de compilacior nn poco más conrplejo. No damos deta- lles completos de impleinentaciitn para las mejoras ilescritas aquí. pero los dejamos como ejercicios para e! lector.

8,101 Eonsewación de elementos temporales en registros La primera optimizacicin que deseamos describir es un método fácil para mantener elernen- tos temporales e» registros en lugar de aiinacenarlos y volverlos a cargar constantemente desde la memoria. En el generridor de código TINY los elementos temporales siempre eran almaeeriados en la localidad

tnpof f set (rng)

doiide trnpof fset es una variable cstática inicializa$a a O, que se decri.inenta cada vez que se :ilrn:~cena i i n elemento kmporal, y :se iiic~~ementa cada v a que se vuelve a cargar (\&se cI ;ipéntÍii:e B, líneas 7023 y 2027~. Una manera siniple de iitilizar los registros comu Ii?c:rlictades temporulcs es inrc~prcrar tmpoff set haciendo referencia inici:ilti-icnii: ;I i í~, registros, y s6lo ti;yxris quc sc han nüpol:di> !os registros dinponihles. iirilitar!~) u?i:ri! u11

~lcspl;izaitiicr~ti> rcal I:I r:ici?rori:i. IJor cjeiíipio. :4 cjricreinos eiiiplcnr Iot!os !os ~ q ; l s t i ~ ~ ~

Page 486: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 1 GENERACION DE CODIGO

tlisponibles corno elerrientos temporiiles (después del pc. gp y rnp), eritonces los desde O hasta -4 de tmpOf f set se pueden interpretar como referencias a los r hasta el 4, iriieritc.;is que los valores que coniicrizan con -5 se eniplean como despl tos (con el S ;~gregailo a sus valores). Este ~nec:rnisriio se prrede iniplementar dit-ec en el generador de ciídigo mediante pmchas apropiadas o encapsulado en procedi auxilinres (los cuales pc~dríari denomiiiarse savelhip y loadlhipj . También deb nirse el hecho de que el resultado de LIII cálculi> de sul~xpresiiíri se pueda encontrar A

regisiro aparte del O después de la gei~eración de código recursivo. Con esta mejora el generador (le ciídigo 'TINY ahora genera la secuencia de có

dada en la figura 8.21 (compare coi1 la figura 8.17). Este c6tligo ahora es 20% más co no contiene a1m;iccnamiento de elernentos ternpor:~les (y el rrlp no utili~a el registr peiar de todo, los registros 2, 3 y 4 nunca se ~rtilimn. Esto no es irn~suaI: I:IS expresi los programas vez son lo srrt'icienterrierite complejas para requerir más de dos o r iiictitos teriipor:iles ;I la vez.

8.102 fonsewaci6n de variables en registros Se puede Iiaeer una mejora adicional en el uso de los registros 'TM reserv~iiclo alg los registros para su riso como localidades de variables. Esto requiere un poco nrri. b-jo que la optiinización ;interior: ya que la ubicaciún de una variable debe ser deter ;itites de la geiieracicín tic chdigo y almacenada en la tabla de símbolos. 1:n esqtiem co!xiste simplemente en seleccionar irnos co:~iitos registros y asignar éstos corno las lo- (!;?des para las v:iriables :I¡& irtilizadas del programa. Para cleter~niitar c~iáles v:triables las "mis iitiliznrlas", dehe tiacerse u n ccintco de reí'erericias (usos y itsignaciones). Las riabies que son rckrenci;rs dur;inte los ciclos (ya se3 en el cuerpo de! ciclo o la cxpresib prueba) deberían tener preferencia. pticsto que las referencias tienen la probabilidad d repetici-is a rnedida que se ejecuta el ciclo bucle. Un enfoque simple que ha demoit fiinciimar bien 211 ~ I L I C ~ O S compiladores existentes es ii~ultiplicar por 10 el conteo de to las refercricias dentro de iai ciclo, por 100 dentro de un ciclo doblerncrite anidado. y asís ceiivamente. Este conteo de referencias se puede Iiacer durante el ~tnálisis seinántico, y un vez que se 1x1 hecho, se reelim un paso de asignaciítri de variable por separado. El atrib 'iie localidad que se ulrriacena en la tabla de sínibolos debe ahora tener en cuenta ;yir variables que están asigriailas ;t registros en conirlrposicih a la rnemoriti. Un q u e r n a ple titiliza rin tipo eriurncrado pwa inclicrtr dcíiidc está localizada una variable: en este existen sijlo dos posihilid:ides: inReg c imem. Adicionalmente, se debe conservar el nicro de registro en el primero dc los casos y la dirección de la rneinoria en e1 segundo. ; (Éste es un ejemplo simple de descriptores de dirección para variables: los cksixipt registro no son rtecesarios porque permaiieceil fijos ct~irante el transcirrso de lu gen tiel código.)

Con estas modificaciones el ciidigo del programa de muestra puede :ihhr.:t ~ ~ t i l i , a r el gisim 3 para v:iri:tble x y el registro 4 para !a v:tri:rbie f a c t !hay só!o iios variiibles, d do que iodos piieden caber en los registros), si~poniendo que 10s registros dcL O a1 2 toci:tví~ iht;in st:serv3tlos p:ira eleinsnios reinporalcs. Los incxiific;iciones ai c6cli:io tlc la iig~ira 8.2 1 $S-

[,(!¡ (!:idas ccr I:, figura 8.72. Este ciidigo es de n u c ~ i c~iciita consii:err,hiei;icr~ie I I ¡ C I ~ . que cJ

Page 487: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Optimizaciones simples para el generador de codigo de TlMY

Figurd 8.22 Código JM para e l progra- ma TlNY de muestra ton elen~entos temporales y rariábler tOflSe~ad0S e11

registros

LD 6,0(0)

S T O,O(O)

IN 0,0,0

ST 0,0(5)

LDC 0,0(0)

LD 1.0(5)

SUB O,O,l

J L T 0,2(7)

LDC O,O(O)

LDA 7,1(7)

LDC 0,1(0)

J E Q 0,21(7)

LDC 0,1(0)

ST 0,1(5)

LD 0,1(5)

LD 1,0(5)

MUL 0,0,1

LD

ST

IN

LDC

SUB

J L T

LDC

LDA

LDC

JEQ

LDC

lilUL

LDA

ST

LD

LDC

SUB

S T

LD

LDC

SUB

JEQ

LDC

LDA

LDC

JEQ

LD

OUT

LDA

KALT

LDC

SUB

LDA

LDC

SUB

JEQ

LDC

LDA

LDC

JEQ

OUT

LDA

HALT

tieiie variables booleartns y no necesita esle nivel de generalidad. Esto tüinhitin protfuce rr tc i -

chas cargas :idicionales de las constantes O y 1 ; :idenlis de pnichas adicioit;tles que se gene- ran por separrttlo iiiediante el tlít l igo genstmt piim Iits sentencias de control.

La mejora que describimos itqtií depende del hecho que un operador ile comp;iratión de- he aparecer conio e l nodo r:rír de la cxpresilíii de prueba. E l clítligo de genexp para este opcr:id«r sinipleii~ente generarti cijtligo para sustraer el valor rlcl opcrando de la derecha del operailtlo de la izquierda, tlejaildo el r-esriliado en el registro 0. Ti1 ciícligt> p:irrt 13 setiten- ci. ,i 'f. i o la scriieticia \\hile p~ob:trti ~'~ILOIICCS ~):ir:t cti31 opcr;ttlor de coinp:ir:icitíri es nliiicnilo

4 g c i i e ~ t r i c l c&ligo :ipropiatlo del salto condicion:tl. De cste r i>Oi l i>. c i i el c;iso del c63igo de 13 lig~ii.;~ 8.21, cl cddigo TIYY

if O < x than

Page 488: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

F i ~ u i a 8.23 Código T i l para el progicima l lNY de inueitra

ton elementos iemporaler 1 ~ariables conservador en iegirtror. y cm pruebas de ialtor 3ptinilzadas

4 : SUB 0 , 0 , 3

5 : J L T 0 , 2 ( 7 )

6: LDC 0 , O i t I )

7: LEA 7 , 1 ( 7 )

8 : LDC O , l ( O )

9: J ~ Q 0 , 1 5 ( 7 )

gcnerará en su Ingu el cíidigo Tllí i nk simple

(Atlviena c6mo el salto del caso faso debe ser el coridicional eomplenrrntario JGE cpenid«r de prueba <.)

, . C m esta optin-iizacií,~ el t:othgo generado para el programa de prueba se

en el que se pcoporcioni~ en ia figura 8.23. (Tarrrbién incluirnos en este paso la e1i1 del mito vxío :d fina1 del código. que col-rzsponcle a la parte else v:tcia de la sen kii el código TIXY. Esto requiere 6nic:inienie la adicilin de una prueba siinple al genStmt pua la sentencia ¡f.)

El eiidigo de la figura 8.23 está ahora relativamente cercano al del clidigo gene mano de IU p6giila 458. Aun así, todavía h r y oportunidad para unas cuantas optitnizaei mis de caso especial. las cuales dejaremos para los ejercicios.

O : LD 6 , 0 ( 0 ) 9 : LDC 0 , 1 ( 0 )

1: ST 0 , 0 ( 0 ) 1 0 : SUB 0 , 3 , 0

2 : IN 3 , 0 , 0 11: LDA 3 , 0 ( 0 )

3: LDC 0 , 0 ( 0 ) 12: LDC 0 , 0 ( 0 )

4: SUB 0 , 0 2 3 13: SUB O , 3 , 0

S : m e 0 , 1 0 ( 7 ) 14: SNE 0 , - 8 ( 7 )

6: LDC 4 , 1 ( 0 ) 1 5 : OUT 4 , 0 , 0

7: *WL 0 , 4 , 3 16: HALT 0 , 0 , 0

8: LDA 4 , 0 ( 0 )

Page 489: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]
Page 490: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 / G E W E R A C I ~ N DE CÓDIGO

c. Supoiiga que una instrucción de salto cc~ndiciunnl torna tres veccs el tieiiipo para ejccut:use si el salto cstií seguido en lugar de que el c6digo "caiga eii la nada" (es decir, la condici&l es falsa). ;.'Tienen I:is ory;inizncioncs de wlto de los incisos a y b alguna ventaja en el tiempo sobre la di: la figura 8.1 l ?

8.14 Gna alternativa para iinplenientar sentencias case o switch cutiio una secuencia de prueba cada caso cs la clcnoinin& tshta de salto; donde el índice de caso se iitiliza como un desp mmiento p:trii un salto iiiditado cn una tabla de siiltos ttbsoltitos. a. Este método tle irnplemcntaci6n es veiitüjoso sólo si existen riiuchos casos diferentes que

presentan de inaiirra muy densa sobre un intrrwlo bastmte conipacto de índices. ¿,Por b. Los gcncradores de c6digo tienden a genenir esta clase de c6digo súlo cuando hay mi.

tipro~imatlamente 10 clisrts. Determine si su c(impiia(11ir de C genera una tabla de salto una sentencia switch y si hay un número mínimo de casos para hacerlo así.

8.15 a. I)esai~olle una frirrnula para el ciílculo dc di:-ecci6n de uri eleniento de arreglo nlultidi sion;d semejante al de La ieccióri 8.32 (p5giria 419). Estahlczca ciialquier suposición que haga.

b. Suponga que uiiü vari&le de ;irregio a está definida mediante el código en C

int a[121 [lo01 151;

S~ipoiiiendo que un entero ocupa dos bytes en mernoka, use su kiimrila del inciso a) para de- , .

icrniinar el desp1:tzamirnlo desde la direcciún base de a de la variable subitidizada

a[51 [421 121

8.16 Dado que el programa sigiiieiite escrito de acuerdo con la gramática de definicitin/llainada de funcih eri la página 440:

a. Describa la seciiencia de instrucciones en código P que serían generadas por este programa n16diaiite el prucedimierito gencode de la figura 8.14. pigina 441.

b. Escriba una secuencia de instrucciones de ires direccio~ies que sería generada por este pro- grama.

8.17 El texto no especificó ciiándo se emitía la instnicción de tres direcciones arg durante las llaniadas de función: algunas versiones de código de tres direccioiies requieren que todas 1. LIS . hentericias . arg se reúnan irimcdiatanierite antes de la iltimada asociada, mientras que otras perniiten que se entremezclen el cálculo de los argtinientos y las sentencias arg. (Véase la página 438). Discuta los pros y los contras de estos dos tidtotlos.

8.18 a. Enumere todas las iiistrucciones en c6digo P utilizadas en este c:!pítulo. junto con una descrip~.iún de su significado y uso.

b. Enirniere todas I:is insl.riicciones eri cúdigo de tres direcciones utilizadas en este capítulo, junto con uiin ~lescripción de su significado y tuso.

3.19 Escriba un pgrarnn TZ4 eqriival~iite al prograinii gcd de TlNY del ejercicic 8.5. 8.20 a. La TM ni> tiene iiisti-uccidn (le ~noviiniciito de registro :i rcgimo. 1)escrih:i c6nio se cfectúa

esto. b. La '1'M no tiene iirstriicciiíri de lliiinada o scii>rno. rlescrihn ciinro re ptiedcn imit:ir.

8.21 I!iwie ui: co[irr)cesa(lor de pmitii ilottinte p x i la in:iquiii;i I'INY que se pueda ,igreg:ir \¡ti

riiot1ific;ir iiinguiia ile I:is tleclsracioiii..i dc iri~.in~ii-iü o w~isirn cxiiicrites ilel .il:c'iidii.c C .

Page 491: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ejercicios 487

8.22 Escriba la secuencia de instrucciones TM generadas por cl compilador de TINY parn las siguie~i- res expresiones y :tsignaci»ries de TINY: a. 2+3+4+5 b. 2+(3+(4+5)) c. x: = x+ (y+2*z ). suponiendo que x. y y z tengan localiiladcs dMem O, I y 2. rcspccti-

vametite. d. v : u - U/V*V; (Una línea del programa gcd de TINY del ejercicio 8.5; suponga e1 ambiente de ejecucidn TINY estándar.)

8.23 Diseñe un :nnhientc de ejecuciSn TM para la gramática de 11aii1udi~ de fiinción de la sección 8.5.2.

8.24 El conipitador U«rIai~d 3.0 genera el códign 80x86 siguiente para calcular el resultado Itigico de tnia comparaciih x<y, suponiendo que x y y son entera a desplaramicntos --2 y --4 en el registro de activación local:

mov ax,word gtr [bp-21

cmg ax,word gtr [bg-41

i se short @1@86 mov ax, 1

j mg short @1@114 @1@86 :

xor ax, ax @1@114 :

Compare esto con el código TM producido por el conipilador TINY para la misma expresih. 8.25 Examine cómo su conipil:id«r de C implemeiita operaciones bo«lean:is de cortocircuito, y

compare la implenient:ición con las estructuras de control de la sección 8.4. 8.26 Dibuje una gráfica de flujo para el código de tres direcciones correspondiente al programa gcd

de TlNY del ejercicio 8.5. 8.27 Dibuje una DAG para el bloque básico correspondiente al cuerpo de la s e n t m h repeat del

prngrama gcd de TINY del ejercicio 8.5. 8.28 Considere la DAG de la figura 8.19, pitgina 477. Si se supone que el operador de igualdad del

nodo de extrema derecha es imitado en la rnUquina TM nicdiante sustraccicin, el cddigo TM correspoiiilientc a este nodo puede scr de la inane= siguiente:

LDC 2,0(0) load constant O into reg 2

SUB 2,1,2

Escriba descriptores de registro y de direccidn (basados en los de la página 480) como podrían llegar a ser después de la ejecuciún del código anterior.

8.29 Determine las optirnizaciones que realiza su compilador de C, y compárelas con las descritas en la sección 8.9.

8.30 Dos optirniraciones ;idicionales que pueden ser implementadas en el generador de cóiiigo TINY son las siguientes: l . Si uno de los operandos de tina expresión de prueba es la constante 0, entonces no necesita

.. '1 i m s e ninguna sostracci6n antes de generar e1 salto condicional. 2. Si el objetivo de una ;isign:ición estU ya en un regimo, entonces la exprcsicin del ¡;ido dcre-

cho puede calcularse en este registro, ahorrando así tin moviriiiento de registro a registro. Muestre el código para el progrmia (actor y a1 TINY de (nuestra que sería generado si cstas dos «ptirrii~ricinocs se :igrepi.an ;rl gcneradnr de código que produce el código de la figura 8.23. ¿,<'*jniu w conipara este c d d i p con el cúdigo gcnwido a mano de Iü figura 8.16 i j~igi- tia 45X)Y

Page 492: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

488 CAP. 8 1 GENEMION DE ~ 6 0 1 ~ 0

EJERCICIOS DE 8.31 Vuelva a escribir el código de IU figura 8.7 (sección 8.2.2, página 412) para producir código p 'i PROCRAMACION como un atributo de cadena sintetizado de acuerdo con la gramática cnn atributos de la tabla

8.1 (página 408), y compare la com[~iejidad del código con el de la figura 8.7. 8.32 Vuelva a escribir cada uno de los siguientes procedimientos de generación (le código P para

producir en su lugar código de tres direcciones:

a. Figura 8.7, página 412 (expresiones simples tipo C ) . b. Figura 8.9, pAgina 424 (expresiones con arreglos).

c. Figura 8.12, página 435 (sentencias de control). d. Figura 8.14, página 441 (funciones).

8.33 Escriba una especificación Yacc similar a la de la figura 8.8, página 413, correspondiente ;I los procedimientos de generación de código de la a. Figura 8.9, página 424 (expresiones con ,meglos). b. Figura 8.12, página 435 (sentencias de control). c. Figura 8.14, página 341 (funciones).

8.34 Vuelva a escribir la especificación Yacc de la figura 8.8 para producir código de tres direccio- nes en lugar de código P.

8.35 Agregue los operadores de1 ejercicio 8.10 al procedimiento de generación de código de la figu- ra 8.7, página 412.

8.36 Vuelva a escribir el procedimiento de generación de código de la figura 8.12. página 435, para incluir lm nuevas estructuras de control del ejercicio 8.1 1.

8.37 Vuelva a escribir el código de la figura 8.7, página 412, para producir código TM en vez de código P. (Usted puede tomar las utilidades de generación de código del archivo code .h del compilador TINY.)

8.38 Vuelva a escribir el código de la figura 8.14 (página 441) para generar código TM, utilizando su diseño de ambiente de ejecución del ejercicio 8.23.

8.39 a. Agregue arreglos simples al compilador y lenguaje TINY. Esto requiere que las declaracio- nes de arreglo se agreguen antes de las sentencias mismas, como en

array a[101; i := 1;

repeat read a [ i l ; i := i + 1;

until 10 < i

b. Agregue verificaciones de límites a su código del inciso a, de manera que los subíndices fuera de límites provoquen un alto en la máquina TM.

8.40 a. Implemente su diseño del coprocesador de punto flotante TM del ejercicio 8.21. b. Utilice su capacidad TM de punto flotante para reemplazar los enteros por números re.iJi.s

en el compilador y lenguaje TINY. c. Vuelva a escribir el compilador y lenguaje TINY para incluir tanto valores enteros como valores de punto flotante.

8.41 Escriba un traductor de c6digo P a código de tres direcciones. 8.42 Escriba un traductor de código de tres direcciones a código P. 8.43 Vuelva a escribir el generador de código TINY para generar código P. 8.44 Escriba un traductor de código P a código de máquina TM. tomando el generador de cúdigo P

del ejercicio anterior y el ambiente de ejecución TINY descrito cn el texto.

Page 493: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Notas y referencia 489

NOTAS Y REFERENCIAS

8.45 Vuelva a escribir el generador de código TINY para generar código de tres direcciones. 8.46 Escriba un traductor del código de tres direcciones a código de máquina TM, tomando el ge-

nerador de código de tres direcciones del ejercicio anterior y el ambiente de ejecucih TINY descrito en el texto.

8.47 Implemente las tres optimizaciones del generador de código TlNY descritas en la sección 8.10: a. Utilice los primeros tres registros TM como localidades temporales. b. Emplee los registros 3 y 4 como localidades para las variables más utilizud~s. c. Optimice el código para las expresiones de prueba de manera que ya no generen los valores

hooleanos O y l. 8.48 Implemente la incorporación de constante en el compilador TINY. 8.49 a. Implemeute la optimización 1 del ejercicio 8.30.

b. Implemente la optimización 2 del ejercicio 8.30.

Existe una variedad enorme de técnicas de generación de código y optimización; este capítu- lo representa sólo una introducción. Una buena visión general de tales técnicas (en paiticular el análisis de flujo de datos), desde un punto de vista algo teórico, está contenida en Aho, Sethi y Ullman 119861. Varios temas prácticos se tratan con más detalle en Fischer y LeBlanc 11991 1. En ambos se describen las tablas de salto para sentencias caselswitch (ejercicio 8.13). Para ejemplos de generación de código en procesadores particulares (MIPS, Sparc y PC) véase Fraser y Hanson 119951. La generación de código como análisis de atributo se trata en Slonneger y Kurtz 119951.

La variabilidad del código intermedio de compilador a compilador ha sido una fuente de problemas de transportabilidad desde que los primeros compiladores fueron escritos. Origi- nalmente, se pensaba que se podría desarrollar un código intermedio universal que serviría para todos los comptladores y resolvería el problema de la transportabilidad (Strong [19581, Steel [1961]). Desgraciadamente, éste no ha probado ser el caso. El código de tres d~rec- ciones o cuádruples es una forma tradicional de código intermedio y se utiliza en muchos textos de compiladores. El código P se describe con detalle en Nori et al. [1981]. Una fo~ma un poco más sofisticada del código P, llamada código U, que permite una mejor optimización del código objetivo, se describe en Perkins y Sites [1977]. Una versión similar de código P fue utilizada en un compilador de optimización de Modula-2 (Powell[1984]). Un código in- termedio especializado para compiladores Ada, llamado Diana, se describe en Goos y Wulf [l98l]. Un código intermedio que utiliza expresiones de prefijo tipo LISP se conoce como lenguaje de transferencia de registro, o RTL, por sus siglas en inglés, y se utiliza en los com- piladores Gnu (Stallman [19941); se describe en Davidson y Fraser [1984a,b]. Para un ejem- plo de un código intermedio basado en C que se puede compilar utilizando un compilador de C, véase Holuh [1990].

No hay una referencia detallada actuali~ada para técnicas de optimización, aunque las referencias estándar de Aho, Sethi y Ullman 119861 y de Fischer y LeBlanc [19911 contie- nen buenos resúmenes. Muchas técnicas poderosas y útiles han sido publicadas en el ACM Progrumrnin,q L.anguage.s Design and Irnplementutir>n Conference Proceedings (anterior- mente conocida como la Compiler Construction Conference), que aparecen como parte de las ACM SIGPWN Notices. Fuentes adicionales para muchas técnicas de optimizaciítn son los ACM Principies of Pro~rumming Lunguuges Conference Proceedings y las ACM Transuctions on Prograrnming Languuges urrd Systeins.

Page 494: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

CAP. 8 i GENERAC~OW DE C ~ D I G O

Un aspecto de la generación de c6digo que no liemos nieneion;tdo es la generaciú" aiitomática de un generador de código utilizando una descripción formal de la arquitectura de la miquina, de una rmnera semejante a la forma en que los analizadores sintBcticos y analizt~dores se~ilánticos se pueden genera de manera automática. Tales niétodos varían desde los puramente sintácticos (Glanville y Graham [19781) hasta los basados en atributos (Ganapatlii y Fischer (19851) y los basados en el código intermedio (Davidson y Eraser [198&1). Una visión general de éstos y otros métodos se puede encontrar en Fischer y ..

LeBlanc [19911.

Page 495: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Apéndice A

Proyeclo de compilador

A. 1 Convenciones Iéxicas de C- AS Proyectos de programación A.2 Sintaxis y semántica de C- utilizando C- y TM

A.3 Programas de muestra en C- A.4 Un ambiente de ejecución de

la Máquina TlNY para el lenguaje C-

Definimos aquí un lenguaje de programación denominado C- Minus o C-, para abre- viar, el cual es un lenguaje adecuado para un proyecto de compilador que es más com- plejo que el lenguaje TlNY debido a que incluye funciones y arreglos. En esencia es un subconjunto del lenguaje C, pero sin algunas partes importantes de éste, de donde se de- duce su nombre. Este apéndice consta de cinco secciones. En la primera enumeramos las convenciones lexicas del lenguaje, incluyendo una descripción de los tokens del lenguaje. En la segunda proporcionamos una descripción BNF de cada construcción del lenguaje, jun- to con una descripción en idioma ingles de la semántica asociada. En la tercera sección ofrecemos dos programas de muestra en C-. En la cuarta describimos un ambiente de ejecuci6n de la máquina TlNY para C-. La última sección describe varios proyectos de programación utilizando C- y TM apropiados para un curso de compiladores.

A.1 CONVENCIONES LÉXICAS DE C-

1. Las palabras clave o reservadas del lenguaje son las siguientes:

e l s e i f int return v o i d while

Todas las palabras reservadas o clave están reservadas. y deben ser escritas en rninúsculas.

2. Los símbolos cspecides son los siguientes:

Page 496: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

APÉNDICE A / PROYECTO DE COIIPIMDOR

3. Otros tokens son ID y m, definidos mediante las siguientes expresiones regulares:

I D = l e t r a l e t r a *

NVM = d í g i t o d í g i t o *

l e t r a = a l . . l z l A I . . I Z d í g i t o = O1. .19

Se distingue entre letras minúsculas y mayúsculas. 4. Los espacios en blanco se componen de blancos, retornos de Iínea y tabulaciones.

El espacio en blanco es ignorado, excepto cuando deba separar ID, NüMy palabras reservadas.

5. Los comentarios están encerrados entre las anotaciones habituales del lenguaje C / * . . . * /. Los comentarios se pueden colocar en cualquier lugar donde pueda aparecer un espacio en blanco (es decir, los comentarios no pueden ser colocados dentro de los token) y pueden incluir más de una línea. Los comentarios no pue- den estar anidados.

Una gramática BNF para C - es como se describe a continuación:

l. progrurn -+ cleclarcttion-list 2. t/eclurution-list -+ rkclaration-list declurution / cieclrruticm 3. declurution -t vur-(leclaration / furr-tieclrrution 4. var-declurution -t tpe-spec$er I D ; 1 type-specifier I D [ NUM 1 ; 5. type-specifkr -+ i n t / void 6. fin-decluration -+ type-specifier I D ( parctms ) cornpound-stmt 7. pctrums -+ puram-list 1 void 8. pclram-list -t parum-list , pctrum / parum 9. ptrram -+ type-spec$er I D / type-specifkr I D [ 1 LO. corupound-stmt -+ { local-declarutions stutement-list } 11. local-declurutions -+ local-úecluruti(tns var-declurution / empty 12. statement-iist -+ stutement-list stutemerzr / empty 13. .statement -+ expression-stmt / cornpound-stmt / selection-stmt

1 iteration-stmt / returrl-stmt 14. e.xpression-stmt -+ expressiori ; / ; 15. selection-stmt -+ i f ( expression ) statement

1 i f ( expres.rion ) statement else stutement 16. iterution-stmt-+ while ( expression ) statement 17. return-sttnt -+ r e t u r n ; / r e t u r n expressiotr ; 18. expression -+ var = expression / simp1e;expression 9 a -+ ID 1 I D [ expression 1 20. simple-expression -+ udditive-rírpression relop additive-expressiorr

1 ~zdditive-e.upre.s.sion 21. r e l o p - + < = / < / > l > = / = = I != 22. ridclitive-e.rpression -+ rrdditive-rrpression acldop ternt / terrn 23. udclop ---. + / - 24. renn -+ tenn mulopjrctor /factor 25. rnrdop -. * / / 26. firimr --t ( uxpr~rssiori ) 1 reir / (.ir11 ¡ N W

Page 497: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Sintaxis y semántica de C-

27. cal¿ 4 I D ( args )

28. args i arg-list / empíy 29. arg-list i. arg-list , exprexrion / expre>sion

Proporcionamos una breve explicación de la semhtica asociada para cada una de estas re- glas gramaticales.

1. progrum i clrclurution-list 2. declurution-list -+ declarution-list declarrttion / declaration 3. declaration -+ vczr-decluration / Jun-deckiruiion

Un programa (progrcitn) se compone de una lista (o secuencia) de declaraciones (de- claration-list), las cuales pueden ser declaraciones de variable o función, en cualquier orden. Debe haber al menos una declaración. Las restricciones semánticas son como sigue (éstas no se presentan en C). Todas las variables y funciones deben ser declaradas antes de utili- zarlas (esto evita las referencias de retroajuste). La Última declaración en un programa debe ser una declaración de función con el nombre main. Advierta que C- carece de prototi- pos, de manera que no se hace una distinción entre declaraciones y definiciones (como en el lenguaje C).

4. vur-declarution -+ lype-spec$er I D ; / type-spec$er I D [ NUM 1 ; 5. type-.spec$er -+ int / void

Una declaración de variable declara una variable simple de tipo entero o una variable de arreglo cuyo tipo base es entero, y cuyos índices abarcan desde O . . NUM-l. Observe que en C- los Únicos tipos básicos son entero y vado ("void"). En una declaración de varia- ble sólo se puede utilizar el especificador de tipo int. Void es para declaraciones de fun- ción (véase más adelante). Advierta también que sólo se puede declarar una variable por cada declaración.

6. fun-&clrrcition -+ type-.~pecijier I D ( purams ) cumpound-stmt 7. purums --. purum-list / void 8. parum-list -+ param-list , purum 1 param 9. parum 4 type-specijier I D / lype-specifer I D [ 1

Una declaración de función consta de un especificador de tipo (ype-specifier) de retor- no, un identificador y una lista de parámetros separados por comas dentro de paréntesis, se- guida por una sentencia compuesta con el código para la función. Si el tipo de retorno de la función es void, entonces la función no devuelve valor alguno (es decir, es un procedi- miento). Los parámetros de una función pueden ser void (es decir, sin parimetros) o una lista que representa los parámetros de la función. Los parámetros seguidos por corchetes son parámetros de arreglo cuyo tamaño puede variar. Los parámetros enteros simples son pasa- dos por valor. Los parámetros de arreglo .ron pasados por referencia (es decir, como apun- tadores) y deben ser igualados mediante una variable de arreglo durante una llamada. Ad- vierta que no hay parámetros de tipo "función". Los parámetros de una función tienen un ámbito igual a la sentencia compuesta de la declaración de función, y cada invocación de una función tiene un conjunto ~eparado de parámetros. Las funciones pueden ser recursimi (habta el punto en que la declaración antes del uso lo permita).

Una sentencia compuesta se compone de llaves que encierran un conjunto de declara- ciones y sentencias. Una sentencia compucsta se rcaliia al ejecutar Iü secuencia de sentencias

Page 498: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

APENDICE A i PROYECTO DE COMPllADOR

en cI orden dado. Las declaraciones locales tienen un ámbito igual al de la lista de senten- cias de la sentencia compuesta y reentplazan cualquier declaración glohrl.

Advierta que tanto la lista de declar;iciones como la lista de sentencias pueden estas vrtcías. (El no terminal etnpty representa Iu cadena vacía, que se describe cn ocasiones como e.)

Una sentencia de expresión tiene una expresión opcional seguida por un signo de punto y coma. Tales expresiones por lo regular son evaluadas por sus efectos cokitcrales. Por con- siguiente, esta sentencia se utiliza para asignaciones y Ikimadas de función.

15. selection-.\tmt -t i f ( expression ) statement / i f ( expression ) statenlenl e l s e stutement

La sentencia if tiene la semántica habitual: la expresión es evaluada; un valor distinto de cero provoca la ejecución de la primera sentencia; un valor de cero ocasiona la cjecu- ción de la segunda sentencia, si es que existe. Esta regla produce la ambigüedad clásica del else ambiguo, la cual se resuelve de la manera esthdar: la parte else siempre se analiza sintác- ticamente de manera inmediata como una subestructura del if actual (la regla de eliminación de .

ambigüedad "de anidación más cercana").

16. itrratiori-.mnt -t while ( apression ) statemerrt

La sentencia wbile es la única sentencia de iteración en el lenguaje C-. Se ejecuta al evaluar de manera repetida la expresión y al ejecutar entonces la sentencia si la expresión eva- lúa nn valor distinto de cero, finalizando cuando la expresión se evalúa a O.

17. relurn-stmt - return ; / return expression ;

Una sentencia de retorno puede o no devolver un vdor. Las fnnciones no declaradas como void deben devolver valores. Las funciones declaradas void no deben devolver valores. Un retorno provoca la transferencia del control de regrebo a1 elemento quc Ilüina (o la teminación del programa si está dentro de main).

18. expression -+ vur = e.xpres~ion 1 simple-expression 19. vnr -+ I D / I D [ expression ]

Una expresión es una referencia de variable seguida por un sínibolo de asigna:- r i

(signo de igualdad) y una expresión, o solamente una expresión simple. La asignación tiene la semántica de almacena~niento habitual: se encuentra la localidad de !a variable repre- sentada por w r , Luego se evalúa La subexpresión a la derecha de la asignación, y se al- rnztcena el valor de la subexpresión cn la localidad dada. Este valor tambitn es devuelto como el valor de la expresión completa. Una iJar es una variable (entera) simple o hien tina variable de arreglo subin<iizada. Un subíndice negativo provoca que cl programa se detenga (a diferencia de Cj. Sin cnthargo, no se verifican los Iímitcs snperiorcs de los subíndices.

Page 499: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Sintaxis y semántica de C- 495

Las variables representen i n ~ a restriccih :~dicion;il cn C-- respecto 11 C. En C el objeti- vo de una asignación debe ser u11 valor 1, y los valores I son clirecciones que ptrcden ser &- tenidas mediante muchas operaciones. En C- los únicos valores 1 son aquell«s dotlos por In sintaxis de vtzr, y así esto categoría es verificada sintácticarncnte. en va de hiicerlt~ durante la verificación de tipo con~o en C. Por consiguiente, en C- está prohibida la aritmética de :~ptin? CI d ores.

Una expresión simple se compone de operadores relacionales que n<i se asocian (es decir, una expresión sin paréntesis puede tener solainente un operiiclor rclacionul). E l va- lor de una expresih sirnple es el v:tlor de su expresión aditivii s i no contiene operadores rclncionales, o bien, 1 s i el cipertidor re1acion:il se evalúa como verdadero. o O s i se ev;~lúa como falso.

Los términos y expresiones aditivas representan la asociativitlad y precedencia tipicas de los operadores aritméticos. E1 síinbolo/representa la división míera; es decir. cu~ilqtiier residuo es tnrncado.

Un factor es una expresión encerrada entre paréntesis, una variable. que evalúa e1 vdor de su v:~riable; una llarniida de una función, que evalúa el vdor devuelto de la función; o un NUM, cuyo valor es calculado por el analiz:~dor léxico. Una variable de arreglo debe estiu sub- indizada, excepto en el caso de una expresión cotnpuesta por una I D simple y empleada eri una llamada de función con un parámetro de arreglo (vkase a continuación).

Una llarriada de ftnlción consta de un ID (el nombre de la función), seguido por sus ar- gumentos encerados entre paréntesis. Los argumentos pueden estar vacíos o estar compues- tos por una lista de expresiones separadas mediante comas, que representan los valores que se asignarán a Los paránietros durante una 1lam;ida. Las funciones deben ser declaradas mies de llamarlas, y el número de parimetrus en una declaracicín dehe sse igual al núinero de ar- gurrientos en una llamada. Un p:irámetro de arreglo en una declaración de función debe coincidir con una expresión coitipuesta de un ideiitificador siitiple que representa una varia- ble de meglo.

Finalmente. las reglas :tnteriores no proporcionan sentencia de entrada o salida. Debe- ntos incluir tales funciones en la definición de C-, puesto que a diferencia del lenguaje C, C- no tiene facilidades (le ligado 0 conipilación por scpiirado. Por lo t:into, considerare- tnos dos ftiticiones por scr predefiniclas cn el ambiente global, corno s i tuvieran las dcck- r;iciones inclicatlns:

int input(void) I . . . 1 void outgut (int x) I . . .

Page 500: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

A P ~ N O I C E A / PROYECTO DE CONPllADOR I l

1

i La función input no tiene parámetros y devuelve un valor entero desde el dispositivo de i

!

entrada estándar (por lo regular el teclado). La función outgut toma un parámetro entero, cuyo valor imprime a la salida estándar (por lo regular la pantalla), junto con un retorno de : ! línea. L

I

A.3 PROGRAMAS DE MUESTRA EN C- El siguiente. es un programa que introduce dos enteros, calcula su máximo común divisor y /

: lo imprime:

/ * Un programa para realizar el algoritmo !

de Euclides para calcular mcd. * /

int gcd (int u, int V) ( if (V = = O) return u ; else return gcd(v,u-u/v*v); / * u-u/v*v = = u mod v * /

1

A continuación tenemos un programa que introduce una lista de 10 enteros, los clasifica por oiiten de selección, y los exhibe otra vez:

/ * Un programa para realizar ordenación por selección en un arreglo de 10 elementos. */

int x[10];

int minloc [ int al], int low, int high )

( int i; int x; int k; k = low; x = a[lowl: i E low + 1; while (i < high)

C if (a[il < X) [ x = a[il; k = i ; }

i = i + l ;

1 return k;

}

void sort( int al], int low, int high)

Page 501: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Un ambiente de ejecución de la máquina TlNY para el lenguaje C-

{ int i; int k;

i = low; while (i < high-1)

( int t;

k = minloc (a, i, high) ; t = a[kl; a[kl = a[il; a[il = t; i = i + 1 ;

1 1

void main(void)

( int i;

i S o;

while (i < 10)

( x[il = input();

i = i + l ; f

sort (x, 0,lO) ;

i 5 O;

while (i < 10)

( outgut (x[il);

i = i + l ; )

1

PARA EL LENGUAJE C- La descripción siguiente supone que se conoce la máquina TiNY como se expuso en la sección 8.7 y se comprenden los ambientes de ejecución basados en pila expuestos en cl ca- pítulo 7. Como C- (a diferencia de TINY) tiene procedimientos recursivos, el ambiente de ejecución debe estar basado en pilas. El ambiente consta de un área global en la parte superior o tope de dMem, y la pila justo debajo de ella, creciendo hacia abajo en dirección al O. Puesto que C- no contiene apuntadores o asignación dinámica, no hay necesidad de itn apilamiento. La organizacibn básica de cada registro de activación (o marco de pi- la) en C- es

el fp - apunta aqui

params 1 . 1

temps j 1 : : 1

Page 502: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

APINDICE A 1 PROYECTO DE COMPllADOR

Aquí, fp es el apuntador de niarco actual, que se mantiene en un reyiswo para facilidad de acceso. El ofp (por las siglas del término apuntador de marco antiguo en inglés), como se co- mentó en el capítulo 7, es el vínculo de control. Las constantes para la terminación derecha en FO (por las siglas del término desplazamiento de marco en inglés) son los desplazamientos en los que se ctlmaccna cada una de las cantidades indicad;is. El valor initFO es el deq9:tia- miento en cl que los parámetros y variables locales comienzan sus ireas de aIt~tacenainii.rtto en un registro de activación. Como la máquina TINY no contiene apuntador de pila, toda las referencias a campos dentro de un registro de activación utilizarán el fp, con despiazamien- tos de marco negativos.

Por ejemplo. si tenemos la sigu~ente declaracicín de función en C-,

int f (int x , int y )

{ int z ;

entonces x, y y z deben asignarse eti el marco actual, y el desplazamiento de marco en el principio de la generación de código para el cuerpo de f ser6 -5 (una localidad para cada una de x, y y z y dos localidades para la información de administración del registro de ac- tivación). Los tiesplazamientos de x, y y z son -2, -3 y -4, respectivamente.

Pueden encontrarse referencias globales en localidades absolutas en memoria. No obstan- te, como con TINY, también preferimos referencia estas variables mediante desplazamientos desde un registro. Hacemos esto manteniendo un registro fijado, al cual llamaremos gp, que siempre apunta a la dirección máxima. Puesto que el simulador de TM alnwen:r esta dirección en la localidad O antes que comience la ejecución, el gp puede ser cargado des- de la localidad O en el arranque, y el siguiente preludio estándar inicializa el ambiente de ejecuci6n:

O: LD gp, Ofac) * carga gp con maxaddress 1: LDA fp, O(m) * copia gp a fp

2: ST ac, O(ac) * limpia la localidad O

Las llamadas de función también requieren que la localidad del código de micio para ws cuerpos se utilice en una secuencia de llamada. También preferimos llamar funciones efec- tuando un salto relativo utili~ando el valor actual del pe en vez de un salto absoluto. (Esto hace al código potencialmente refocalizable.) El procedimiento de utilería emitRAbs en code . h/code . c se puede emplear para este propósito. (Toma una localidad de código absoluta y lo hace relativo al utilizar la localidad de generación de código actual.)

Por ejemplo, suponga que deseamos llamar una función f cuyo código comienza en la localidad 27, y que estamos actualmente en la localidad 42. Entonces, en lugar de generar el salto absoluto

42: LDC pc, 27(*)

generaríamos

42: LDA gc, -16ípC)

puesto que 27 - (42 t 1 ) - - 16

l a secuencia de llamada üria ciivisih del trabajo i.ttzon;ihle entre el clemento que llama i.1 qtie es lIl;irn:ido c r consentir ytie e1 clemento que llama alinaccne los valores de los ;irgunicn- rcs cn 4 nuevo marco y crear e1 rioc\o mrco. txccpto para el :~lniaccnairiieiit tiel apiintador

Page 503: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Un ambiente de ejecución de la máquina TlNY para el lenguaje C- 499

de 1-etorno en la posición retFO. En vez de almacenar el apuntador de retorno mismo, el ele- mento que llama lo deja en el registro ae, y el elemento llamado lo almacena en el nuevo marco. De este modo, todo cuerpo de funcibn debe comenzar con cddigo para alniacenar ese valor en el (ahora actual) riiarco:

ST ac, retFO(fp)

Esto guarda una instrucción para cada sitio de llamada. Al regreso, cada función carga cn- tonces el pc con esta direccibn de retorno al ejecutar la instruccibn

LD pc, retFO(fp)

De manera correspondiente, el elemento que llama calculii los argurrientos uno por uno, e inserta cada uno de cllos en la pila en su posicicín apropiada antes de insertar el nuevo marco. EI element« que llama también debe guardar el fp actual en el niarco del oSpFO antes de in- sertar el nuevo marco. Después de un retorno desde el elemento llamado, el elemento qne llama descarta entonces el nuevo marco al cargar el fp con el fp antiguo. De esta forma, una Ilo- tnada a una funcibn de dos paránietros provocará la generación del cbdigo siguiente:

<código para calcular el primer arg> ST ac, frameoffset+initFO (fp)

<código para calcular el segundo arg>

ST ac, frameoffset+initFO-1 (fp)

ST fp, frameoffset+ofpFO (fp) * almacena el fp actual LDA fp frameOffset(fp) * inserta nuevo marco LDA ac,l(pc) * guarda el retorno en ac LDA pc, . . .(pc) * salto relativo a la entrada de función LD fp, ofpFO(fp) * extrae el marco actual

Cálculos de dirección Puesto que se permiten tanto variables como arreglos s~~hiridizados en el lado izquierdo de la asignación, debemos distinguir entre direcciones y valores durante Itt

compilación. Por ejemplo, en la sentencia

la expresión a [i] Iiace referencia a la direccibn de a [il, mientras &e la expresión a [i+l] hace referencia al valor de a en la localidad ¡+l. Esta distincibn se puede obtener utilizando un pürámetro isAddreas para el procedimiento cGen. Cuando este parhrnetro es verdadero, cGen genera código para calcular la dirección de una variable más que de su va- lor. En el caso de una variable simple, esto significa sumar el desplazarriiento al gp (en el caso de una variable global) o a1 fp (en el caso de una variable local) y cargar el resultado en el ac:

LDA ac, offset(fp) * * pone la dirección de la variable local en ac

En el caso de una variable de i~rreglo, esto significa sumar el valor del índice a la direccih hitse del arreglo y cargar el resultado en cl ac, conio si: describe a conlinuación.

Arreglos Los arreglos se ziigniin en 13 pila comenzando en cl desplazamiento de niarco actual y extendiéndose hacia nhajo en la memoria a f in (le incrementar el suhinttice. de la mancra que se mucstrci :I coritinnacitin:

Page 504: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

A P ~ N O I C E A I PROYECTO DE COMPIMDOR

c- dirección base del arreglo A

espacio asignado para los elementos del arreglo A en la pila

Advierta que las localidades de arreglos se calculan restando el valor del índice de la direc- ción base.

Cuan& un ;ureglo se pasa a una función, solamente se pasa la dirección base. La asigna- ción del área para los elementos base se hace sólo una vez y permanece fija durante la vida del arreglo. Los argumentos de la función no incluyen los elementos reales de un arreglo, so- lamente la dirección. De este modo, los paránietros de arreglo se convierten en parámetros de referencia. Esto causa una anomalia cuando los p h e t r o s de arreglo son referenciados den- tro de una función, puesto que deben ser tratados como si tuvieran sus direcciones base en vez de sus valores almacenados en memoria. Así, un parámetro de arreglo tiene su dirección hase calculada utilizando una operación LD en vez de una LDA.

A.5 PROYECTOS DE PROGRAMACION UTILIZANDO C- Y TM No es irracional en un curso de compiladores con duración de un semestre requerir como proyecto un compilador completo para el lenguaje C- basado en el compilador TINY que se analizó en este texto (y cuyo listado se puede encontrar en el apéndice B). Esto se puede coordinar de manera que se iniplemente cada fase del compilador a medida que se estudia la teoría asociada. Otra manera de hacerlo sería que el instructor suministrara una o más par- tes del compilador C- y pidiera a los estudiantes que completaran las partes restantes. Esto es especialmente útil cuando se tiene poco tiempo (por eiemplo en un curso trimestral) o cuando . . - . los estudiantes estarán generando código ensamblador para una máquina "real", como la Sparc o la PC (lo cual requiere más detalle durante la fase de generación de código). Intple- mentar sólo una parte del compilador C- es menos útil, puesto que entonces se restringen las interacciones entre las partes y la capacidad para probar el código. La siguiente lista de tareas separadas se suministra como una manera de facilitar las cosas. con la advertencia de que cada tarea no puede ser independiente de las otras y que probablemente es mejor que todas las tareas se termiden en orden para obtener una experiencia completa de la escritura de un compilador.

PROYECTOS A. 1. lmpiemente una utiiina de tabla de símbolos adecuada para el lenguaje C-. ~ s t o requerirá una esiructtm de tabla que incorpore informxiún de imbito, ya sea como tablas separadas vinculadas en conjunto o con un tnecanismo <le eliniinación que funcione de manera basada en pilas, como se describió en el capítulo 6.

A.Z. lmplemente un analimdor léxico de C -. ya sea a mano como un DFA o utilimndo Lex, como se describió en el capítulo 2.

A.3. Diseñe una estructura de 5rbol sintáctico para C- apropiada para la generxiún mcdiante un ün;iliz:tdor sintictico.

Page 505: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Proyectos 50 1

A.4. Implemente un analizador sintáctico de C- (esto rcquiere un analiz::dor l6xico de C-), ya sea a mano utilimndo deecendentes recursivos o mcdiantc el uso (le Yacc, como se describiú en el capítulo 4 o 5. El analirador sintáctico dehería gener:ü un árbol sint,ictico apropiado (véase el proyecto ~1.3).

A.5. lmplemente un analirador sem61itico para C -. El requerimiento principal del :tiializador. ade-

más de obtener informaciitn en la i ~ h l a de símbolos, es realizar verificaciríti de tipo en el uso de variables y fnncioncs. Como no hay apuntadores o estructuras, y el Único tipo básico es d ente- ro, los tipos que necesitan ser tratados mediante el verificador de tipo son los tipos vnid, inte- ger, itrray y function.

A.6. lmplemente u n gcneratlor de ctidigo para C-, de acuerdo con el ambiente de rjecuciiin descrito en la seccirín anterior.

Page 506: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Apéndice B

l i s l a d o del compilador TINY

/ * Archivo: main.c * / / * Programa principal para el conpilador TINY * / / * Construcción de compiladores: principios y práctica * / / * Kenneth C. Louden * / . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

/ * establece NO-PARSE a TRUE para obtener un compilador sólo de análisis laxico * /

#define NO-PARSE FALSE

/ * establece NO-ANALYZE a TRUE para obtener un compilador sólo de análisis sintáctico * /

#define NO-ANALYZE FALSE

/ * establece NO-CODE a TRUE para obtener un compilador que no * genere código * / #define NO-CODE FALSE

#include "uti1.h"

#if NO-PARSE #include "scan. h" #else Kinclude "parse.hU # i f ! NO-ANALYZE #include "ana1yze.h"

#if !NO-CODE Kinclude "cgen. h" #endif #endi£

#endi£

/ * asignación de variables globales * /

Page 507: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

listado del compilador TlNY

int lineno = 0;

FILE * source; FILE * listing; FILE * code;

/ * asignar y establecer marcas de rastreo * /

int Echosource = TRUE;

int TraceScan = TRUE; int TraceParse = TRUE; int TraceAnalyze = TRUE;

int TraceCode = TRUE;

int Error = FALSE;

main( int argc, char * argvfl )

{ TreeNode * syntaxTree; char pgmf201; / * nombre de archivo de código fuente * / if (argc != 2 )

( fprintf(stderr,"usage: %S <filename>\n",argv[Ol);

exit (1);

1 strcpy (pan, argv[ll) ; if (strchr (pgm, ' . ' ) == NULL)

strcat (pgm, " . tny") ; source = fopen(pgm, "r") ; if (source= =NULL)

{ fprintf (stderr, "File %S not found\n",pgm) ;

exit(1);

1 listing = stdout; / * enviar listado a la gantalla * / fprintf(listing,"\nTINY COMPILATION: %s\n",pgm);

#if NO-PARSE

while (getToken0 !=ENDFILE);

#else

syntaxTree = parse ( ) ;

if (TraceParse) (

fgrintf(listing,"\nSyntax tree:\nU);

printTree(syntaxTree);

#i f ! NO-ANALYZE

if ( ! Error)

( fprintf(listing,"\nBuilding Symbol Table...\nW);

buildSymtab(syntaxTree);

fprintf(listing,"\nChecking Types ... \nb'); typeCheck(syntaxTree);

fprintf(listing,"\nType Checking Finished\nS);

Page 508: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

504 APÉNDICE B 1 LISTADO DEL COMPILADO8 TiNY

if ( I Error)

( char * codefile; int fnlea = strcspn(pgm, ". " ) ;

codefile = (char * ) calloc(fnlen+4, sizeof(char)i;

strncpy(codefile,ggm,fnlen);

strcat(codefile,".tm");

code = fopen(codefile,"w");

if (code = = NULL) ( printf("Unab1e to ogen %s\nl',codefile);

exit(1);

1 codeGen(syntaxTree,codefile);

fclose (code) ;

1 #endi£

#endi£ #endi£

return O;

>

/ * Archivo: globals . h */ / * Loa tipos globales y variables para el comgilador TINY * / / * deben aparecer antes de otros archivos "include" * / / * Construcción de compiladores: principios y práctica * / / * Kenneth C. Louden * / . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

# i f nde f -GLOBALS-H- #define -GLOBALSH-

#ifndef PALSE

#define FALSE O

itendif

#ifndef TRUE #define TRUE 1

#endi£

Page 509: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del compilador TlNY

/ * MAXRESERVED = el número de palabras reservadas * / #define MAXRESERVED 8

typedef enum

/ * tokens de administración * / {ENDFILE,ERROR,

/ * palabras reservadas * / IF, THEN, ELSE,END, REPEAT,UNTIL, READ, WRITE,

/ * Tokens de caracteres múltiples * / ID, m, / * símbolos especiales * / ASSIGN,EQ,LT,PLUS,MINüS,TIMES,OVER,LPAREN,RPAREN,SEMI

1 TokenType;

extern FILE* source; / * archivo de texto de código fuente * / extern FILE* listing; / * archivo de texto de salida de listado * / extern FILE* code; / * archivo de texto de código para el simulador TM */

extern int lineno; / * número de línea fuente para el listado * /

.................................................. / * * árbol sintáctico para el análisis sintáctico **/ .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

typedef enum {StmtK,ExpK) NodeKind; typedef enum fIfK,RepeatK,AssignK.ReadK,WriteK) StmtKind;

typedef enum {OpK,ConstK,IdK) ExpKind;

/ * ExpType es utilizado para verificación de tipo * / typedef enum {void,Integer,Boolean) ExgType;

#define MAXCHILDREN 3

typedef struct treeNode

í struct treeNode * child[MAXCHILDRENJ; struct treeNode * sibling; int lineno; NodeKind nodekind;

union { StmtKind stmt; ExpKind ea;) kind;

union { TokenType op;

int val;

char * name; ) attr; ExpType tyge; / * para verificación de tipo de expresiones * /

1 TreeNode;

Page 510: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

A P ~ N D ~ C E B 1 LISTADO DEL COMPllADOR TINY

.................................................... /*********** Marcas para rastreo ************/ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

/ * EchoSource = TRUE causa que el programa fuente se * replique en el archivo de listado con números de línea * durante el análisis sintáctico * /

extern int EchoSource;

/ * TraceScan = TRUE ocasiona que la información de token sea

* impresa al archivo de listado a medida que cada token sea * reconocido por el analizador léxico * / extern int TraceScan;

/ * TraceParse = TRUE provoca que el árbol sintáctico sea * impreso en el archivo del listado en forma linealizada * (utilizando sangrías para los hijos)

* / extern int TraceParse:

/ * TraceAnalyze = TRUE provoca que la tabla de símbolos inserte * y busque para informarlo al archivo de listado * / extern int TraceAnalyze;

/ * TraceCode = TRUE causa que se escriban comentarios * al archivo de código TM a medida que se genera el código * / extern int TraceCode;

/ * Error = TRUE evita pasos adicionales si se presenta un error * / extern int Error;

#endi£

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . / * Archivo: uti1.h * / / * Funciones de utilidad para el compilador TINY */ / * Construcción de compiladores: principios y práctica * / / * Kenneth C. Louden * / . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Page 511: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del compilador TlWY

/ * E1 procedimiento printToken imprime un token * y su lexema al archivo del listado * / void printToken( TokenType, const char* ) ;

/ * La función newStmtNode crea un nuevo nodo de sentencia * para la construcción del árbol sintáctico * / TreeNode * newStmtNode(StmtKind);

/ * La función newExpNode crea un nuevo nodo de expresión * para la construcción del árbol sintáctico * / TreeNode * newExpNode(ExpKind);

/ * La función copystring asigna y crea una nueva * copia de una cadena existente * / char * copyString( char * 1;

/ * el procedimiento printTree imprime un árbol sintáctico para el * archivo de listado en el que los subárboles se indican mediante sangrías * /

void printTree( TreeNode * ) ;

/ * Archivo: uti1.c * / / * Implementación de función de utilidad * / / * para el compilador TINY * / / * Construcción de compiladores: principios y práctica * / / * Kenneth C. Louden * / . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

/ * E1 procedimiento printToken imprime un token * y su lexema al archivo de listado * / void printToken( TokenType token, const char* tokenstring )

{ switch (token) ( case IF: case THEN: case ELSE:

Page 512: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

AP~NDICE B 1 LISTADO DEL COMPllADOR TlNY

case END:

case REPEAT: caae UNTIL: case READ :

case WRITE : fprintf(listing,

'reserved word: %s\nN,tokenString);

break; case ASSIGN: fprint£(listing,":=\n"); break; caae LT: fprintf (listing, "<\n") ; break; case EQ: fprintf (listing, "=\nl) ; break; case LPAREN: fprintf (listing, "(\n") ; break;

case RPAREN: fgrintf(1isting. ")\n"); break; case SEMI: fprintf(listing,";\nn); break; case PLUS: £printf(listing,"+\n'% break; case MINUS: fprintf (listing, "-\n" ) ; break; case TIMES: fprintf(liating,"*\n"); break; case OVER: fprintf(listing,"/\nU); break; case ENDFILE: fprintf (listing, "E0F\n1') ; break; case NUM: fprintf(listing,

"NUM, val= %s\n",tokenString);

break; case ID: fprintf(listing,

"ID, name= %s\nM,tokenString); break;

case ERROR: fprintf(liating,

"ERROR: %s\n" , tokenstring) ; break;

default: / * nunca debería ocurrir * / fgrintf(listing,"Unknown token: %d\nn,token);

1

/ * La función newStmtNode crea un nuevo nodo de sentencia * para la constmcción del árbol sintáctico * / TreeNode * newStmtNodefStmtKind kind) [ TreeNode * t = (TreeNode *) malloc(sizeof(TreeNode)); int i; if (t==NüLL) fprintf(listing, "Out of memory error at line %d\nM, lineno) ;

else [

for (i=O;i<MAXCHILDREN;i++) t->child[il = NULL;

t->sibling = NULL;

Page 513: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

listado del compilador TlNY

t->nodekind = StmtK;

t->kind.stmt = kind; t->lineno = lineno;

> return t;

>

/ * La función newExpNode crea un nuevo nodo de expresión * para la construcción de árbol sintáctico * /

TreeNode * newExpNode(ExgKind kind) { TreeNode * t = (TreeNode * ) malloc(sizeof (TreeNode)); int i; if (t==NULL) fprintf(listing,"out of memory error at line %d\n",lineno);

else (

for (i=O;i<MAXCHILDREN;i++) t->child[il = NULL; t->sibling = NULL; t-znodekind = ExpK; t->kind.exp = kind; t->lineno = lineno; t->type = Void;

1 return t;

1

/ * La función copystring asigna y hace una nueva * copia de una cadena existente * / char * copyString(char * S) { int n; char * t; if (s==NULL) return NULL; n 3 strlen(s)+l; t = malloc(n); if (t==NüLL) fgrintf(listing,"Out of memory error at line %d\nW,lineno);

else strcpy(t,s); return t;

1

/ * printTree utiliza la variable indentno para * almacenar el número actual de espacios para sangría * / static indentno = 0;

/ * macros para incrernentar/decrementar la sangría * /

Page 514: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

510 APÉNDICE B 1 LISTADO DEL COi4PllADOR TlNY

#define INDENT indentno+=2

#define UNINDENT indentno-=2

/ * printSpaces realiza las sangrías mediante la impresión de espacios * / static void printSpaces(void) ( int i; for (i=O;iiindentno;i++) fprintf (listing," " ) ;

1

/ * el procedimiento printTree imprime un árbol sintáctico al * archivo de listado utilizando las sangrías para indicar los subárboles * / void printTree( TreeNode * tree )

( int i; INDENT; while (tree ! = NüLL) (

printSpaces0; if (tree->nodekind==StmtK) { switch (tree->kind.stmt) {

case IfK: fprintf (listing,"If\n"l;

break; case RepeatK:

fprintf(listing,"Repeat\n'');

break; case AssignK: fgrintf(listing,"Assign to: %s\nT',tree->attr.name);

break; case ReadK: fprintf(listing,"Read: %s\n",tree->attr.name);

break; case WriteK: fprintf (listing, "Write\n8) ; break;

default : fprintf (listing, "Unknown ExpNode kind\n8' ) ;

break;

1 1 else if (tree->nodekind==ExpK) { switch (tree->kind.exp) (

case OpK: fprintf (listing, "Qp: " ) ;

printToken(tree->attr.op, ' ' \ O 0 ' ) ;

break; case ConstK:

Page 515: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del compilador TINY

f~rintf(listin~,~const: %d\nm,tree->attr.val); break;

case IdK: fprintf(listing,"Id: %s\nu,tree->attr.name); break;

default : fprintf (listing, "Unknown ExpNode kind\nl') ; break i

1 f else fprintf(listing,"Unknown node kind\nm); for (i=O;i<MAXCHILDREN;i++)

print~ree(tree-xhildtif); tree = tree->sibling;

f UNINDENT;

1

........................................................................

/ * Archivo: scan.h * / / * La interfaz del analizador léxico para el compilador TINY * / / * Construcción de compiladores: principios y práctica * / / * Kenneth C. Louden * / . .....................................................................

/* MAXTOKENLEN es el tamaiío máximo de un token * / #define MAXTOKENLEN 40

/ * el arreglo tokenstring almacena el lexema de cada token * / extern char tokenString[MAXTOKENLEN+ll;

/ * la función getToken devuelve el * token siguiente en el archivo fuente * / TokenType getToken(void);

......................................................................

/ * Archivo: scan-c * / / * La implementación del analizador léxico para el compilador TINY */

603 / * Construcción de compiladores: principios y práctica * /

Page 516: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

APÉNDICE 8 1 LISTADO DEL COHPllAOOR TlNY

/ * Kenneth C. Louden * / . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

/ * estados en analizador léxico DFA * / typedef enum

( START,INASSIGN,INCOMMENT,INNUM,INID,DONE f StateType;

/ * lexema de identificador o palabra reservada * / char tokenStringIMAXTOKENLEN+l];

/ * BUFLEN = longitud del buffer de entrada para las líneas del código fuente * /

#define BUFLEN 256

static char 1ineBufEBUFLENI; / * mantiene la línea actual * / static int linepos = 0; / * posición actual en LineBuf * / statlc int bufsize = 0; / * tamaño actual de la cadena del buffer * /

/ * getNextChar obtiene el siguiente carácter distinto del blanco de lineBuf leyendo en una nueva línea si lineBuf está agotado * /

static char getNextChar(void) f if ( ! (linepos c bufsize))

( lineno++; if (fgets(lineBuf,BUFLEN-1,source)) ( if (EchoSource) fgrintf(listing,"%46: %s",lineno,lineBuf);

bufsize = strlen(1ineBuf); linepos = O:

return lineBuf[linepos++l;

1 else return EOF;

f else return lineBuf[lineposttl;

?

/ * ungetNextChar reajusta un carácter en lineBuf * /

static void ungetNextChar(void) ( linepos-- ;}

/ * tabla de búsqueda de palabras reservadas * / atatic struct

Page 517: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del compilador TlNY

( char* str;

TokenType tok; 1 reservedwords [MAXRESERVEDI

= ffUif",IF}, {"thenn,THEN}, {"else",ELSE}, {"end",ENDI, ("repeat",REPE~~}, ("until",üNTILI, f"readl'. READI, f "write" ,WRITE) ) ;

/ * busca un identificador para ver si es una palabra reservada * / / * utiliza búsqueda lineal * / static TokenType reaervedLookup (char * a) f int i; for (i=O;i<MAXRESERVED;i++)

if (!strcmp(s,reservedWords[il.str~) return reservedWords[il.tok;

return ID;

3

/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * e 1

/ * la función principal del analizador léxico * / . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . / * la función getToken devuelve el * token siguiente en el archivo fuente * / TokenType getToken(void) ( / * índice para almacenamiento en tokenstring */

int tokenStringIndex = 0; / * mantiene el token actual que se devolverá * / TokenType currentToken; / * estado actual - siempre comienza en START * / StateType state = START; / * marca para indicar grabar en tokenstring * / int save; while (state != DüNE)

( char c = getNextChar0; save = TRUE;

switch (state) ( case START:

if (isdigit (c) ) atate = INNCiM;

else if (isalpha(c)) state = INID;

else if (c == ' : ' )

state = INASSIGN;

else if ((c == ' ' ) I I (C = = '\t') I I (C == save = FALSE;

else if (c = = ' ( ' )

( save = FALSE;

Page 518: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

APENDICE B 1 LISTADO DEL COBPILADOR TlNY

state = INCOMMENT;

1 else 1 s ta te = DONE;

s w i t c h ( c )

( case EOP: save = FALSE;

c u r r e n t T o k e n = ENDFILE;

break;

case ' = ' : c u r r e n t T o k e n = EQ;

break;

case ' < ' : currentToken = LT;

b r e a k ;

case ' + ' : c u r r e n t T o k e n = PLUS;

break;

case ' - ' : c u r r e n t T o k e n = MINUS;

break;

case ' * ' : c u r r e n t T o k e n = TIMES;

break;

case ' / ' : currentToken = OVER;

break;

case ' ( ' : c u r r e n t T o k e n = LPAREN;

break; case ' ) ' :

c u r r e n t T o k e n = RPAREN;

b r e a k ;

case ';' : c u r r e n t T o k e n = SEMI;

break; default :

c u r r e n t T o k e n = ERROR;

break;

1 1 break;

case INCOMMENT: save = PALSE;

if ( C == ' 1 ' ) state = START;

b r e a k ; case INASSIGN:

Page 519: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del compilador TlNY

state = DONE; if (C = = ' a ' )

CurrentToken = ASSIGN; else { / * respaldo en la entrada * / ungetNextChar0; save = FALSE; currentToken = ERROR;

1 break;

case INNüM: if ( ! isdigit (c) ) { / * respaldo en la entrada * / ungetNextChar0; save S FALSE; state = DONE; currentToken = NUM;

1 break;

case INID: if (!isalpha(c)) ( / * respaldo en la entrada * / ungetNextChar0; save S FALSE; state = DONE; currentToken = ID;

1 break;

case DONE: default: / * nunca debería ocurrir * / fprintf (listing, "Scanner Bug: state= %d\n". state) ; state = DONE; currentToken = ERROR; break;

1 if ((save) && (tokenStringIndex <= MAXTOKENLEN)) tokenString[tokenStringIndex++l = c;

if (state == DONE) ( tokenString[tokenStringIndex] = ' \ O ' ;

if (currentToken == ID) currentToken = reservedLookup(tokenString);

1 1 if (TraceScan) {

fprintf (listing, "\t%d: ", lineno) ; printToken(currentToken,tokenString);

3

Page 520: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

APÉNDICE B 1 LISTADO DEL COMPIMDOR TINY

return currentToken; } / * fin de getToken * /

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . / * Archivo: parse.h * / / * La interfaz del analizador sintáctico para el compilador TINY * / / * Construcción de compiladores: principios y práctica * / / * Kenneth C. Louden * / ........................................................................

/ * La función parse regresa el * árbol sintáctico recién construido * / TreeNode * parse(void);

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . / * Archivo: parse.c * / / * La implementación del analizador sintáctico para el compilador TINY */ / * Construcción de compiladores: principios y práctica * / / * Kenneth C. Louden * / . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

#include "globa1s.h" #include "uti1.h" #include "scan. h" #include "parse. h"

static TokenType token; / * mantiene el token actual * /

/ * prototipos de función para llamadas recursivas */ static TreeNode * stmt-sequence(void); static TreeNode * statementívoid); static TreeNode * if-stmttvoid); static TreeNode * repeat. stmt (void) ; static TreeNode * assign-stmt(v0id); static TreeNode * read-stmt(void); static TreeNode * write-stmtfvoid); static TreeNode * exp(void); static TreeNode * simple-exp(void); static TreeNode * tenn(void); static TreeNode * factor(void);

Page 521: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

listado del compilador TINY

static void syntaxError(char * message) { fprintf(listing,"\n>>> " ) ;

fprintf (listing, "Syntax error at line %d: %S", lineno,message) ; Error = TRUE;

1

static void match(TokenType exgected) { if (token == exgected) token = getToken0; else {

syntaxError("unexpected token - > " ) ;

printToken(token,tokenString); fgrintf(listing," " ) ;

1 1

TreeNode * stmt-sequence(void) { TreeNode * t = atatemento; TreeNode * p = t; while ((token!=ENIJFILE) 6& (tokenl=END) &&

(token!=ELSE) && (token!=UNTIL)) t TreeNode * q; match(SEM1); q = statementoi if (q!=NULL) {

if (t=sNULL) t = p = q; else / * ahora p ya no puede ser NULL * / { p-zsibling = q; P = 9;

1 l

1 return t;

1

TreeNode * statement(void) { TreeNode * t = NULL: switch (token) {

case IF : t if-stmt O ; break; case REPEAT : t = repeat-stmt O ; break; case ID : t S assign-stmt0; break; case READ : t = read-stmt0; break; case WRITE : t = write-stmt0; break; default : syntaxError("unexgected token -> " ) ;

printToken(token,tokenString); token E getTokeni ) ; break;

1 / * fin del case * /

Page 522: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

APENDICE B 1 LISTADO DEL COMPllADOR TlNY

974 return t;

975 1 976 977 TreeNode * if-stmtfvoid) 978 { TreeNode * t = newStmtNode(1 fK) ;

979 match(1F);

980 if (t!=NüLL) t->child[O] = exp0; 981 match(THEN);

982 if (t !=NüLL) t->child[ll = stmt-sequence() ;

985 if (t ! =NüLL) t-xhildf 21 = stmt-sequence ( i ; 986 1. 987 match(END);

988 return t;

989 1 990 991 TreeNode * regeat-stmt (void) 992 ( TreeNode * t newStmtNode(RepeatK);

993 match(REPEAT); 994 -if (t !=NüLL) t-xhild[OI = stmt-sequence ( ) ; 995 match (UNTIL) ;

996 if (t!=NüLL) t->child[ll = exp0; 997 return t;

998 1 999 1000 TreeNode * assign-stmt (void) 1001 ( TreeNode * t = newStmtNode(AssignK);

1007 return t; 1(1(18 1 1 tM9 1010 TreeNode * read-stmt (void) 1011 { TreeNode * t = newStmtNode(ReadK) ; 1012 match (READ) ;

1013 if ( (t!=NüLL) && (token==ID) )

1014 t->attr.name = copyString(tokenString); 1015 match(1D) ;

1016 return t;

1017 )

1018 1019 TreeNode * write-stmt (void) 14120 { TreeNode * t = newStmtNode(WriteK);

Page 523: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del compilador TlNY

1021 match (WRITE) ;

1022 if (t!=NULL) t->child[O] =exPo; 1023 return t;

1024 1 1025 1026 TreeNode * exp(void) 1027 ( TreeNode * t = aimple-exp(); 1028 if ( (token==LT) I I (token==EQ) ) (

1029 TreeNode * p = newExpNode(0pK); 1030 if (p!=NULL) (

1031 p->child[Ol = t; 1032 p->attr.op = token;

1033 t = p; 1034 I)

1035 match(token); 1036 if (t!=NULL) 1037 t->child[l] S simpleex!?( ) ;

1038 1 1039 return t;

1040 )

1041 1042 TreeNode * simple-exp(v0id) 1043 ( TreeNode * t = termo; 1044 while ( (token= =PLUS) I l (token==MIN[fS) ) 1045 ( TreeNode * p = newExpNodeíOpK); 1046 if (p!=NULL) (

1047 p->child[OI = t; 1048 p->attr.op = token; 1049 t = p;

1050 match(token); 1051 t->child[ll = termo; 1052 1 1053 > 1054 return t; 1055 1 1056 1057 TreeNode * termívoid) 1058 { TreeNode * t factor0; 1059 while ( (token==TIMES) I l (token==OVER) ) $060 { TreeNode * p = newExpNodeiOpK); 1061 if (p!=WLL) (

1062 p->child[Ol 3 t; 1063 p->attr.op = token; 1064 t = p; 1065 match(token) ; 1066 p->child[l] = factor(); 1067 >

Page 524: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

APENDICE B 1 LISTADO DEL COMPllADOR lINY

1068 1 1069 return t;

1070 1 1 O7 1 1072 TreeNode * f actor (void) 1073 ( TreeNode * t = NüLL;

1074 switch (token) f

1 075 case NüM :

1076 t = newExpNode (ConstK) ;

1077 if ( (t! =MULL) && f token==NüM) )

1078 t->attr.val = atoi(tokenString);

1079 match(NOM) ;

1080 break;

1081 case ID :

1082 t = newExpNode(1dK);

1083 if ((t!=NüLL) && (token==iD))

1084 t->attr.name = copyString(tokenString);

1085 match(1D);

1086 break;

1087 case LPAREN :

1 O88 match(LPAREN);

1089 t = exp0;

1090 match(RPAREN);

1091 break;

1092 default:

1093 8yntaxError ( "unexpected token - > " ) ;

1094 printToken(token,tokenString);

1095 token = getToken0;

1096 break;

1097 1 1098 return t;

1699 1 I 100 1101 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1102 / * la función principal del analizador sintáctico * / 1103 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1104 / * La función parse devuelve el 1105 * árbol sintáctico recién construido 1106 * / 1107 TreeNode * parse(void) 1108 { TreeNode * t; i l O Y token = getToken0;

1110 t = stmt-sequence() ; i l l l i£ (token!=ENDFILE)

1112 syntaxError("Code ends before £ile\n");

Page 525: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

h a d o del compilador TlNY

1113 return ti

1114 1

1150 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1151 / * Archivo: 8ymtab.h * / 1152 / * Interfaz de la tabla de símbolos para el compilador TINY * / 1153 / * (permite solamente una tabla de símbolos) * / 1154 / * Construcción de compiladores: principios y práctica * / 1155 / * Xenneth C . Louden * / 1156 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1157 1158 #ifndef -SnlTAB-H- t59 #define -SYMTAB-E-

1160 1161 / * E1 procedimiento st-insert inserta números de línea y 1162 * localidades de memoria en la tabla de símbolos 1163 * loc localidad de memoria que se inserta únicamente

1164 * la primera vez, de otro modo se ignora 1165 * / S166 void st-insertf char * name, int lineno, int loc ) ;

S167 1168 / * La función st-lookup devuelve la localidad de 1169 * memoria de una variable o -1 si no se encuentra 1170 * / 1171 int st-lookup ( char * name ) ;

1172 1173 / * E1 procedimiento printSymTab imprime un listado

1174 * formateado del contenido de la tabla de símbolos 1175 * al archivo de listado 1176 * / 1177 void printSymTab(F1LE *** listing);

1178 1179 #endif

1201 /* Archivo: 8ymtab.c * / 1202 / * Implementación de tabla de símbolos para el compilador TINY * / 1203 / * (permite solamente una tabla de símbolos) * / 1204 / * La tabla de símbolos es implementada como una * / 1205 / * tabla de dispersión encadenada * / 1206 / * Construcción de compiladores: principios y práctica * / 12W / * Kenneth C. Louden * / 120) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1209 1210 ttinclude <atdio.h>

Page 526: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

522 APÉNDICE B / LISTADO DEL COHPllADOR TlNY

1213 #include "symtab.h0 1214 1215 / * SIZE es el tamaño de la tabla de dispersión * / 1216 #define SIZE 211

1217 1218 / * SHIFT es la potencia de dos empleada como multiplicador 1219 en la función de dispersión * / 1220 #define SHIFT 4

1221 1222 / * la función de dispersión * / 1223 static int hash ( char * key )

1224 ( int temp = 0; 1225 int i = 0; 1226 while (key[il 1 = '\O') 1227 f temp = ((temg << SHIFT) + key[il) % SIZE; 1228 ++i; 1229 }

1230 return temp; 1231 }

1232 1233 / * la lista de números de línea del 1234 * código fuente en el que es referenciada una variable 1235 * / 1236 typedef struct LineListRec 1237 { int lineno; 1238 struct LineListiiec * next; 1239 } * LineList; 1240 1241 / * E1 registro en las listas de cubetas para 1242 * cada variable, incluyendo nombre, 1243 * localidad de memoria asignada, y 1244 * la lista de números de línea en los que 1245 * aparece en el código fuente 1246 * / 1247 typedef struct BucketListRec 1248 f char * name; 1249 LineList lines; j

1250 int memloc; / * localidad de memoria para variable * / 1251 struct BucketListRec * next; 1252 } * BucketList; 1253 1254 / * la tabla de dispersión * / 1255 static ~ucketList hashTable [SIZE] ; 1256 1257 / * E1 procedimiento st _insert inserta números de línea y

Page 527: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

listado del compilador TlNY

1258 * localidades de memoria en la tabla de símbolos 1259 * loc = localidad de memoria que se inserta solamente la 1260 * primera vez, de otro modo se ignora 1261 * / 1262 void st-insert( char * name, int lineno, int loc )

1263 ( int h = hash(name); 1264 BucketList 1 S hashTableth1; 1265 while ((1 != NULL) && (strcmp(name,l->name) != 0)) 1266 1 = 1->next; 1267 if (1 == mLL) / * variable que todavía no está en la tabla * / 1268 ( 1 I (BucketList) malloc(sizeof(struct BucketListRec)); 1269 1->name = name; 1270 1->lines = (LineList) malloc(sizeof(struct LineListRec)); 127 1 1->lines->lineno = lineno; 1272 1->memloc = loc; 1273 1->lines->next = NULL; 1274 1->next = hashTable [hl ; 1275 hashTableih1 = 1; )

1276 else / * está en la tabla, de modo que sólo se agrega el número de línea*/ 1277 ( LineList t = 1->lines; 1278 while (t->next ! = NULL) t = t->next; 1279 t->next ;. (LineList) malloc(sizeof(struct LineListRec)); 1280 t->next->lineno = lineno; 1281 t->next->next = NULL; 1282 > 1283 } / * de st-insert * / 1284 1285 / * La función st-lookup devuelve la localidad de 1286 * memoria de una variable o -1 si no se encuentra 1287 * / 1288 int st-lookup ( char * name )

1289 { int h = hash(name) ;

1290 BucketList 1 = hashTableEh1; 1291 while ((1 != NULL) && (strcmp(name,l->name) != 0)) 1292 1 = 1->next; 1293 if (1 == NULL) return -1; 1294 else return 1- >memloc;

1295 1 1296 1297 / * E1 procedimiento printSymTab imprime un listado 1298 * formateado del contenido de la tabla de símbolos 1299 * para el archivo de listado 1300 * / 1301 void grintSymTab(F1LE * listing) 1302 ( int i; 1303 fprintf (listing, "Variable Name Location Line ~umbers\n") ; 1304 fprintf (listing, " - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \nn' ) ;

Page 528: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

APÉNDICE B 1 LlSTADO DEL EOIZPIMDOR TiWY

Eor (isO; icSIZE; +ti) ( if (hashTableIi1 != NULL)

{ SucketList 1 = hashTableli1; while (1 ! a NULL) [ LineList t = 1->lines; fprlntf(listing,"%-14s ",l-zname); Eprintf(listing,"%-8d ",l->memloc); while (t != NULL)

{ fgrintf(listing,"%4d ",t->lineno); t = t->next;

1 fprintf(listing,"\n"): 1 = 1->next;

1. i

1350 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1351 / * Archivo: ana1yze.h * / 1352 / * Interfaz del analizador semántico para el compilador TINY * / 1353 / * Construcción de compiladores: principios y práctica * / 1354 / * Kenneth C. Louden * / 1355 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1356 1357 #ifndef -ANALYZE-H- 1358 #define -ANALYZñ-H- 1359 1360 / * La función buildsymtab construye la tabla de 1361 * simbolos mediante recorrido preorden del árbol sintáctico 1362 * / 1363 void buildsymtab (TreeNode *) ;

1364 1365 / * El procedimiento typecheck realiza verificación de tipo 1366 * mediante un recorrido postorden del árbol sintáctico 1367 * / 1368 void typecheck (TreeNode * ) ;

1369 1370 #endif

1J(]0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1401 / * Archivo: ana1yze.c * / 1402 / * Implementación del analizador semantico * / 1403 / * para el ccmpilador TINY * / 1404 / * Construcción de compiladores: principios y práctica * /

Page 529: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del compilador TlNY

/ * Kenneth C. Louden * / . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

/ * contador para localidades de memoria de variable * / static int location = 0;

/ * El procedimiento traverse es una rutina * de recorrido del árbol sintáctico recursiva genérica: * aplica preProc en preorden y postProc * en postorden para el árbol al que apunta mediante t * / static void traverse( TreeNode * t,

void ( * preProc) (TreeNode * ) , void ( * postProc) (TreeNode * ) )

{ if (t != NüLL) { preProc(t);

{ int i; for (i=0; i < PIAXCHLLDREN; i++) traverse(t->child[il,preProc,poetProc);

1 postProc(t); traverse(t->sibling,preProc,postProc);

}

1

/ * nullproc ea un procedimiento que no hace nada para * generar recorridos solamente greorden o solamente poatorden * a partir de traverse * /

static void nullProc(TreeNode * t) f if (t==NüLL) return; else return;

>

/ * El procedimiento insertNode inserta * identificadores almacenados en t dentro de * la tabla de símbolos * / static void insertNode( TreeNode * t) { switch (t->nodekind)

{ case StmtK: switch (t->kind.stmt) I case AssignK:

Page 530: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

526 AP~NDICE B 1 LISTADO DEL COHPllADOR TlNY

1452 case ReadK: 1453 / * todavía no está en la tabla, de modo que se trata como nueva definición * / 1454 if (st-lookup(t->attr.name) == -1) 1455 st-insert(t->attr.name,t->lineno,location++);

1456 else 1457 / * ya está en la tabla, así que se ignora la ubicación, 1458 se agrega el número de línea de uso solamente * / 1459 st-insert(t->attr.name,t->lineno,O);

1460 break; 1461 default: 1462 break; 1463 1 1464 break; 1465 case ExpK: 1466 switch (t->kind.exp) 1467 f case IdK: 1468 / * todavía no está en la tabla, de modo que se trata como nueva definición */ 1469 if (st-lookup(t->attr.name) == -1) 1470 st-ínsert(t->attr.name,t->lineno,location++);

1471 else 1472 / * ya está en la tabla, así que se ignora la ubicación, 1473 se agrega el número de línea de uso solamente * / 1474 st-insert(t->attr.name,t->lineno,O);

1475 break; 1476 de£ ault : 1477 break; 1478 f 1479 break;

1480 default : 1481 break; 1482 }

1483 f

1484 1485 / * La función buildsymtab construye la tabla de símbolos 1486 * mediante recorrido preorden del árbol sintáctico 1487 */ 1488 void buildSymtab(TreeNode * syntaxTree) 1489 { traverse(syntaxTree,insertNode,nullProc);

1490 if (TraceAnalyze) 1491 { fprintf (listing, "\nSymbol table:\n\nW) ;

1492 printSymTab(1isting); 1493 )

1494 }

1495 1-196 static void typeError(TreeN0de * t, char * message) 1497 { fprintf (listing, "Type error at line %d: %s\nt', t->lineno,message) ; 1498 Error = TRUE;

Page 531: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del compilador TlNY

1499 1 1500 1501 / * E1 procedimiento checkNode efectúa 1502 * verificación de tipo para un solo nodo de árbol 1503 * / 1504 static void checkNode(TreeN0de * t) 1505 ( switch (t->nodekind)

( case ExpK: switch (t->kind.exp) { case OpK:

if ((t->child[O]->type != Integer) I I (t->child[l]->type 1 = Integer))

typeError (t, "üp applied to non-integer" ) ;

if ((t->attr.og == EQ) l I (t->attr.op == LT)) t->type = Boolean;

else t->type = Integer;

break; case ConstK: case IdK: t->type = Integer; break;

def ault : break;

f break;

case StmtK: switch (t->kind.stmt) ( case IfK:

if (t->child[O] ->type a = Integer) typeError(t->child[O],"if test is not Boolean");

break; case AssignK: if (t->child[Ol ->type != Integer) type~rror(t->child [ol,"asslgnment of non-integer value") ;

break; case WriteK: if (t->child[Ol->type != Integer) typeError(t->child[O], "write of non-integer value") ;

break; case RegeatK: if (t->child[ll ->type = = Integer) typeError(t->child[l] , "rapeat test is not Boolean") ;

break; default: break;

1

Page 532: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

AF~NDIEE B 1 LISTADO DEL FOMPILADOR TINY

1546 break; 1547 default: 1548 break;

1549 1550 1 1551 1 1552 1553 / * El procedimiento typeCheck realiza verificación de tipo 1554 * mediante un recorrido postorden de árbol sintáctico 1555 * / 1556 void tygeCheck(TreeNode * syntaxTree) 1557 { traverse(syntaxTree,nullProc,checkNode);

1558 1

1601 / * Archivo: c0de.h * / 1602 / * Utilidades de emisión de código para el compilador TINY * / 1603 / * e interfaz para la máquina TINY * / 1604 / * Construcción de compiladores: principios y práctica * /

1608 #i f nde f -CODE-H-

1609 itdef ine -CODEH

1610 1611 / * pc = contador de programa * / 1612 #define pc 7

1613 1614 / * mp = al "apuntador de memoria" apunta 1615 * a la parte superior de la memoria (para almacenamiento temporal) 1616 * / 1617 #define mp 6

1618 1619 / * gp = el "apuntador global" apunta 1620 * a la parte inferior de la memoria para 1621 * almacenamiento de variable (global) 1622 * / 1623 #define gp 5 1624 1625 / * acumulador * / 1626 #de£ ine ac O 1627 1628 / * aegrindo acumulador * / 1629 #define acl 1 1630 1631 / * utilidades de emisión de código * /

Page 533: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del compilador TlNY

1632 1633 / * El procedimiento emitRO emite una 1634 * instrucción TM sólo de registro 1635 * op = el opcode 1636 * r I registro objetivo 1637 * S = ler. registro fuente 1638 * t = 2do. registro fuente 1639 * c = un comentario para ser impreso ai TraceCode es TRUE

1640 * / 1641 void emitRO( char *op, int r, int S, int t, char *c); 1642 1643 / * E1 procedimiento emitRM emite una instrucción TM 1644 * de registro-a-memoria 1645 * op = el opcode tú46 * r = registro objetivo 1647 * d = el desplazamiento 1648 * a = el registro base 1649 * c = un comentario para imprimirse si TraceCode es TRUE 1650 * / 1651 void emitRM( char * op, int r, int d, int s, char *c); 1652 1653 / * La función emitSkip salta las localidades de código "howMany" 1654 * para reajuste posterior. También 1655 * devuelve la posición del código actual 1656 * / 1657 int emitSkip( int howMany); 1658 1659 / * El procedimiento emitBackup respalda a 1660 * loc = una localidad previamente saltada 1661 * / 1662 void emitBackup( int loc) ; 1663 1664 / * El procedimiento emitRestore restablece la posicidn 1665 * del código actual a la más alta 1666 * posición no emitida previamente 1667 * / 1668 void emitRestore(void) ; l a 9 1670 / * El procedimiento emitcomment imprime una línea de comentario 1671 * con el comentario c en el archivo de código 1672 * / 1673 void emitcommentí char * c ) ;

1674 1675 / * El procedimiento emitKAbs convierte una referencia absoluta 1676 * en una referencia relativa al pc cuando se emite una 1677 * instrucción TM de registro a memoria 1678 * op = el opcode (código operacional)

Page 534: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

APENDICE B 1 LISTADO DEL CONPllADOR TINY

* r = registro objetivo * a = la localidad absoluta en memoria

* c = un comentario para imprimirse si TraceCode es TRUE * / void emitRM-Abs( char *op, int r, int a, char * c);

/ * Archivo: c0de.c * / / * Implementación de utilidades de emisión de código TM * / / * para el compilador TINY * / / * Construcción de compiladores: principios y práctica * / / * Kenneth C. Louden * / ...........................................................

#include "globals . h" #include "code .h"

/ * Número de localidad TM para la emisión de la instrucción actual * / static int emitLoc a O ;

/ * Localidad TM más alta emitida hasta ahora Para su uso en conjunto con emitSkip, emitBackup y emitRestore * /

static int highEmitLoc = 0;

/ * El procedimiento emitcomment imprime una línea de comentario * con comentario c en el archivo de código * / void emitComent( char * c ( if (TraceCode) fgrintf(code,"* %s\n",c);l

/ * El procedimiento emitRO emite una * instrucción TM sólo de registro * op = el opcode * r = registro objetivo * s = ler. registro fuente * t = 2do. registro fuente * c = un comentario para imprimirse si TraceCode es TRUE

* / void emitRO( char *op, int r, int S, int t, char *c) { fprintf (code,"%3d: %5a %d,%d,%d ",emitLoc++,op,r,s, t); if (TraceCode) fprintf(code,"\t%s",c) ; fprintf(code,"\n") ; if (highEmitLoc < emitLoc) highEmitLoc = emitLoc ;

Page 535: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del compilador TlNY

/ * Procedimiento emitRM emite una instrucción TM * de registro-a-memoria * op = el opcode r = registro objetivo

* d = el desplazamiento * s = registro base * c = un comentario para imprimirse si TraceCode es TRUE

* / void emitRM( char * op, int r, int d, int S, char *c) f fprintf(code,"%3d: %5s %d,%d(%d) ",emitLoc++,op,r,d,s); if (TraceCode) fprintf (code, "\t%sl', c) ; fprintf (code, "\n") ; if (highEmitLoc < emitLoc) highEmitLoc = emitLoc ;

1 /* de emitRM * /

/ * la función emitSkip salta las localidades del código "howMany" * para un reajuste posterior. También * devuelve la posición del código actual * / int emitSkip( int howMany) f int i = emitLoc;

emitLoc += howMany ; if (highEmitLoc < emitLoc) highEmitLoc = emitLoc ; return i;

1 / * de emitSkip * /

/ * El procedimiento emitBackup respalda a * loc = una localidad previamente saltada

* / void emitBackup( int loc) f if (loc > highEmitLoc) emitcomment ( "BUG in emitBackupl' ) ; emitLoc = loc ;

) / * de emitBackup */

/ * E1 procedimiento emitRestore restablece la * posición de código actual a la posición más alta * no emitida previamente * / void emitRestore(v0id) { emitLoc = highEmitLoc;)

/ * El procedimiento emitRM-Abs convierte una referencia absoluta * a una referencia relativa al pc cuando se emite una * instrucción TM de registro-a-memoria * op = el opcode

Page 536: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

AP~NQICE B 1 LISTADO DEL COMPIIADOR TlNY

* r = registro objetivo * a = la localidad absoluta en memoria

* c = un comentario para imprimirse si TraceCode es TRUE

* / void emitRM-Abs( char *op, int r, int a, char * c) (: Epsintf(code,"%3d: %5s %d,%d(%d) ",

emitLoc,op,r,a-CemitLoc+l),pc);

++emitLoc ; if (TraceCode) fprintf(code,"\t%s",c) ; fprintf(code,"\nm) ; if ChighEmitLoc < emitLoc) highEmitLoc = emitLoc ;

j / * de emitRM-Abs * /

/ * Archivo: cgen.h * / / * La interfaz del generador de código para el eompilador TINY * / / * Construcción de compiladores: principios y práctica * / / * Kenneth C. Louden * / . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

#ifndef -CGEN-H- #de£ ine -_CGEN-H-

/ * E1 procedimiento codeGen genera código hacia un * archivo de cOdigo por recorrido del árbol sintáctico. El * aegundo parámetro es el nombre de archivo * del archivo de código, y se utiliza para imprimir el * nombre de archivo como un comentario en el archivo de código * /

void codeGen(TreeNode * syntaxTree, char * codefile);

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . / * Archivo: cgen.c * / / * La implementación del generador de código * / / * para el compilador TINY * / / * (genera código para la máquina TM) * / / * Construcción de compiladores: principios y practica * / / * Renneth C. Louden * / . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

#include "globals. h" #include "symtab.hm #include "code. h"

Page 537: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

listado del compilador TlNY

/ * tmpoffset es el desplazamiento de memoria para elementos temporales Se decrementa cada vez que un elemento temporal es almacenado, y se incrementa cuando se carga de nuevo

* / static int tmpoffset = 0;

/ * prototipo para generador de código recursivo interno * / static void cGen (TreeNode * tree);

/ * El procedimiento genstmt genera código para un nodo de sentencia * / static void genStmt( TreeNode * tree) I TreeNode * pl, * p2, * P3; int savedLocl,savedLoc2,~urrentLcc; int loc; switch (tree->kind.stmt) i

case IfK : if (TraceCode) emitComment("-> if " ) ;

pl = trae-zchild[Ol ;

p2 = tres->childlll ; p3 = tree->child[21 ; / * genera código para expresión de prueba * / cGen(p1) ; savedLoc1 = emitSkip(1) ; emitComment("if: jump to else belongs here"); / * recursividad en la parte then * / cGen(p2) ; savedLoc2 = emitSkip(1) ; emitComment("if: jump to end belongs here"); currentLoc = emitSkip(0) ; emitBackup(aavedLoc1) ; emitRM-Abs("JEQ",ac,currentLoc,"if: jmp to else"); emitRestore0 ; /* recursividad en la parte else */ cGen (p3) ; currentLoc = emitSkip(0) ; emitBackup(savedLoc2) ; emitRM-~bs ("LDA'~,Pc, currentloc, "jmp to end" ) ; emitRestore ( ) ; if (TraceCode) emitCcmment("i- if") ; break; / * if-k * /

case RepeatK: if (TraceCode) emitcomment ( O - > regeat" ) ;

p l = tree->child[OI ;

Page 538: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

APÉNDICE B 1 LISTADO DEL CORPllADOR TlNY

p2 = tree->child[ll ; savedLocl = emitSkip(0); emitcomment("repeat: jump after body comes back here"); / * genera código para el cuerpo * / cGen (pl ) ; / * genera código para prueba * / cGen(p2) ;

emitRMRMAbs("JEQ",ac,savedLocl,"repeat: jmp back to body"); if (TraceCode) emitComment("<- repeat") ; break; / * del repeat * /

case AssignK: if (TraceCode) emitComment("-> asaign") ; / * genera código para rhs * / cGen(tree->childiOl) ;

/ * ahora almacena valor * / loc = st-lookup(tree->attr.name); ernitRM("ST", ac, loc, g g , <'assign: store value") ; if (TraceCode) emitcomment ( "< - aasign" ) ; break; * / de assign-k * /

case ReadK: emitRO("INt', ac, O, O, "read integer value") ; loc = st-lookup(tree->attr.nama); emitRM( "ST" , ac, loc, gp, "read: store value") ; break;

case WriteK: / * genera código para la expresión a escribir * / cGen(tree->child[OI); / * ahora la extrae * / emitRO("OUT",ac,O,O,"write ac");

break; default :

break;

1. 1994 } / * de genStmt * / 1995 1996 / * El procedimiento genExg genera código en un nodo de expresión * / 1997 static void genExp( TreeNode * tree) 1998 { int loc; 1999 TreeNode * pl, * p2; 2000 switch (tree->kind.exp) { 2001 7002 case ConstK : 2003 if (TraceCode) emitcomment ( " - > Const" ) ; 2004 / * genera código para cargar constante entera utilizando LDC * / 2005 emitRM("LDCn,ac,tree->attr.val,O,"load const");

Page 539: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del compilador TlNY

if (TraceCode) emitComment("<- Const'') ;

break; / * de ConstK * /

case IdK :

if (TraceCode) emitcomment ("-2 Id") ;

loc = st_lookup(tree->attr.name); emitRM ( "LD" , ac, loc, gp, "load id value") ; if (TraceCode) emitcomment ( " < - Id") ;

break; / * de IdK * /

case OpK : if (TraceCode) emitComment("-> Op") ;

pl = tree->child[OI; p2 = tree->child[ll ; / * genera código para ac = argumento izquierdo * / cGen(p1) ; / * genera código para insertar operando izquierdo * / emitRM("STW,ac, tmpoffset--,mp, "op: push left'') ;

/ * genera código para ac = operando derecho * / cGen (p2 ) ; / * ahora carga el operando izquierdo * / emitRM("LD",acl,++tmpOffset,mp,"op: load left");

switch (tree->attr.op) {

case PLUS : emitRO("ADDn,ac,ac1,ac, "op + " ) ;

break;

case MINUS :

emitRO("SUB",ac,aci,ac,"op - " ) i

break;

case TIMES : emitRO( "MUL1',ac, acl,ac, "op * " ) ;

break;

case OVER :

emitRO("DIV",ac, acl,ac, "op / " ) ;

break;

case LT : emitRO("SUB",ac,acl,ac,"og c " ) ;

emitRM("JLT",ac,2,pc,"br if true") ;

emitRM("LDC8',ac, O,ac, "false Case") ;

emitRM( "LDA" , pc, 1. pc, "unconditional jmg" ) ;

emitRM("LDC",ac,l,ac,"true case") ;

break;

case EQ : emitRO("SUB",ac,aci,ac,"ap S = " ) ;

emitRM("JEQ",ac,2,pc,"br if true");

emitRM("LDC",ac,O,ac, "false case") ; emitRM("LDAP1,pc, 1,pc. "unconditional jmp") ;

Page 540: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

536 AP~NDICE B 1 LISTADO DEL COMPlLADOR TlNY

2053 emitRM("LDC",ac,l,ac,"true case") ; 2054 break; 2055 default : 2056 emitComment("BUG: Unknown operator"); 2057 break; 2058 1 / * de case op * / 20159 if (Traeecode) emitcomment ( "< - Op" ) ;

2050 break; / * de OpK * / 2061 2062 default : 2063 break; 2064 > 2065 ) / * de genExp * / 2066 2067 / * El procedimiento cGen genera código recursivamente mediante 2068 * el recorrido del árbol 2069 * / 2070 static void caen( TreeNode * tree) 2071 { if (tree != NULL) 2072 { switch (tree->nodekind) {

2073 case StmtK: 2074 genstmt (tree) ; 2075 break; 2076 case ExgK: 2077 genExQ (tree) ; 2078 break; 2079 default : 20110 break; 2081 1 2082 cGenftree->sibling) ;

2083 3 2084 )

2085 -0116 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2087 / * la función principal del generador de código * / 2088 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2089 / * E1 procedimiento codeGen genera código a un archivo 2090 * de código mediante recorrido del árbol sintáctico. El 2091 * segundo parámetro (codefile) es el nombre de archivo 2092 * que contiene el código, y se emplea para imprimir el 2 0 3 * nombre del archivo como un comentario en el archivo de código 2094 * / 2095 void codeGen(TreeNode * syntaxTree, char * codefile) 2096 { char * s = malloc(strlen(codefile)+7); 2097 strcpy (s. "File: " ) ;

2098 strcat(8,codefile); 2099 einitComment("T1NY Compilation to TM Code");

Page 541: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

listado del compilador TINY

emitcomment (S) ;

/ * genera preludio eatándar * / emitComent("Standard prelude:"); emitRM("LDv,mp, O, ac, "load maxaddress from location O") ; emitRM("ST1' ,ac,O,ac,"clear location O"); emitComent("End of standard prelude.");

/ * genera código para el programa TINY * / cGen(ayntaxTree); / * final * / emitComment("End of execution."); emitRO("HALT" ,o,O,O,"");

1

3004) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3001 / * Archivo: tiny. 1 * / 3002 / * Especificación Lex para TINY * / 3003 / * Construcción de compiladores: principios y priictica */ 3004 / * Kenneth C. Louden * / 3005 .......................................................... 3006 3007 %(

3008 #include "globals .hn 3009 #include "util .h" 3010 #include "sean. h" 3011 / * lexema del identificador o palabra reservada */ 3012 char tokenstring [MAXTOKENLEN+ll; 3013 %3 3014 3015 digit 3016 number 3017 letter 3018 identif ier 3019 newline 3020 whitespace 3021 3022 %%

3023 3024 "if" 3025 "then" 3026 "else" 3027 "end" 3028 '' repeat " 3029 "unt i 1 3030 "read" 3031 "mite" 3032 " : S "

{ return IF; 1 ireturn THEN; 1 (return ELSE; (return END; (return REPEAT;) (return üNTIL; 1 {return READ; ) íretun WRITE; 3 {return ASSIGN;)

Page 542: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

APÉNDICE B 1 LISTADO DEL COMP!IADOR TINY

3043 [identif ier)

3044 {newlinel

3045 {whi tespace )

3046 " { "

3047 3048 3049 3050 305 1 3052 . 3053 3054 %%

3055

(return EQ; 3 {return LT; 1 (return PLUS; )

[return MINvS;)

{return TIMES;)

[return OVER; 1 {return LPAREN:}

{return RPAREN;}

[return SEMI;]

[return NOM; 1. {return ID;}

{lineno++;}

[ / * salta espacio en blanco * / )

i char c; do

{ c = input(); if (C = = '\n') lineno++;

1 while (c ! = ' 1 ' ) ;

3 [return ERROR; 1

3056 TokenType getToken(void) 3057 ( atatic int firstTime = TRUZ;

3058 TokenType currentToken;

3059 if (firstTime) 3060 { firstTime = FALCE;

306 1 lineno++;

3062 yyin = source; 3063 pout = listing; 3064 3 3065 currentToken = yylex( ) ; 3066 strncpy(tokenString,yytext,MAXTOKENLEN);

3067 if (TraceScan) {

3068 fprintf(listing,"\t%d: ",lineno); 3069 printToken(currentToken,tokenString);

3870 }

3071 return currentToken;

3071 1

4000 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4001 / * Archivo: tiny.y * / 4002 / * El archivo de enpecificación Yacc/Bison de TINY * / 4003 / * Construcción de compiladores: principios y práctica * / 4004 / * Kenneth C . Loudea * /

Page 543: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del campilador TINY

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . %f

#define YYPARSER / * distingue la salida Yacc de otros archivos de código * /

#include "globa1s.h" #include "util .h"

#include "scan. h" #include "parse. h"

#define YYSTYPE TreeNode * static char * savedName; I* para su uso en asignaciones * / static int savedLineNo; / * ídem * / static TreeNode * savedTree; / * almacena el árbol sintáctico para un retorno posterior * /

%token IF THEN ELSE END REPEAT UNTIL READ WRITE %token ID NUM %token ASSIGN EQ LT PLUS MINüS TIMES OVER LPAREN RPAREN SEMI %token ERROR

%% / * Gramática para TINY * /

program

stmt-seq

stmt

if-stmt

stmt-seq { savedTree = $1; 1

stmt-seq SEMI stmt { YYSTYPE t = $1; if (t I = NULL) ( while (t->sibling != NüLL)

t = t->sibling; t->sibling = $3;

$$ = $1; 1 else $$ = $3;

1 stmt t S $ 5 $1; 1

if-stmt ( S $ = $1; 1 regeat-stmt ( $$ = $1; )

assign-stmt { $ S = $1; 1 read-stmt i $$ = $1; 1 write-stmt f $$ = $1; 1 error { $ $ = NüLL; 1

IF exp THEN stmt-seq END { $ $ = newStmtNode(1fK) ;

$$->child[Ol = $2;

Page 544: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

APÉNDICE B i LISTADO DEL COhPllADOR TlHY

$$->childill = $4; 1

1 IF exp THEN stmt-seq ELSE stmt-seq END { $$ = newStmtNode(1fK); $$->child[O] = $2; $$->child[l] = $4; $$->child[2] = $6;

1 1

repeat-stmt : REPEAT stmt-seq UNTIL exp ( $ $ = newStmtNode(RepeatK); $$->childiOl = $2; $$->child[l] = $4;

1 ;

assign-stmt : ID { savedName = copyString(tokenString);

JU611 4069 4070 4071 4072 4073 4074 4075 4076 read-stmt 4077 4078 4079 11080 4081 4082 write-stmt 4083 4084 4085 4 w 4087 exp 4088 4089 4090 4091 4092 4093 w 4 4095 4096 4097 1098

savedLineNo = lineno; 1 ASSIGN exp

{ $$ = newStmtNode ( AssignK) ; $$->child[Ol = $4; $$->attr.name = savedName; $$->lineno = savedLineNo;

1 ;

: READ ID ( $$ = newStmtNode (ReadK) ; $$->attr.name = copyString(tokenString);

? i

: WRITE exp { $$ newStmtNode(WriteK); $$->child[Ol = $2;

1 ;

: simple-exp LT simpleexp ( $ $ i newExpNode ( OpK) ; $$->childlOl = $1; $$->child[ll = $3; $$->attr.op = LTI

> I aimpleexp EQ simple-exp

{ $$ = newExpNode(0pK); $$->child[Ol = $1; $$->child[lI = $3; $$->attr.op = EQ;

?

Page 545: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

listado del campilador TlNY

4099 I simple-exg E $$ = $1: 4100 ;

4101 simple-exp : simple-exg PLUS term

(: $$ = newErgNode (OPK)

4115 term 4116 4117 4118 4119 4120 4121 4122 4123 4124 4125 4126 4127 4128 4129 factor 4130 4131 4132 4133 4134 4135 4136 4137 4138 4139 4140 4141 4142 %%

4143

$S->childfOI = $1; $$->child[ll = $3; $$->attr.op = PLUS;

3 I simple-exp MINUS term

{ $ $ = newExpNode ( OpK) ; $$->child[Ol = $1; $$->child[ll = $3; $$->attr.op = MINUS;

3 I term { $$ = $1; 3 ;

: term TIMES factor { $ $ = newErgNode f OpK) ; $$->child[Ol = $1; $$->child[ll = $3; $$->attr.op = TIMES;

1 I term OVER factor

( $$ = newExpNode (OPK) ; $$->child[Ol = $1; S$->child[ll = $3; S$->attr.oP = OVER;

3 I factor { S$ = $1; 1 ;

: LPAREN exp RPAREN

(: $S = $2; 1 I m

{ $$ = newExpNode (ConstX) ; $$->attr.val = atoi(tokenString);

3 1 ID f $$ = newExpNode (IdK) ;

$$->attr.name = copyString(tokenString);

1. I error f S $ = NüLL; 1 ;

4144 int nt.error(char * message) 4145 { fprintf(listing,"Syntax error at line %d: %s\n".lineno,message);

Page 546: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

542 APÉNDICE B i LiSTADO DEL iO In l i l i iDOR i iNY

4146 fprintf (listing, "Current toker.: " ) ;

4147 printToken(yycbar,tokenString);

4148 Error = TRUE; 4149 return O;

4150 1. 4151 4152 / * yylex llama a getToken para hacer la salida Yacc/Bison 4153 * compatible con versiones más antiguas del 4154 * analizador léxico TINY 4155 * / 4156 static int yylex(void1 4157 ( return getToken0; 1 4158 4159 TreeNode * parse (void) 4160 ( marse(); 4161 return savedTree;

4162 >

4200 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4201 / * Archivo: globa1s.h * / 4202 / * Versión Yacc/Bison * / 4203 / * Las variables y tipos globales para el compilador TINY * / 4204 / * deben venir antes de otros archivos "include" * / 4205 / * Construcción de compiladores: principios y práctica * / 4206 / * Kenneth C . Louden * / 4207 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4208 4209 #i f ndef -GLOBALS_H_ 4210 #define -GLOBALSH- 421 1 4212 #include cstdio.h> 4213 #include cstdlib.h> 4214 #include <ctype. h> 4215 Ilinclude cktring . h> 4216 4217 / * Yacc/Bison genera internamente sus propios valores 4218 * para los tokens. Otros archivos pueden tener acceso a estos valores 4219 * incluyendo el archivo tab.h generado utilizando la 4220 * opción -d de Yacc/Bison ("generar encabezado") 4221 * 4222 * La marca o bandera YYPARSER previene la inclusión del tab.h 4223 * en la salida Yacc/Bison misma 4224 * / 4225 4226 #i f nde f YYPARSER 4227

Page 547: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del compilador TlNY

4228 / * el nombre del archivo siguiente puede cambiar * / 4229 #include "y.tab.hW 4230 4UI / * ENDFILE está definido implícitamente por medio de Yacc/Bison, 4232 * y no se incluye en el archivo tab.h 4233 * / 4234 #define ENDFILE O

4235 4235 #endi£ 4237 4238 # if ndef FALSE

4239 #define FALSE O

4240 #endi£ 4241 4242 #ifndef TRUE 4243 #define TRUE 1 4244 #endi£ 4245 4246 / * MAXRESERVED = el número de palabras reservadas * / 4247 #define MAXRESERVED 8

4248 4249 / * YaccfBison genera sus propios valores enteros 4250 * para tokens 4251 * / 4252 typedef int TokenType; 4253 4254 extern FILE* source; / * archivo de texto del código fuente * / 4255 extern FILE* listing; / * archivo de texto de salida del listado * / 4256 extern FILE* code; / * archivo de texto del código para el simulador TM * / 4257 4258 extern int lineno; / * número de línea fuente para el listado * / 4259 4260 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

4261 / ** Árbol sintáctico para el análisis sintáctico * * / 4262 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

4263 4264 typedef enum (StmtK, ExpKl NodeKind; 4265 typedef enum {If K, RepeatK,AssignK, ReadK, WriteKl StmtKind; 4266 typedef enum {OpK,ConstK,IdK) ExpKind; 4267 4268 / * ExpType se utiliza para verificación de tipo * / 4269 typedef enum {Void, Integer, Boolean) ExpType; 4270 4271 #define MAXCHILDREN 3

4272 4273 typeiief struct treeNode 4274 { struct treeNode * child[MAXCHILDRENl;

Page 548: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

544 APENDICE B 1 LISTADO DEL COMPIUDOR TlNY

4275 struct treeNode * sibling; 4276 int lineno; 4277 NodeKind nodekind; 4278 union { StmtKind stmt; ExgKind exp;) kind; 4279 union TokenType opr 4280 int val; 4281 char * name; 1 attr; 4282 ExpType type; / * Rara verificación de tipo de expresiones * / 4283 1 TreeNode; 4284 4285 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1286 /************ Marcas paca rastreo ************ / 4287 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4288 4289 / * Echosource = TRUE causa que el programa fuente sea 4290 * repetido en el archivo de listado con números de línea 4291 * durante el análisis sintáctico 4292 * / 4293 extern int EchoSource; 4294 4295 / * TraceScan = TRUE provoca que la información del token sea 4296 * impresa en el archivo de listado a medida que cada token es 4297 * reconocido por el analizador lQxico 4298 * / 4299 extern int TraceScan; 4300 4301 / * TraceParse = TRUE provoca que el árbol sintáctico sea 4302 * impreso al archivo de listado en forma lineal 4303 * (utilizando sangrías para los hijos) 4304 * / 4305 extern int TraceParse; 4306 4307 / * TraceAnalyze = TRUE grovoca que la tabla de simbolos inserte 4308 * y busque para informar al archivo del listado 4309 * / 4310 extern int TraceAnalyze; 4311 4312 / * TraceCode = TRUE causa que los comentarios sean escritos 4313 * al archivo de código TM a medida que se genera el código 4314 * / 4315 extern int TraceCode; 4316 4317 / * Error = TRUE evita pasos adicionales si se presenta un error * / 4318 extern int Error; 4319 liendif 4320

Page 549: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

l i s l a d o del simulador de l a máquina TI

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . / * Archivo: tm.c * / / * La computadora TM ("Tiny Machine") * / / * Construccidn de compiladores: principios y practica * / / Kenneth C. Louden * / ..........................................................

#ifndef TRUE #define TRUE 1 #endif #ifndef FALSE

#define FALSE O #endi f

/******* const *******/ #define IADDR-SIZE 1024 / * incremente para programas grandes * / #define DADDR-SIZE 1024 / * incremente para programas grandes * / #define NO-REGS 8

#define PC-RED 7

#define LINESIZE 121 #define WORDSIZE 20

typedef enum (

opclRR, / * reg operandos r,s,t * / opclRM, / * reg r, mem d+s * / opclRA / * reg r, int d+s * / ) OPCLASS;

Page 550: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

AP~NDICE C 1 LISTADO DEL SIMULPDOR DE LA M ~ Q U I N A TINY

typedef enum {

/ * ~nstrucciones RR * / opHALT, / * RR alto, los operandos deben ser cero * / OPIN, / * RR lee en reg(r) ; s y t son iqnocados/ opOUT, / * RR escribe de reg(r); s y t son ignorados/

opADD, / * RR regir) = reg(s)+reg(t) * / opSUB, / * RR reg(r) = reg(s)-reg(t) * / OPMUL, / * RR reg(r) = reg(s)*reg(t) * / opDIV, / * RR reg(r) = reg(s)/reg(t) * / opRRLim, / * limite de opcodes RR * /

/ * Instrucciones RM * / OPLD, / * RM reg(r) = mem(d+reg(s)) * / OPST, / * RM mem(d+reg(s)) = reg(r) * / opRMLim, / * Límite de opcodes RM * /

/ * ~nstrucciones RA * / opLDA, / * RA reg(r) = d+reg(s) * / opLDC , / * RA reg(r) = d ; reg(s) es ignorado * / opJLT, / * RA if reg(r)<O then reg(7) = d+reg(s) * / opJLE, / * RA if reg(r)<=O then reg(7) = d+reg(s) * / op JGT , / * RA if reg(r)>O then reg(7) = d+reg(s) * / opJGE, / * RA if reg(r)>=O then reg(7) = d+reg(s) * / opJEQ, / * RA if reg(r)==O then reg(7) = d+reg(51 * / OPJNE, / * RA if reg(r)!=O then reg(7) = d+reg(s) * / opRALim / * Limite de opcodes RA * / 1 OPCODE;

typedef enum (:

sroKAy,

s rHALT,

SrIMEM-ERR,

srDMEM-ERR,

SrZERODIVIDE

} STEPRESULT;

typedef struct (:

int iop ;

int iargl ;

int iarg2 ;

int iarg3 ;

1 1NSTF.UCTION;

Page 551: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del rimulador de la máquina TlNY

/ ******** vars ******** / int iloc = O ; int dloc = O ; int traceflag = FALSE: int icountflag = FALSE;

INSTRUCTION iMem [IADDR-SIZEI;

int dMem [DADDR-SIZEI ;

int reg [NO-REGSI ;

/ * RA opcodes * /

char * stepResultTab[l = {"OK","Halted'~,"Instruction Memory Fault",

"Data Memory Fault","Division by O "

1 . ;

char pgmliame [ 2 0 1 ; FILE *pgm ;

char in-Line ILINESIZEI ;

int lineLen ;

int inCol ;

int num ;

char word[WORDSIZEl ;

char ch ; int done ;

/ * * * * * * * * * * * e * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /

int opClass( int c )

í. if ( c <= opRRLim) return ( opclRR ) ;

else if ( c <= opRMLim) return ( opclRM ) ;

else return ( opclRA ) i

1. / * de opclass * /

void writeInstruction ( int loc

{ printf ( "%5d: ", loc) ; if ( (loc >= O) && (loc < IADDR-SIZE) )

{ printf("%6~%3d,", opCode~ab[iMem[loc].iop~, iMem[locl.iargl); switch ( opClass(iMem~loc1.iop) )

Page 552: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

APENDICE C 1 LISTADO DEL SIMULADOR DE LA MAQUINA TlNY

( case opclRR: grintf ( "%ld,%ldZ', iMem[locl . iarg2, iMem[locl . iargi) ; break;

case ogclRM: case ogclRA: grintf("%id(%ld)'*, iMemIlocl.iarg2, iMem[locl.iargi);

break;

1 printf ("\nq') ;

1 } / * de writeInstruction * /

.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . void gatCh (void) { if (++inCol < linelen)

ch = in-Line [inColl ; elae ch = ' ' ;

) / * de getCh * /

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . int nonBlank (void) (while ((incol < IineLen)

&& (in-Line[inCol] = = ' ' ) )

inCol++ ; if (incol < linelen) { ch = in-Line[inCol] ;

return TRUE ; ) else t c h = " ;

return FALSE ; 1 ) / * de nonslank * /

int getNum (void) ( int sign; int term:

int temg = FALSE; n u m = O ; do ( sign I 1; while ( nonBlank0 && ((ch == ' + ' ) I I (ch == l - * ) ) )

1: temp = FALSE ; if (ch == ' - ' ) sign = - sign ; getch ( ) ;

1 tem = O ; nonBlank ( ) ;

while (isdigit (ch) )

Page 553: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del simulador de la máquina TIMY

( temp = TRUE ; term = tenn * 10 + ( ch - 'O' ;

getch ( ) ;

1 num = num + (term * sign) ;

1 while ( (nonBlank0) && ((ch == ' + ' ) I t fch a = l . - ' ) ) 1 ; return temp;

1 / * de getNum */

...............................................

int getword (void) f int temp = PALSE; int length = 0; if (nonFllank O ) ( while (isalnum(ch))

{ if (length 4 WORDSIZE-1) word Ilength++l = ch ; getCh0 ;

1 wordIlength1 m '\O1; temp = (length != 0);

1 return temp;

1 /* de getWord */

/ * * e * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /

int skipch ( char c )

int temp = FALSE; if ( nonBlank0 && (ch == c) )

{ getCh0; temp = TRUE;

1 return temp;

} / * de skipCh * /

.............................................. int atEOL(void) { return ( ! nomlank 0 ); 1 /* de atEOL */

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . int error( char * msg, int lineNo, int inatNo) { grintf ( "Line %dn, lineNo) ; if (instNo >= 0) printfi" (Instruction W)",instNo); printf ( " %a\n",msg) ; return FALSE;

1 / * de error * /

Page 554: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

550 APÉNDICE C 1 LISTADO DEL SIMULADOR DE LA MÁQUINB TlNY

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

int readInstructions (void) { OPCODE op;

int argl, arg2, arg3; int loc, regNo, lineNo; for (regNo = O ; regNo < NO-REGS ; regNo++)

reg[regNol = O ; dMemjOl = DADDR-SIZE - 1 ; for (loc = 1 ; loc < DADDR-SIZE ; lec++)

clMem[locl = O ; for (loc = O ; loc < IADDR-SIZE ; lec++) { iMem[loc] . iop = opHALT ; iMem[loc] .iargl = O ; iMem[loc] .iarg2 = O ; iMem[locl .iarg3 = O ;

1 lineNn = O ; while ( ! feo£(pgm)) { fgets( in-Line, LINESIZE-2, pgm f ;

inCol = O ; lineNo++; lineLen = strlenfin Line)-1 ; if (in-Line [lineLenl==* \n' ) in-Line [lineLenl = ' \O ' ;

else in-Line[++lineLenl = ' \O' ; if ( (nonBlank0) && fin-Line[inColl ! = ' * ' ) )

( if ( ! getNum0) return error("8ad location", iineNo,-1);

loc = num; if (loc z IABDR-SIZE)

return error("Location too large",lineNo,loc); if ( ! skipCh(':'))

return error ("Missing colon", lineNo, loc) ; if f! getWord 0 ) return error("Missing opcode", lineNo,loc);

op = opHALT ; while ((op < opRALim)

&& (strncmp(opCodeTab[opl, word, 4 ) ! = 0) f op++ ;

if (strncmp (opCodeTab [opl , word, 4 ) 1 = 0) return error("Illega1 opcode" , lineNo, loc) ;

switch ( opClassfop) ) ( case opclRR :

.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . if ( ( ! getNum O ) I I fnum < O) 1 I (num >= NO-REGS) )

return error("Bad first register", lineN0,loc); argl = num; if ( ! skipCh( ' , ' f

return error("Missing comma", lineNo, loc);

Page 555: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del simulador de la maquina TlNY

if ( ( ! getNum 0 ) I I (num < O) 1 I (num >= NOREGS) )

return error("Bad second register", lineNo, loc);

arg2 = num;

if ( ! skipCh(',')) return errorfnMissing comma", lineNo,loc);

if ( ( ! getNum 0 ) I I (num < O ) I I (num >= NO-REGS) 1 return error ("Bad third register", lineNo, loc) ;

arg3 = num;

break:

case opclRM :

case opclRA : . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . if ( ( 1 getNum 0 ) I I (num < O) I I (num >= NO-REGS) )

return error ( "Bad f irst register", lineNo, loc) ;

argl = num; if ( ! skipCh(','))

return error ( "Missing comma", lineNo, loc) ;

if ! getNum 0 ) return error("Bad displacement", lineN0,loc);

arg2 = num; if ( ! skipCh('(') && ! skipCh(',') )

return error ( "Missing LParen", lineNo, ioc) ;

if ( ( ! getNum 0 ) I I (num < 0) I I (num >= NO-REGS))

return error("Bad second register", lineNo,loc);

arg3 = num; break;

iMem[locl .iop = op; iMem[locl . iargl = argl; iMem[loc] . iarg2 = arg2;

iMem[loc] .iarg3 = arg3:

1 1 return TRUE;

} / * de readInstructions * /

STEPRESULT stepTM (void)

f INSTRUCTION currentinstruction ;

int pc ;

int r,s,t,m ;

int ok :

Page 556: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

552 BP~NDICE C i LISTADO DEL SIMULADOR DE LA HAQUINA TlNY

pc = reg[PCREGl ; if ( (pc < O) 1 I (PC > IADDR-SIZE) )

return srIMEM-ZRR ;

reg[PC-REG1 = gc + 1 ; currentinstruction = iMem[ pc 1 ; switch (ogClass(currentinstruction.iop) ) í case opclRR :

/ * * * * * * * * * * * * * * * * * * * * * * * * e * * * * * * * * * * /

r currentinstruction.iarg1 ;

s = currentinstruction.iarg2 ; t = currentinstruction.iarg3 ; break;

case opclRM : ... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . r = currentinstruction.iarg1 ; s = currentinstruction.iarg3 ; m = currentinstruction.iarg2 + reg[sl ;

if ( (m < O) 1 I (m > DADDR-SIZE) )

return srDMEMERR ; break;

case opclRA : . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

r = currentinstruction.iarg1 ; s = currentinstruction.iarg3 ; m currentinstruction.iarg2 + reg[sl ;

break; } / * de case * /

switch ( currentinstruction.iog) ( / * Instrucciones RR * / case O P ~ L T : .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . printf ("HALT: %ld,%ld,%ld\n",r, S, t) ; return srHALT ; / * break; * /

case opIN : .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . do f printffMEnter value for IN instruction: " ) ;

fflush (stdin) ; gets(in-Line); inCol = O;

ok = getNum() ; if ( ! ok ) printf ("Illegal value\nm); else reg[rl = num;

J

Page 557: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del simulador de la máquina TlNY

while ( 1 ok); break;

case OPOUT :

printf ("OUT instruction grints: %d\ni', regfrl ) ;

break;

case opADD : reg[r] = regfsl + regftl ; break;

case opSUB : reg[r] = regla] - regftl ; break; case opMüL : regfr] = regfsl * regltl ; break;

case opDIV : .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . if ( reg[t] ! = O ) reg[rl = regfsl / reg[tl; else return srZERODIVIDE ;

break;

/ * * * * * * *e * * * * * * Instrucciones m e * * * * * * * * * * * * * * * * * * /

case opLD : regfrl = daIem[ml ; break; case opST : dMem[m] = regfrl ; break;

/ * * * * *e * * * * * * * * * Instrucciones *********e********** /

case opLDA : reglrl = m ; break; case OPLDC : regfr] = currentinstruction.iarg2 ; break; case opJLT : if ( reg[r] < O ) regfPCREG1 = m ; break; case ogJLE : if ( reg[r] <= O ) regfPC-REGI = m ; break; case ogJGT : if ( reg[rl > O ) reg[PC-REGI = m ; break;

case opJGE : if ( reg[r] >= O ) regfPCREG1 = m ; break; case opJEQ : if ( regfr] == O ) reg[PC-REGI = m ; break; case ogJNE : if ( reg[r] ! = O ) reg[PC-REGI = m ; break;

/ * fin de las instrucciones legales * / } / * de case */ return sr0K.Y ;

> / * de stepTM * /

.............................................. int docommand (void) { char cmd; int stepcnt=O, i;

int printcnt; int stepResult; int regNo, loc;

do { printf ( "Enter command: " ) ;

fflush (stdin) ; gets(in_Line); inCol = O:

Page 558: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

554 APÉNDICE C / LISTA80 DEL SIMULADOR DE LA MAQUINA TINY

l while ( ! getWord 0 ) ;

cmd = word[O] ;

switch ( cmd )

{ case 't' : / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * e * /

traceflag = ! traceflag ;

printf ( "Tracing now " ) ;

if ( traceflag ) print£("on. \nr') ; else printf("0ff .\n") ;

break;

case 'h' : / * * * * * e * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /

printf ("Commands are: \ni') ;

printf(" s(tep <n> " \ "Execute n (default 1) TM instructions\n") ;

printf ( " g(o " \ "Execute TM instructions unti1 HALT\nn);

printf ( " r (egs " \ "Print the contents of the registers\n" ) ;

printf ( " i(Mem <b a>> " \ "Print n iMem locations starting at b\n3');

printf(" d(Mem <b <m> " \

"Print n üMem locations starting at b\nv); printff" t(race " \

"Toggle instruction trace\nl') ;

printf ( " pfrint " \ "Toggle print o£ total instructions executedW\

" ('gol only)\nW);

printf ( " c (lear " \ "Reset simulator for new execution o£ program\nW);

printf(" h (elp " \ "Cause this list of commands to be printed\nn);

printf(" q(uit " \ "Teminate the simulation\n") ;

break;

case 'p' : . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . icountflag = ! icountflag ;

printf ("Printing instruction count now " ) ; if ( icountflag ) printf ("on.\n") ; else printf ("o££. \n") ;

break;

case 'S' : . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

if ( a t E O L 0 ) stepcnt = 1;

Page 559: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

listado del simulador de la máquina TlNY

else if ( getNum 0 ) stepcnt = abs(num); else printf ('Step count?\n") ;

break;

case 'g' : stepcnt = 1 ; break;

case 'r' : . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . for (i = O; i < NO-REGS; i++)

{ printftW%ld: %4d ", i,regIil); if ( (i % 4) == 3 ) printf ("\no);

1 break;

case 'ir : . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . printcnt = 1 ; if ( getNum 0 ) { iloc = num ; if ( getNum 0 ) printcnt = num ;

l if ( ! atEOL 0 ) printf ( "Instruction locations?\n") ;

else

( while ( (iloc >= 0) && (iloc < IADDR-SIZE) && (printcnt > 0) )

{ write~nstruction(iloc);

iloc++ ;

printcnt-- ;

1 1 break;

case 'di : /* * * * * * * * * * * * * * * * * * * * * *e* * * * * * * * * * * * * * /

printcnt = 1 ; if ( getNum ( ) )

( dloc = num ; if ( getNum ( ) ) printcnt = num ;

1 if ( ! atEOL 0 ) printf ("Data locations?\n") ;

else ( while ((dloc >= 0) && (dloc < DADDR-SIZE)

&& (printcnt > 0))

{ printf("%5d: %5d\n",dloc,dMem[dlocl); dloc++;

Page 560: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

AP~NDICE C 1 LISTADO DEL SIMULADOR DE LA HAQUINA TlNY

grintcnt--;

1. 1. break;

case 'c' : / * * * * * * * * * * * * e * * * * * * * * * * * * * * * * * * * * * * /

iloc = O; dloc = O; stepcnt = O;

for (regNo = O; regNo < NO-REGS ; regNo+ + )

reg[regNol = O ;

dMem[O] = DADDR-SIZE - 1 ; for (loc = 1 ; loc < DADDR-SIZE ; loc++)

dMemlloc1 - O ; break;

case 'q' : return FALSE; / * break; * /

de£ ault : printf ( "Comand %c unknown. \n" , cmd) ; break; 1 /*decase * / stepResult = srOKAY; if ( stepcnt > O )

{ if ( cmd == 'g' 1 { stepcnt = 0; while (stepReault = = srOKAY) { iloc = regtPCREG1 ;

if ( traceflag ) writeInstruction( iloc ) ;

stepResult = stepTM 0 ; stepcnt++;

1 if ( icountflag )

printf("Number of instructions executed = %d\nm, stepcnt);

1. else { while ((stepcnt > 0 ) && (stepResult == srOKAY))

{ iloc = reg[PC-REGI ;

if ( traceflag ) writeInstruction( iloc ) ;

stepResult = stepTM 0 ; stepcnt-- ;

> 3 grintf( "%s\n",stepResultTab[stepResultl 1;

1 return TRUE;

! / * de doCammand * /

Page 561: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Listado del simulador de la máquina TINY

main( int argc, char * argv[l )

( if (argc != 2 ) printf("usage: %S <filename>\nm targvio]); exit(1);

1 strcpy(psmName,argvIll : if (etrchr (pgmName, l . ' ) == NULL)

strcat (pgmName, " . tm") ; pgm = fopen(pgmName,"r") i if (pgm == NULL) ( printf("fi1e '%S' not found\nw.~gmName):

exit(1);

3

/ * lee el programzi * / if ( ! readInstructions 0)

exit(1) ; / * archivo de entrada comuta a terminal */ / * reset (input); * / / * lee-eval-imprime */ printf("TM simulation (enter h for help) ... \nl'); do

done = I docommand ( ) ;

while ( 1 done );

printf ("Simulation done. \nn) : return O:

3

Page 562: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Los númems de piginas seguidos por una c indican que la pagina ctirresponde a ejercicios; los que \e wicticritran seguidos por unü n correspiinden a LIS páginas con notas a pie de págiria: los seguidos por r. indican que son páginas de las secciones de notas y referencias; los seguidos por tina t señalan píginas con tablas, y la abreviatura ilirs. hace I-eferencia a las páginas del texto que contiene11 figuras.

Acceso encaderiüdo, 368-370, 3Y2e Acci6n de rediicción, 108-199, 206-209 Acción tienerate, en imálisis sintáctico

<lescendente, 153 Acción "por defa~ilt" (predeteriiiina&), 86 Accibn Shiit, 1')s- 199, 206-209 4.: . ~clolies enihcbidas, en código Ywc,

229,24 1-242,242t Acciones üotii, 212, 233-234ilirs.,

246-247 /\ccioiies. V<;mr rcrrnhiéri Acción prede-

terniioada; Acciones embebidas; Generiición de ;icci<in; i2cci<111es Cl»t<~; l~istrucciones de s:iltos; Acción de reducción; Acción de desplazamiento

de autómatas finitos determinísticos, 53-56

en código Len, 85, 90 cn código Yacc, 229-230

:iction defuidt, en código Yacc, 234 Adición (suma), 117-1 18. 118-I 19

rtinhigüedad no esencial con, 123 Adniinistraci<in ;iutomitic~ de estructuras

dinámicas (heap), 380-381 Administracióii de memoria, 345,346-349 Adminis¡r:ición manual de la estructura

dinámica, 377-380 ,Atlministradores de proyecto, 6 Alfabetos, 34-35.49-50,57-58 "Jgoritino de m&is sintáctico general

L.R( 1 ), 22 1 Algoritmo de construcción de subconjun-

tos, 09, 70-72 iilgoritrno de Euclides para el máximo

coiniin divisor, 353-354 Alg«ritmos

autómatas tiniros detertninísticcis pira. 5+56

ile primer ;ijtiste. 378 p:im :inálisis sintáctico LAI.Ri 1).

214-225 p:is:t calcular conjuiitos I'iriniero,

i Xh- 10'1. I OXiliis. I0')ilir.s. Ip:Iril c;ilcuiíir ctinju~it(~s Siguiente,

173- 174, I74i11,s.

para cálctilo de atributo. 270-295 pm constniir autómatas finitos deter-

ministicos. 252e para determinar equivalencia de tipos.

322-329 p;ira recolección de hasusa, 17

Aig«ritnicis, [>:ira rinálisis sintáctico des- cendente, 197- 198

Aliüs, 300, 328, 383 ambiente dc ejecución basado en pilas

pmi. 353-354. 354ilrt.s. ,Ambientes coinplctainerite di~iiinicos,

345. Vdaw tmhi&c Ambientes de ejecución

Anrbientes de iiesarrollo iiiteriictivos (DE, por sus siglas en inglés), 4 6

iZsnhientes de ejecución, 301, 345-388, 388-391~. VL'use tuprrbid?i Amhirn- tes de ejecución dinámicos; Am- hientes de ejecución basados en piliis; Ambientes de ejecuci6n estáticas

hasados en pilas, 352-373 c«mpletarnente estáticos, 349-352,

352ilus., 349-352, 352ilrrs. construcción de compiladores y, 17 dinAinicos, 373-381 mecanismos de paso de parimetro en.

38 1-386.394-305e Orgmizdción de memoria en,

346-349 para el lenguaje TINY, 386-388 para la ni&jnina TM. 456 tipos de, 345-346

Ambientes de ejecución basados en piliis 17, 345, 352-373

acceso a nombres en, 356-359 cori paránictros de procediniiento.

370-373 con pnicedirnieiitos locales, 165-370 ilatos de longitud variable en,

16 1-306 ~lccl;if.;icioncs :iniJ:iJ:is cri. 303.

Xi4-i6.S ~xg;iiiimción Jc. 352-351

secuencias de Il;itna<las de pnxcdi- miento cii. 350-36 l

sin procetlimientos lowles, 353-365 ternpor:tles locales en. 363

Ainhicntes de ejcciición completi~tnerite estáticos, 345, 149-352, 350ilus., 352ilci.r. V4u.w trzrnhiéti Amhieiites de ejrsución estáticos

Ainhieiites de ejecución dinámicos, 17, 373-38 1 . V<;lr,s~ trimhiéri Ambientes completatneritc ditiiiiiicris

Atnbiwtes de ejecuciriii estáticos, 17, 39 1 c. V&c tr~tnhiCn Ambientes de ejecución completaniente estáticos

Ambientes en ventanas, 4 Anibigüedad, 1 1-1-123, I4li,, 20111, 253e.

Vhse /~atibi&r I>niblcoia de elsc ambiguo: Reglas de ir« ninhigücd;id

ámbito dinámico y, 306 de graniáticns con atributos, 270 definición formal de, 129 en análisis si~itáctico LR( I ) , 221 en expresiones de C , 342e CII gramáticas de expresión aritin4tica

simple, 308-30Y. 341u en griim.iticas LL( I), 155 en gram6ticas I.R(O), 207-208 en grainiticas SLR( 1 ), 2 10-21 1,

213-215 en sentencias "if', 120- 122 lenguajes con, inherente. 133 no esencial, 172-123 resolución de, en Lex' 87, 89 simplificación de gr:tmúticas con atri-

btitos con, 269 iíinbitii, 300, 301-308

dinámico, 305-306, 365-316

para declarriciones :11 tnisino nivel de n~iidnción. 306-308

para dcclaracioncs dc cupresión ler. 3 w 3 1 0

q l n s del. !O1 -307. 303, 305-306. 365-366

ii~iihito cst:ítico, 305-.iOh

Page 563: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Ámbitos anidados, 3

simple, 308-3 13 Análisis de flujo de datos, 474 Aililisis griunaticales. gramáticas mbi -

gua, y, 116 Análisis léxico, Y, 31. Véa~e tiwlbiérz

Rastreo Análisis,.operaciones de compilador

para, 15 Análisis sernántico, 257-338

atributos y, 259-261 cilculo de atributos en, 270-295 categoiías y naturaleza de, 257-259 de lenguaje TINY, 334-338 gramáticas con atributos y, 261-270 tablas de símbolos en, 295-313 tipos de datos y verificación de tipo

en, 3 13-334 Análisis semántica estático, 257,

258 Análisis sintáctico, 95-97, 106-128, 343r.

Véuse tumbiéu Análisis sintáctico ascendente; Análisis sintáctico LALR(I); Analisis sintdetico LLí 1 ): Análisis sintActico LRi0); Análisis sintictico LR(I); Análisis sintáctico SLR(1); Análisis sintáctico descen- dente

cálculo de atributos durante, 289-293

generwiún de cóiiigo intermedio y objeto durante, 407-410

nroceso del. 96-97 Anáhis bintáctico ascendente, 96,

197-250 .. ~

ünlilisis sintáctico LALR(I), 224-242 ;tn&lisis sintáctico LRtOi, 201-210 análisis sintiictico LR(I), 217-223 ;inálisis sint6ctico SLR(l), 210-217 descripción, 198-201 ~eneralior de analizadores siiitácticos

Yacc para, 226-245 para sinraxis de TINY, 243-245 recuperación de errores durante,

245-250 Análisis sicitáctico descendente, 96,

143-189. 152t I96r :inálisis siiltáctico LL(I j, 152- 180 de lenguaje TINY, 179- 182 mediante recursivo descendente,

144- 152 recuperación de errores durante,

IXZ-1x9

Anjlisis sintictico LL(l), 143-1.14, ts2-1sa 190-1936 1 9 6 ~ 197

algoritmo y tabla par& 154-157 búsqueda. delante extendida pa-

r* 1x0.~ ,,; . , . . . .

conju$tos r?timera'pala;. 168- 173 cclniuhioa Ciguietge para, 1 68,173- t 77 "nstrueciún de &bol sintjcticn en,

166-167.. - ' ,, , :,, , ,

eliminación <le mcursi<inpoi la, izquierda en, 157.162 ; . I. .:

factoriza~ión por la izquie+ en,. 157, 162-166 ,,, . , .,, . , ,

mitodo bisico de, 15211~4;. , :,.

recuperacidn de errores en;. 186- kX7 Análisis sintáctico LK! 289-290; 34%. Análisis sinth&ico LR(O), 197. 201,

206-210,245 Anilisis sintáctico LR(l), 197,224-225,

245,256r de búsqueda hacia delante. Véuse

Análisis sintzíctico LALR(I) Análisis sintáctico recirrsivo descendente,

143-144, 144-152, 196r malizadores sintlicticos LL(k) y, 180 dc lenguaje TINY, 180- 1 S2 eliminación de recursión por la

izquierda y, 161-162 forma Backus-Naur extendida en,

145-151, método bdsico del, 114- 145 modificación de analizadores sintácti-

cos para. 193-1% problemas con, 151- 152 recuperación de errores en, 183-185 cetroseguimiento en, 192e

i\tiálisis sintáctico simple LR(1). I1du.se Análisis sintáctico SI.R(I)

,Análisis sintáctico SLLik), 180 Análisis sintáctico SLR( l), 197, 210-21 7,

251e, 252e, 256r algoritmo para, 210-21 1 análisis sintáctico LALR(1) y. 224-225 análisis sintúctico SLR(k) y, 216-217 conflictos de no ainbigüed;id en,

213-215 detección de errores en, 245 ejemplos de, 21 1-213. 341e limitaciones de. 215-216

Ai>alizad«r léxico TINY :uchiva de entrada de Lex para,

90-9 1 trutómata finito determinístico pura,

77ilits. inrpletiientacitin, 32, 75-80 modificación, 93-94e

,411nlizador sintktico TINY, 187- 189, 143-745.255-256e

Analiradorcs scmánticos, 9- 10 ,\naliuid«res siiitácticos, 4-9. Vehe

rurnbi6n h o l c s de análisis sintác- tic0

abstrnc(os wrisfruidtis mediante. 109-II 1

con deaplummientos. 357,358, 358il-us.

en pilas de ejecución con vínculos de acceso, 367ilus.. 369ilus., 37Oilus., 372ilus., 373il11s.

en secuencias de llamada de procedi- miento, 359-361, 359ilrrs., 360ilus., 361 ilirs,, 362ilus., 363i/rts., 364ilu.~ 365ifus., 366ilus.

Apuntador de pila isp, por sus siglas en 1 inglés), 348, 353, 354ilrrs.. 356ilu.s.. 392r i

Apuntador de tabla de función virtual, 376ilus. !

Apuritador de tope de pila (tos, por sus i siglas en inglés), 353 1 i

.Apuntador m e m p t r , 378-380,378ilu.s., i 379ilrrs., 380ilus I

Apuntadores, 322. Véase tnmbién 1 i Apuntadores de acceso; apuntador 1 de argumento (ap, por sus siglas en inglés); Registro de apuntador ba- se; Carácter caret ("); Apuntador dt ambiente (ep, por sus siglas en inglés); Apuntadores remotos; Apuntador de marco (fp, por sus siglas en inglés); Apuntador de instrucciítn (ip, por sus siglas en inglés): Apuntadores cercanos: Desplüzamicntos; Apuntador de pila isp. por sus siglas en inglés); 1 tope del ;ipuntador de pila (tos, por 1 sus siglas en ingEs); :~ptintador de ! tnhlas de funcibn virtiial ! 1

Page 564: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Apuntadores cercanos, 3 19 Apuntadores de acceso. 304 ,\puntiitfores lejanos, 319 irbol de anilisis gramatical recorrido de

i~quierda a dcrecha y , 289-290, 29 1-292

,<rboles binanos enteros, gramática para, 339e

Árboles de activación, 356, 357iltrs. írboles de análisis gramatical, 8-9,

qilus., I Oil~is., 1 1 i lm, 96, 106- 109. liéme tambidn t%rboles de análisis sinráctico

análisis sintáctico descendente y, 153-154

irboles de análisis sintáctico abstrac- tos y, 109-1 14

atributos sintetizados y, 277 definición formal de, 129 dependencias "hacia atrás" en,

289-290 después de eliminación de recursión

por la izquierda, 160- 16 1 especificación de asociatividad y

precedencia en, 1 18-1 19 para expresiones aritmiticas, I Wilus.,

272-273 para expresiones aritniéticas enteras,

26Silus., 265 para grainática de declaración de va-

riables, 266ilirs. para gramáticas amhiguas. 1 14- 1 16 pira gramáticas de números con base,

273-275 ptira gramáticas de números sin signo.

263-264, 264ilu.s., 267-268, 268ilrrs., 27 1-272, 276-277

pira grsmiticas libres de contexto, 95

para sentencias "il", 120-1 22 para tipos de valor de punto flotante,

294ilus. representación de hijo extremo iz-

quierdo con hermanos a la derecha, 114 . . .

;\rboles de búsqueda binarios, en lengua- je ML, 321-322

Arboles sintitcticos, 8, I lilirs., 13, 96, 193e, 194e. Véase tainbién Árboles sintácticos abstractos

atributos heredados en, 280-281 como atributos sintetizados, 289 con código P, 411,4l6 de Yaec, 243 en análisis sintáctico LL(l), 166-167 especificación de asociatividad y

precedencia en, 1 18- 1 19 1inealiz:aión de, 398, 399-400 para arreglos cun siibindice, 422-423,

424-425ilrr.s. para cornpilador 'TINY, 134-178.

1.16- 1 .~Xilris., 182ilits., 462 !::ira e~presiones de tilio. 32.3-324 pisa «riiitxiticas :inibigi~as, 1 I J-l 18

para gnimáticas libres de contexto Y3

recorrido postorden de, 277 recorrido prcorden de, 279 recorrido rccursivo de, 410-411 simplificación, 114

Árboles sintácticos abstractos, 9. 106, 109-1 14, 258. Véme tr~rnbidn Árho- les sintácticos

con llamadas y definiciones de lutición, 440-44 1 ,44 1 ihts.

generación de código en tres direccio- nes de. 399-400

generüción de código intermedio de, 398-309

para gamáticas de control de senten- cias, 434-436

sirnpliticación de las gramáticas con atributos con, 269-270,269ilu.~., 270t

árboles sintácticos como, 289 Archivo n n a l y z e . c, 337 Archive a n a l y z e . 11, 336 Archivo c a lc .y, opciones de Yacc

con, 230-23 1 archivo ccde . e, 459-460 Archivo c-ide . h, 459-460

delkiciones de función y constantes en, 460-46 1

Archivo de código fuente tn. c, 26 Archivo de código cample . tm, 25,26,

464-465 Archivo de texto s a m p l e . tilny, 24 Archivo rn r m . c

para el walizador lévico de TINY, 78 para el compilador de TINY, 23, 24

Archtvo parse. c, 1x1 Archivo p a r s e . h. 181 Archivo n c a n . c. 77 Archivo s c ü n . h. 77 Archivo syrntab. e , 336 Archivo s p a t a b . h, 336 Archivo t i.ny . 1.90 Archivo t irijl .y , 243, 250 A r c h i v o ~ t i 1 . c ~ 181, 182 Archivo u t i l . h. 181 Archivo y . n u t p u t para Yacc,

231-232, 233-234ilus., 237ilirs., 238

Archivo y . t a n . c , 226,238-239 Archivo y . tab . h. opciones de Yacc

con, 230-23 1 Archivo y y i n , 89 Archivo yymit, 89 Archivos a n a l y z e , para compilador

TINY, 23 Archivos C, para el compilador de

TINY, 23 Archivos ¿«m, para el compil:rilor Je

TINY, 23 :irchivos -«de' para el coriipilüdor

TINY. 23-24 Archivos de cabecera, para el compila-

&ir i'INY, '3-2.1

Archivos de código TM, generación de, 464-465

Archivos de entrada Lex, formato de los, 83-89

Archivos de especiticación, para Yacc, 226-230

Archivos de salida. \'é(i.se tnmbi4n Archi- voy.ourpi l i .

de Yacc, 226-227 Archivos # i n c l u & , para especificacio-

nes Yacc, 227-228 Archivos parse. para el compilador

TINY, 23 Archivos scan . 23.77 , ,

Archivos :wmt ai;, para el compiledor TINY, 23-24

Archivos temporales, 14 Archivos u: i 1 para el compilador

TTNY, 23-24 Archivos. Véase archivos de C; Archivos

de cabecera; Archivos #incl .ude; Archivos de entrada Lex; Archivos de salida; Archivos p a r c e ; Archi- vos s c a n ; Archivos de especifica- ción; Archivos symtab; Archivos temporales; Archivos de código de TM; Archivos u t i 1; Archivo yy i n ; Archivo y y o v t

k e a de código, de memoria, 346-347, 350ilus.

Área de datos, de memoriü, 336-347,

Área de pila, en la asignación de memo- tia, 347-348

Área de registro, 346, 348-340 V4aw también Procesador

Argumentos. Vénse también P~srimetros en llamadas de función

y procedimiento, 437 Arquitectura Sparc RISCo 3"1 Arquitectura VAX, 340n, ii. Arranque automático, de transferencia,

("Bootstrqping"), 18-21, 20-21 ilus. Arreglos, 488e

corno funciones, 3 19 constructores de tipos jma, 3 15-3 16 direcciones hose de, 4 i 3 en ambientes de ejecución basados en

pilas, 357-358 multidimensionales. 316,423,486e tia acotado, 362-363 pasos por valor de, 383 tablas de transición conio, 6 i -64

Arreglos Completamente subindizados, 423

Arreglos de indice abierto, 3 16 r\rreglos locales, en ambientes (le ejecu-

ción basados en pila, 337-358 \rreglos multidimension:iles, 423' 486e .\rrcglos p:ircialmente subindizados,

423 .airreglos sin liiriitacii,n, en ambientes

de ejeci~ciún hasados en pilas, 162-363

Page 565: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Asrgnación de memoria, 346- 349. Véuse ta~nbrh Memoria dinámica; EsWc- turas dinámicas, en dsignación de memoria

en ambientes de ejecución baqados en pilas, 352-373

en ambientes de ejsuci6n dinhicos, 373-381

para arreglos, 3 16 para el lenguaje FORTRAN77,

349-352,350ilus. para estructuras y apuntador de refe-

rencia, 423-424,424ilus. para la máquina TM, 460 para lenguaje TINY, 386-388 para registros y estructuras, 3 17 para tipo de datos apuntador, 319 para tipo de datos de unión, 318 para tipos de datos recursivos,

322 para variables, 259, 260, 30-301 por declardción, 301

Asignación de registro, 469,471 optimización de código, 473

Asignación de variables, 259, 260, 300-301,349-352. Véase también Asignación de memoria

Asociatividad, 253e en expresiones aritméticas, 117-1 18 en notación de la forma Backus-Naur

entendida, 124 en Yace, 236-238,238ilus. especificación mediante paréntesis,

118 resolución de ambigüedad con,

118-1 19 Asociatividad por la derecha, 119, 124,

125,238 Asociatividad por la izquierda, 117,

118-119, 167 en la notación de la forma Backus-

Naur extendida, 124 en Yacc, 237-238 recursión por la izquierda y,

157-158 Atributo base, 266-268, 267t, 268ilus.,

273-275,275ilus., 276,278, 279, 282-283, 285-286,287-288

Atributo dlype, 265-266,266t, 266ilr<s., 272-273.278-282.288-289, 292-293,294,294ilus.

Atributo err. 310-313, 31 It Atributo etype. 284 Atributo isAddr, 422 !\tributo isFloar, 284 Atributo nestlevel, 3 10-31 2, 31 1 1 Atributo pcode, 408, 408t Atributo postfi, 33Ye Atributo rymrab, 310-312,31 I r Atributo vol, 263-265, 2636 264ilrrs.,

265t, 265ilus., 268-269, 2691, I69ilc<,s., 27 1-272, 273-275. 27Siiris., 276, 277-278, 285-286. 790

.Atributos, 10, 259-261 aumento de la eficiencia de. 72-74

Atributos diniimicos, 260-261 Atributos estáticos, 260-261

&-bol de aniilisis gramatical recorrido de izquierda a derecha y, 289-290, 292

cálculo de, 291-293 como parjmeu's y v a l o ~ s devueltos,

285-286 definidos, 278 yjficas de dependencia para,

278-279 heredados en, 278-281 heredados y, 283,283-284 m&todoc algorítmicos para evaluación

de, 279-284 modificación en ahibutos sintetizados,

293-295 sintetizados y, 283,283-284 tablas de símbolos como, 295

Atributos sintetizados, 283-284 cálculo de, durante el análisis sintácti-

co, 289-295 como pmhetros y valores devueltos,

285-286 de nodos de &-bol, 134- 135 de tokens, 33 definidos, 259 en el análisis semántica, 258, 259 cstmcturas de datos externas para

almacenamiento de, 256-289 heredados, 277.278-284 orden de evaluación para, 271,

276-277,343r sintetizados, 277-278, 283-284

Autómatafs) tinito(s) determinísticos (DFA, por sus siglas en inglés), 9 1 -93e, 94r, 250-252e

acciones sobre tokens de, 53-56 análisis sintáctico ascendente y, 197,

198 análisis sintáctico SLR(1) y,

213-214 definición, 49-5 1 ejemplos de, 51-53 elementos LR(0) como, 204-206,

205ilus., 206ilus. elementos LR(1) como, 217-220,

22Oil14s. implementación de, 59-63 minimizarión del número de estados

en, 72-74 para análisis sinttrctico LALRO),

224-225, 225ilus. para análisis sintáctico LR(I), 220-223,

223ilus. para analizcidor sintáctico de scnten-

cia if, 215ilrr.~. para el analizador lexicográfico de

TINY, 75-77. 77ilirs. traducción de expresiones regulares

en. 64-74

traducción de los aut6matas finitos no deterministicos en, 69-72

Autómata(s) finito(s) no determinístico(s) (NFA, por sus siglas en inglés), 3, 31-32.47-M, 53, 56-59,92e, 94r

constmcción de autómatas finitos determinísticos a partir de, 69-72

definiciún, 57-58 ejemplos de, 58-59 elementos LR(0) como, 202-204,

204ilus. impletnentaciún, 59, 63-64 simulación utilizando construcción de

snbconjuntos, 72 traducción de expresiones regulares

en, 64-69 Autómatas finitos, 3, 31-32,47-64. V&se

tcrmbién Autómata(s) finitos deter- ministicos (DFA por sus siglas en inglés); Autómatafs) finitofs) no detministico(s) (NFA, por sus siglús en inglés)

de elementos, 202-206 implementacih en código, 59-64 para identificadores con delimitador y

valor devuelto, 5 4 t h para identificadores con transiciones

de error, 50ilus. para numcros de punto flotante, 52 tablas de transición para. 61-64 teoría matemiitica de, 94r

Backpatching, 14,428,432,458 Backus, John, 3,98 Bases, para código relocalizable, 346n Bloques, 302. Vt!ase también Bloques

básicos de código de estructura dinámica de memoria

(heap), 377-380, 378ilus., 380ilc~s., 380-381

en código, 3,64365,474 Bloques anidados, en código, 364-365 Bloques básicos de ciKtigo 477-48 1,

478ilus. como nodos de gráfica de flujo,

475-477 gráficas aciclicas d'igidas de, 416n,

477-48 1,478ilr~s. llamadas de procedimiento, 47% optimización &, 474

Bloques de memoria fragmentados, 377 Bloques let, cálculo de niveles de anida-

ci6n en, 310-313, 31 I r Bloques no básicos de código, 474 Bucketiist, 336 Búsqueda hacia delante, 53-54, 184- 185,

197, 200, 216-217 de un carácter simple. 46 Ile un sínibolo simple, 13 elementos LR(0) y. 202 clenientos LR( 1) y , 217-218 en análisis sintáctico descendente,

1-13-144. 180

Page 566: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

en análisis sintktico LRíI) contra iiniilisis sintktico SLK(I), 222-273

para iirialimdores sinticticos SLR(k), 216-217

pn~hlenia de, 16 producciortes de error en Yricc y,

247-250 BÚ\qiieclü lineal, N O Iiílsquedas binarias, hacietido los nnali-

~ a d o n s léxicos más eficientes con, SO

Uúsilucdas hacia delante c~tiihinüción, en ariálisis sintictico

LAI,R(I ), 124 en listados Yücc, 232-234 espontáneamente generadas, 226 generadas cspont~ncarnente, 226 propagaciún, 2S5-226

Cidcna vacia íü), 35 autóiiiütas finitos no determinísticos

y, 56-57, 64-65 excluyendo repeticioi~es, 41-42 gramáticas pana lenguajes incluyendo,

105 Cadena yytext , 85, Y6 <::idenas, 3 1. Vkrse tutiihiéri Cariictcres;

Lztras; Metacaracteres árboles múiti[~les de ali5lisis gr:iiiiati-

cal para, I 14- l 16 aritórnatas finitcts no deterniinísticos

y , 56-59 c61ciilo de dirección en enteros,

297-298 chdigo internteilio y objetivo como,

407-4 10, 408t, 40% corno reglas de granática libre (le

coii~cxto BNF, 98 complcrrientarias, 42-43 concatenación de, 36-37 deriv:ici«nes múltiples para, 106- 107 qxesiones regulares como, 34-35,

38-41, 41-43 f k m s de sentencia de, 129 praficas de dependencia para, 27 1 literal, 43-44 números binarios como, 41-42 principio de la subcadena más Larg;~,

46 cilci~lo de, 290-293 Cálcnlo de atributo. 270-295 Cilculo Lambda, 37ln Ciilculos de dirección, en gcneraciih de

código, 416-41 8 iairihio de atributos here&dos m ,

293-295 C'ritnp<r :;c!.:: ;1.!0;.:!e, 4 17 C:iillpo '.y.,<?. 135 C.':trnp«s de atributos. en 6rholes 4ncacti-

cm, 06 ('iiiiipos ile repisiro

~~ilciilo dc i-eSct.enci;n liara, en gciic- s:ici<irt de c~idigo. 417. 173-428

c6digo del coinpilatior C de Borlatid para, 445-46

código del cornpilador C de Sun para, 450-45 1

Crimpos s t r v a l , 410-411, 41 Itt Carácter asterisco (*), 35, 36-37, 97.

Vktse t(trnhi611 Operación de repeti. ci6n

fi~riicter caret (A), 42-43 como constructor de tipo apuntador

en Pascal, 3 19 Cariicter de barra vertical (/), 35-36.

Véase trtnzbiPn Selección entre (11- temativas en gramiticas libres de contexto

en especificaciones de reglas de Yacc. 227

tipo de drttos tiniún definido por, 3 19 Ci~rkter de diagonal inversa (\), 8 1, 82,

O0 Carácter de espacio en blmco, 46 Cr6cter de signo monetario ($)

cáIciiIo de conjuntos Siguiente y , 173-174, 176-177

marcación del final del archivo de entrada con, 152- 153, 154

C. ', meter de d m y a (-J. eii la conven- cihn de nombrado de funciones del compilatlor C, 447

C':irácter guión (-), 42, 82 Cwtícter Nileva-línea, '46 Caráctcr Tab. 46 Carácter tilde (-), 42 Caracteres, 78. Vkse futnhi<;n Letras;

Metücaracteres; C~idenas Ciiracteres de escape. 35 Ciirücteres en rmyusculas. ct~nversión a

ininúsculas, 87-88 Cw:icteres no alfa~~uméricos. 5011 Caracteres y símbolos legales, 33-35 Cargadores, 5 Caso base, cii definiciones recursivas.

102- 103 Cerrndula, 35. 36, 371 rt. Vkise tcnt~hii%

cerradura E

Cerrcidura (coino pariimetro de procedi- miento), 371-373, 372iltrs,, 373iltr.r.

Ccrcidura de Kieene, 36,371n Cciradura s, 69-70, 92r, 204205, 217,

226,371n Chitmsky, Noani. 3. 132- 133, l 42r &le Fetch-execute, de la nihquiiia TM,

4s 4 Ciclos. 474-475. V&sr ri~rnOi¿n Ciclos

infinitos eii grarniiticas. 159

Ciclos infinitos, 159. V6mr tund~itn ciclo.\

Clases de caracteres. convc~ición de L.ex I I ~ . 82-83

Código de niiquiiia, l Chdigo Diana, 489r Código en C, 84-87,89,90-91,227-228,

229-230 Código cn tres direcciones, I l. 3%.

399-402,477n, 486e, 488-489e conlo un atributo sintetizado, 407,

408.4 I o contracódigo P, 406-407 estnicturrts de datos para, 402-404 gramática con ittributos para. 407,

3.08-4 10,409t para ciilculos de dirección, 4 17 para expresiones aritniéticas. 484e,

485e para funciones y procedimientos,

438 para referencias de apuntador y

estructura, 425.426 para referencias de arreglo, 419.420 para setitencias de concroi, 430 programa TINY en, 400-402 traducciún de cúdigo P en, y vicever-

sa, 4 14-4 16 Código ensanihlador, 397-398, 447-448,

452-453 para expresiories aritméticas, 443-44,

448-449 para Ilaniadas y definiciones de fun-

c i h , 47-4-48, 452-453 prisa referencias de arreglos, 444-445,

439-450 para referenci:ts de campos y apunta-

dores, 45-446. 450-45 1 para sentencias "if' y "while", 446,

451-452 Código inalcanzable, 469-470 Código intermcdio, 11, 14, 397-398,

398-399, 489r. Vrktsuse tatirrbilrt Có- digo P; Código en tres direcciones

ccídigo P como, 404 como un atributo sintetizado, 407-410 construcción de gráficas de flujo a

partir de, 475-476, 47hilus. generación de cúdigo objeto desde,

411-416 para funciones y procedimientos,

437-439 y objetivo corno, 407-4 10

Código muerto, 469-470 Código objeto (Ob~eUvo), 1, 4. 259, 260,

199 como un atributo sintetii:i&, 407-410 de gráficas acíclicas dirigidas, 478-479 generación de, 398-399 gcncrüción de, desde código intenne-

dio, 41 1-416 I'iídigo P, 11, 398, 399, 404-407, 477n.

48he, 48Xe, 489r 'unto un atributo sintetizado, 407408 ct~ritracódigo en tres direcciones,

-106-407 cr;imitica con atribtit»s para, 407.108, .,

4081. 41 2 h \ .

Page 567: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

pasa cilcuios de dirección, 417-418 \para definiciones y limadas de fun-

ción, 441-443 p m expresiones aritméticas, 484-485e para funciones y procedimientos.

438439 püra relerericias de apuntador y

estructura, 427-428 para rcfcrencias de arreglo, 3110-421.

122-423.424-425ilus. para referencias de esuuctura, 427-428 para sentencias de control, 430-432.

433,134436 procedimiento genCode en, 412ilcw. procedimiento para generación de, 41 1 programas TINY en: 406iiccr. Trailucción de código en tres direc-

ciones en, y viceversa, 414-416 Código nlocalizable, 5 , 346n Código U, 489r Coerción, 332-333 Colisiones. en tablas de crilculo de di-

rección, 296, 297-298 Conlando m n , 26 Cornando g , para el simukador TM, 459 Comando n, para el simulador TM, 459 Comando h a 1 t, en cikiigo de tres direc-

ciones, 401 Comentarios

iiut6m;itas finitos determinísticos para, 52-53

como pceudotokens, 44,46 en lenguaje TINY, 22, 75, 90 expresiones regulares para, 44-45

Coinenhrios estilo lenguaje C. 44-45, 53ihs.. 61, 62, 87-88, 92-93e

como parimetros y vnlores devueltos, 285-286

Compactación de memoria, 381,195-396e Compactación. V&se Compactación de

memoria Compilación TINY, 24-25, 79 Compilador Algol60, 30r Conipilador C Sun 2.0 para estaciones de

trabajo Sun SparcStations, 16, 29r, 396r. 398,443-153.487e. 489r

Compilador de C Borland 3.0 para procesadores 80 x 86 de lnfd, 398, 443-448, 485e, 187e

Compilador de compiladores. Véase Ge- neradores de analizadores sintácticos

Compilador Gofer, 396r Compilados TINY, 111- 142e

archivos y marcas para, 23-25 estructura de &bol sintiictico para.

134- 138, 136- l 3lliirrs. lenguaje T I W y, 22 listado püra. 502-544 rccnperaciiln de errorcs en, 250

Conipil;tdores. 1-2. V & w trimbiiti Com- pilados dc t\lgoibO: C'ompikidorcs de C; Compiladores crnrados: Cotii- pilndor de FTIIITRAIL': i4iqucte tle compiladorcs Giu: (:ot~ipil;~dor

de GoFer; Compilador de Modula-2; Cornpilador de Pascal; Generadores de analizador sintActico: Compila- clor de TINY; Yacc (yet anothti. compiler-cornpiler)

ambiente de ejecución para, 345-346 an&sis semántica mediante, 258 a~ializadores sintácticos para, Y6 cuestiones eswcturales respecto a

los, 14-18 de una pasada, 16 definición del lenguaje y, 16-1 7 diagranias T para, 18-2 1 directivas de ctepilraeión para, 17 efectos de la optirnización sobre el

tiempo de ejecución de, ,168 estructuras de datos principales en.

13-14 etapa inicial y etapa final de, 15 fases de, 7ilus,, 7-12 historia de los, 1-4 manejo de errom y excepciones en. I 8 múltiples de pasadas, 258-259 partes de análisis y síntesis de, 1 5 pasadas de, 15-16 portátiles, 15 programas de prueba estándar para, 16 programas relacionados con, 4-6 referencias sobre. 29-30r transportación de, 18-2 1

Compiladores c~uzados, 18, 20, 21 Conipiladotes de C, 16,29r, 396r, 398.

443-453, 487e, 4489 Véase :oiniiién Compilador C Borland 3.0 para procesadores Intel SO x 86;

Compiladores de Modula-?, 16 Compiladores de múltiples pasadas,

258-259 Conipiladores de I'ascal, 1 1 , 16 Compiladores de perfil, 472 Compiladores de una pasada. 16 Compiladores FORTRAN, 30r Completar las gramáticas con atributos,

270 Computadora con conjunto de instmccio-

nes reducido (RISC, por sus siglas en inglés), 25,156,469

Compuraduras, John von Neumann y las, 2

con atributos S, 277 Cotitlictns de análisis sintáctico. Véaw

tiirnhién Problema del else imbiguo; Conflictos Reduce-reduce; Conflic- tos Shift-reduce

Reglas de no ambigüedad pan, en Yricc, 235-238

Conflictos Reduce-reduce. 207' 21 1 con Yacc, 236-238 reglas de no nmhigiiedad para, 2 13

(:oiiflictos Sbift-reduce, 207, 21 1 , 213. 235-236

Conjiiiito ile c:irxteres ASCII i Amcr.i- can Sianilard Cede h r InS~irriiütior~ l~~tcrclimgc), 74

Coiijutito de hemiinientas de constnic- ciiíti para compilador de I'urdtie (PCCTS, por sus siglas en ingl&s), 196r

Conjunto de programas de prueba, 1 6 Conjuntos de potencia, 57 Coniitntos Primero. 144, 168- 173.

190-IYle definición, 15 1 , 168- 169 para analiz:idores sintácticos LL(k),

180 para gramática de expresión de ente-

ros, 170-171, 171r, 178-179 para gramálica de secuencia de sen-

tencias, 173, 179- 180 para gramática de scntencia %",

171-172, 17% 179 conjuntos regulares, 39 Conjuntos Siguiente, 144, 168, 173-177,

190-191e definicicin. 152 en irnálisis sintáctico SLR(l), 210. 21 1 p m analizadores sintácticos LL(kj,

180 par3 gramática de expresión de ente-

ros, 174-176. 1761, 178-179 para gramática de secuencia de sen-

tencias,. 177, 179-180 para gramática de sentencia "it", 179

Cotisistencia de gramáticas con atribu- los, 270

Constante f a l ce , 13% 432433,433434 Coiistante tr:ie, l39e, 432433,433-434 Constantes, 43 Constnntes na t, 51-52 Constantes ii~~inber. 5 1-52 Constantes numbricas, aiitómatas llnitos

detenninísticos para, 51-52 Constantes s igned f i k t , 51-52, 82-83

asignación de memoria para, 347 descnptores de registro y direcciún

para, 479-48 1,4791, 480t, 48 l t en el archivo cude . h, 460-461

constructores de tipo cmesiano en, 317

Constructores de tipos, 3 14, 3 15-320 Consuuctores de valor, en lenguaje ML,

319 Contidos de programa (pc, p r sus siglas

en inglés). 348 Contexto, definición en, 132 controlado por rübla, 63. 154- 157 Convenciones de Lex, 8 1-83 Convenciones de metacariicter, 81 -83,

83:, 98-99 Convenciones li'xicas. para el lenguaje

I'INY, 75 Convenciones. V&se Convenciories

linicas: Convenciones de rnzraca- mcteres

Conversiiin de tipo, 32-333 Corchetes o p:trinteíis cniidra<los I [ / 1.

.iL, XZ, i 24- 125 í:orrccciiln <le erroreh, i 8.3

Page 568: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Corrección de errores de distancia riiíni- ma. 183

Cuádruples, para código en tres tlirec- ciones, 402. 103ii11,s.

Cribos, 211 tablas de cilciilo de dirección. 296-297, 297ilus.

(:iierp« de una erprehión let, 309

Ihtos de iotigitud variahle, en ambientes de ejeciiciítn hasatlos cn pikis, 361-36'. 391c

Ilatos de menioria para la iiiiy~iina TM. 454

11;itos estAlicos, asigiiación de menioria para, 147

1)ntos glvbales, :rsignación de iiiernoria liara. .M7

Ibclaración antes del uso, 132, 301-302 Declaracidn autorniítica, 301 Declaración c&r, 240. 314 1)eclaración Class, 320, 376, 376ilus..

393-394e 1)eclar;tción CO":MO?J, 350n, 350-351 Decliiración cnrist, 299,114, 347 I)eclaración co:lsr.ar; c, 43 Declaración da!,itype, 328-329 Oeclaración de constantes, 399, 300 Ikclaración c!i;b le, 240, 314 Ikclaración E x p ? y p e , 135

definición, 277 Declaración ext 9 m, 301 1)eclaración 1 i t e ra l , 43 Declaración por uso. 100 kcli~ri~ciítn S?:,> t i c , ,301 1)eclaración st i -uct i , 299

~tcirnhres de tipo y declaraciones en lenguaje C asociados con, 32 1-322

r1cclar:tción 'TcKenType, 33 I>eclaracihi type, 299, 328 Dcclaraciún tysede t. 299 Declaración u.iior:, 111. 299, 317-319

administradores de proyt~to en, 6 11eciar:ición 'kunion, en Yacc, 210-241 tkclaraciones y nombres de tipo en

lenguaje C asociados con, 32 1-322 producciones de unidad, 141e sistema Unix. 4 Yace incluido en, 256r

Declaraciones, 10, 139e. I 4Oe. Vdme turrrhién Declaraciones automáti- cas; Declaración de clase; Declara- ciones colaterales: Declaraciones de constante; Declaraciones de tipo de datos; Definiciones: Declaracio- nes explícitas; Declaraciones por adelantado; Declaraciones de fun- citin; Declaraciones de prototipo de furicidn; rleclümciones gtohales; L)ecliiraciones implícitas: Declii- r:icionec locales; Declamciones :iiii~ladas: Decl;ir:icionei <le proce-

racioiics eit5tic;is: Declaraciones de tipo; Declaraciones de variahle

al mismo nivel de anid:tción, 306-308 c~iniportaiiiiento de tablas de símbolos

y, 298-301 cm exprcsimes k t , 309-3 10 tlc t i p de &tos, 1 I I cn c6digo Yacc, 229 en grnniáticas de expresión ariitnt!tici~

simple, 308-3 13 eii ~nbl:is de sínihol«s, 295 cxtciisión de, 300-301 tiempo tle vida de, 300 verilícación de tipos de, 329-330

I>cclilracioncs ;irtidatlas, 363, 364-365, 368-360

I>ecl:iraci«t~es colatcriiles, 307 1)eclliraciones de fiinción, 299-700,

307-308, 341-342e, 437-439 Declaraciones de procediniimto. 1 4 1 ~

299-300, 307-308, 437-439 Decltu.riciones de prototipo de Sunciún, 308 Declat-aeiones de tipo, Véase t~irnhién

Declaraciones de tipo de datos i)ecl:tlaci«nes de tipos de detos, l I 1,

259, 260, 3 13-3 14. V4use tirmbiin Declaraciones de tipo

en tahlris de sí~nholos, 295 211 Yace, 239-241 grainiítice simple de, 265-266, 288-38')

I)eclaraciones de variables, 299. 3011-30 l gramática simple de. 265-266,

288-239' 291 tipo de d:itos cn, 314

I)eclitracinnes estiticas, 27, 300-301 Dec1:iracicines estilo Pascrtl, gramática

para, 339-34Oe Declaraciones explícitas, 299 Declaraciones Forward, 308 »eclaraciones globales, en el lenguaje

C-Minus, 27 Declaraciones implícitas. 209-300 Declaraciones 1oc:iles. en el lenguaje

C-Mintts, 27 Ikclaraciones recursivas. 307-308 Declaraciones secuenciales, 307, 310 Definición de leriguaje, 16-17 Definición del ambiente de un procedi-

miento, 367 Definiciones, 301, ;t76rt, 476-477

del código objetivo, 3119 Definiciones de alcance, 476-417 Definiciones de función, 486e

código intcnnedia para, 437-439 compilador C de Sun, código para,

452-453 conipilador de C Rorlitml. código para,

U7-448 t:ri CI i w h i v ~ i::oii~. . h. 1M)-461 priredirnicnto de genefiición de ctídigo

pir:1, 435443 riritplificación de grarn;itic:rs con iitri-

hims con. 208 I3cfiniciooes dc tipo, 120

Definiciones f~)rmaIes. de lenguajes de computadora, 16

»efiniciones regulares, 37 l)elimit:idorcs, 34-45. 46. 54 1)eliriiitadores de comenti~rio, 44-15 Deliriiitadores de signo porcentual

(C/c%). 83-84. 84, 85 Delimitatlores dc token, 46 Depuradores, 6, 238-239 Derivaciones. I(B- l O1

irboles de análisis grnmaticnl y, 106- 109

contra reglas gtam~ticales. 101 definición formal de, 129 por la derecha. 109, 1 14, 129 por la izquierda, 108- 109, 1 14- l 16,

129, 153-l5J por la izquierda y por la derdia,

108- 109 reglas gramaticales y. 1 02- 106

Dcscriptor de dirección, 479t, 479-481, 480t

Descriptores de registro. 470-481, 4XOt, 48lt

Desplrizamielitos (Offsets) cjlculo de direcciones coii, 336.358%

357ilirs., 358iius., 365-366, 393e generación de código intermedio y; 399 para refereiicias de :ipuntador y estmc-

tums de registro. ,423-425.3: :;'i!.s. subíntlices de arreglii y, 418-.:

I>etccción de ciclos, 475 Detección de errores, en análisis sint6c-

tico ascendente, 245 [>FA de estados mínimos. 73, 74 Diagramas de sintaxis para aiirílisis sin-

tictico, 123, 125- 128 estructuras de datos hasadas en.

194- 195e para expresiones aritméticas, 126-127,

127ilus. para sentencias "if', 127-1 28, 1?.81!rr.s.

Diagranias T, 18- 19, 19-21 iius., 211-Pfk Dígitos, 43. Viase rurnhién Enteros;

Números en constantes numéricas, 51 -52, 359,

260 en expresiones regulares, 13 1- 133 en gramiticas con atributos, 268 en g~~máticas con tipos de valor de

punto flotante y enteros rnercl:id<is, 283-284

en gramiticas de núnlems cb , !w\e, 273-275.282-283

en gramáticas de nlímems sin signo, 262-264, 266-268, 271-272, 276-277

cn Lex, 82 significativos. 259, 100

Dirección base, de un iirreglo. 418 Dircccionatnienro abierto. 3X-359

Page 569: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Direccionamiento abierto. en tablas de cálculo de dirección, 296

Direccionamiento de byte, 12 Direccionamiento indirecto, 417-418

de registro, 12 Direccionarniento. V&se trrnrbién Di-

reccionamiento de byte; Direccio- namiento de registro indirecto

Directiva %iype, 240-241 Directivas de depuración, 17 Directiva del compilador, 17 Discriminante, de tipo de &tos de

unión, 3 18-319 IXvisión, 117, 193e, 283-284 División de punto flotante, 283-284 División entera, 283-284

Ecuaciones de atributos, 258, 261n, 270-27 1. Viase también Reglas semánticas

definidas, 261 llamadas de pfocedimiento en lugar

de, 287 n~etalenguajes y, 268 para expresiones ¿uitmt'ticas,

272-273,284 para números con base, 273-275 para números sin signo, 263-264,

271-272 Ec~iaciones, reglas gramaticdes como,

129-130 Editores, 6 Editores basados en estructura, 6 Eficiencia

de los autómatas finitos <leteminísti- cos, 72-74

Ejecución de programa, organización de la memoria durante, 346-149

Elección entre alternativas, 35-36 código para, 145-15 1 con gramáticas libres de contexto, 97 en análisis sintáctico LL(l), 157 en expresiones regulares, 38-41.

41-43 iinplementación para autómatas fin¡-

tos no detemiinísticos, 65-66 precedencia de, 37.38

Elementos completos, 202 Elementos de cerradura, 206,232 Elementos de núcleo (kemel), 206,

232-234 Elementos iniciales, 202 Elementos LALR(I), 224-225,250e,

25 1 e, 252e Elementos LR(O), 201-206, 250e, 25 le.

2 5 2 análisis sinthctico I.ALR( 1 ) y, 224 autómatas finitos de, 202-206

Eierrientrrs LR(1 j. 250e, 251e anJlisis sintáctico L..ILR(I j y, 221 uutóinatas tinitos de, 217-220,224.

22.111 Elcrncntos SLR( I ), 2 5 0 ~

Elementos. Véme Elementos cerrados; Elementos completos; Elementos iniciales: Elmentos de núcleo (Kcrnel); Elementos L,ALR( 1); Ele- mentos LR(0); Elementos LR(1): Elementos SLRt 1 )

Eliminación de suhexpresihn comíln. 469,469n

Encadenümicnto. 365. Véuse tnnthién Encadenamiento de acceso; Enca- denanliento por separado

Encadenamientti por seyarado, en tablas de cálculo de dirección, 296-297, 297ilus.

Ensambladores y lengtiaje ensamblador, 3, 5

Enteros. Vdase tambi& Dígitos: ;tsiynación de memoria para grandes,

347 cadenas de caracteres de cálculo de

dirección en, 297-298 grümática de números tipo, 33% rniximo común divisor de, 353-354

Entradas pretletenninadas, en tablas de análisis sintáctico LL(I), f91e

Equivalencia de declaración. 328-329 Equivalencia de noinbre, 324-329,

344, Eyuivatencia de tipos, 322-329, 344r Equivalencia estructural, 324, 327,

328-329, 344r Errores de compilación, 27-2% Errores de semántica, 27-28e Errores de sintaxis, 274% Errores en tiempo de compilación, 18 Errores estáticos, 18 Escritura polirnhrfica, 333-334, 344r Esqnemas de tipo, 333 Estado de error. Véme trirnbién Estados

de atitómatas finitos determinísticos, 50-5 1.54, 73

Estado DONE, en autótnata finito deter- minístico de TINY, 76

Estado error, 50 de Yacc, 247,2248-249,250

Estados de aceptación. Véase rantbién Estiidos

en autómatas finitos, 48.49-51 Estados de inicio. Véase también Estado

de autómatas finitos deteminísticos, 49,53-55

en autómatas finitos, 48 para gramáticas aumentadas, 203

Estados. Véase tarnhich Estados de aceptación; Estados de emtr; Esta- dos de stürt

cerradura E de, 6Y-70 de aiitúmatas finitos, 48, 49-5 1 ,

57-58 de elementos LRt 1 ), 2 18-220 cn an;ilisis sintictico LR(L1. 221,

222-223 en listados de Y:\cc, 232.735.

233-734illl.v.

en tablas de transición, 61-64 niinimiración del número de, 72-74

Estimación conservativa de informacihii de programa, 472

Estimulación estática, 41 3, 416 Esiiuctura de datos de pantalla, 3 0 ,

392e, 396r Estructura de datos del diccionario, de

tablas de símbolos, 295-296 Estructurajictor, 125, 127 Estructura prrqpm, 101-1 02 Estiuctura program, en el malizador

sintáctico TINY, 243-244 Estructura treeNode, 462 Estructuras; 357-358, 423-428. Véase

trimbién Estructuras de datos; Tipo de datos: Campos de registros; Registros

Estructuras de árbol de búsqueda, 296 Estructuras de datos. Véase fornhirJn

Arboles siniácticos abstractos; Arre_elos; Pilas de llamada; Tipos de datos; Gráficas de flujo; Méto- dos: Objetos; Campos de registro; Registros; Cadenas; Tablas de sím- bolos; Arboles sintácticos

basadas en diagramas de sintaxis, 194-195e

de longitud variable, en ambientes de ejecución basados en pilas, 361-363

generación de código de referencias para, 4 16-428

grificas de flujo como, 475-477. 47hilits.

para almacenar valores de atributo, 286-289

para generación de código, 398-407 para in~plementación del código en

tres direcciones, 403-2404 para optimización de código, 375-481 para tablas de palabras reservadas, 80

Estructuras diniimicas (heap), en asigna- ción de memoria, 17,347n. 347-348, 378ilus., 3;JOilus., 396r. Véase también Asignüción de memoria

administración autom&tica de, 380- 38 1

administraci6n de, 376-380, 379ilu.s. Estructuras locales, en ambientes de eje-

cución basados en pilas, 357-358 Etiquetas, 428,432 Etiquetas de salida, de sentencias de

controL 43 1-432 Evaluación de cortocircuito, 428,433.

487e Evaluación descuidada, 386, 396r Evaluailores de atributo

constmcción automiticn de, 344r postordcn recursivos, 277-278

Expaiisión de macin. 413,415-416 Exponentes

en constantes nurniric;rs, .TI-52 iiiultiplicnción y, 470

Page 570: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

not;rzic(n para, 43 siiina de, par8 expresiones iiritrnéticas

enteras' 19% Expresión irt, 3iB-3 10, 3 1 Ir, 3 12-3 13 Expresiones aritméticas. I10e, 25.5~.

V&iw tnrnhiin Expresioiies aritmi- ticas enteras

ambigüedad no esencial en. 123 ;analizador \intktico ascendente para,

1 (19, 700t analizador sintáctico SL.R(l) para,

21 ir, 211-212, 2121 aiittiniata finito detenninistico para

gramática de, 205-206, 206ilus. aiitúitiata finito no :fetcrrtiinístico para

grilrnática de, 204, 701ilu.s. calculadora rccursiva descendente

parü, 116- 15 1, 148- 149ilus. cálculo de conjuntos Primero para.

170-171, 1711 cálculo de conjuntos Siguiente para,

171-176, l76t código en tres direcciones pim.

399-402,414-416.481e código P para, 404-405. 1 1 11.416.

484-485e código para, 146-147 cc(dig« para el eompilador de Borlaiid

c, 443-444 código para el Cornpilador Sun C,

148-449 constnicción de tablas de itnálisis

sincáctico LL(1) para. 178-179 declwaciones de tipo en Yacc para,

23-24 1 diagtsmas sintácticos p m , 126- 127,

127ilit.s. división en, 283-284 co análisis sint6cticct LL(I), 157 en cGdigo Yacc, 230-211, 241-242 en lenguaje T I W , 22, 133-1311. en notuciún extendida de la forma

Backus-Naur, 123- 125 cvaluador de atributos rccursivo dc

urden posterior pani, 277-278, 278ilu.r.

expresiones booleanas en, 192e grificas de dependencia pira, 272-273 gramática LL(1) para. 160- 162,

16Oilu.s., 1638, 165, 166- 167 gramálicri para, 202 gr~máticas con atributos para, 264-

265, 277-278, 283-284, 284t, 290- 29 1, 29 !-7%,294. 308-3 13

ruodifi'ieación de la calculadora recur- siva descendente de, 193- 194e

iiíimrros de punto tlotante en, 283-284

prwedcncia en, L 16- 1 1 S i-egl.rs de seniántica y gramítica p:trn.

269-270.2691, 270t, 272-273 SinipliSicuci<ín tic !:i gr:iiii:itic:t L ~ I

:itrihoto, pmi, ?(j9-270 t i y o i incicl:irlos m 332.333

Yacc generador de analizadores sintic- ticos para la granranrática de, 227-230

Expresiones booleanas, 22, 133- 134, INe, 192e. 428, 463. Veuse k r n - hi& Operadores de comparación; Expresi«nes de prueba

Enpresionw completamente entre parén- tesis, 1 18

Expresiones constantes, en el Lenguaje 'nNY, 134

Expresiones de estructiira (e.xp)> 130 en árboles de anilisis gramatical,

107-109 en especificaciones de Yacc, 229 en expresictnes let, 309 cn gramáticas iunbiguas. 114-1 15 cn gramáticas libres de contexto,

08-100, 100-101 en listados de Yacc, 234 en sintaxis abstracta, 110-1 1 1

Expresiones de operador, en lenguaje TINY, 134, 137ilus.

Expresiones de prueba. V~u<-c trirnhiín Expresiones booleanas; Optimi- 7.ación

de sentencias de conirol, 482-484 Expresiones de reconocimiento, en el

lengiiaje TINY, 134 Expresiones de subíndice, 471-423,

424425ilus., 48% Expresiones de tipo, 313-314. 314-320

con declaraciones de tipo, 324-326, 326iiu.s.

determinación de equivalencia de, 322-129

grarnitica simple para, 323ilus., 323-324

Expresiones "If', 433 Expresiones if-then-else, cn ecnaciones

de atributos, 268 Expresiones lógicas. genecición de C M -

y« para, 428, 432,-433 Expresiones regulares, 3. 3 1-32, 31-47.

91e. 13Xe, 255e hisicas, 35, 38-41. 64-65 extendidas, 41 -43 cadenas como, 34-35,38-41,4143 contra gramiticas libres de contexto,

95, 97-98 convenciones de Lex para;81-83 definición, 35-41 en lenguaje TINY, 75-76 extensiones para, 41-43 formas de, 38-41 gramáticas libres de contexto para,

104- 105 1engtt:ijes generados por. 34-35 nombres para, 37-38 operaciones en, 15 pira tokens de lenguaje de progrüina-

c i h , 43-47 re«ri:t m:ttettráiic;i de, 9Jr rr;icl~icci(,n cn aui6riiatas finitos deter-

iuinísticos. 61-71

Expresiones. V i m ~ tnrnbien Expresiones itritrnéticas; Expresiones rcgularcs b6sicas; Exprcsiones Bwlci!ras; Expresiones c«nst:tntes: Expresio- nes de identific:idor; Expresiones "ir'; Expresiones 'if-then..clsen: Exprcsioncrs witm6ticas enteras; Ex- presiones let; Expresiones Ioigicüs; Expresiones de operador; Espresio- nes regulases; Subexpresioiics; Ex- presiones con subíndice; E?.presi«- nes de tipo

en lenguaje TJNY, 22, 133- 134, 134437

evaluacióu, 2.59, 260 veritlcacibn de tipo de, 33 1

ExtensiGn de declaraciones, 10(1 : ) I Extracción. de pilas, 153, 167, i lih-187,

152

Factor de escala, 358, 4 18-4 l 'i Factonzrición por la izyuierdíí, 317,

162- 166 False, Saltos de, $29 Fases del compilador, 7-12, I:i;ación, 259-260, 375, 376, 381-182 Fijaci6n dinimica, 375, 376 I'lzx, 8 1 Forma Backus-Naur (BNF, por ms siglas

en inglds), 140- 141 e, 258. V h e i«mhién Forma Backus-Nmir Extendida (ENNF, por sus siglas cn

de las gramjticas libres de ctnirexto, 98

del lenguaje C-Minus, 492-496 jerarquía de Chomsky y , 131-1 33 para la gramática del lenguaje TINY,

133-134. 134ilus. para sintaxis abstracta y concreta, 1 1 1 prohlenias al traducir la BNF a la

forma Rackus-Naur extendida. 151-152

reglas de no ambiguedad y, 121 -122 reglas gramaticales para, 98-100

Fwma Backus-Naur Extendida (EBNF). 123-128

análisis sintzíetico recursivo descen- dente y, 1%

('alculaíiora reci~rsiva clescentlciite p:iraaritmética entera simple y, 111.8- 14Yil~is.

diagramas de sintaxis de, 125- 128 elección de tradticcibn y oper:iciones

de repcticih en cridi;u con, 1-$5-lil

.Ierarqiiia de Chomsliv y, 13 1 - 13.3

Page 571: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

F w m ~ s de sentencias por 1 200-20 1, 252e

fp mtiguo, 353 Función booleana hen, 3 10. Fililción &en. para la niáquina TM,

46 1-462 Función ccdeGen para el compilador

TINY, 461-462 Función m i t Back~p, 461 Función ernitco-inent. 461 Función cnitxestore, 461,464 Función emi¿R>l, 461,463 Función emi tRX-Xcs, 461, 364 Función e?iiRO,461 Función enitskip, 461,464 Función factorial

programa de muestra en C-Minus para, 27

programa de muestra en TINY para, 23, 78, 79, 137-138ilux., 335, J87e

programa simulador de TM pnra, 458-459

Función Eiald-o Efset, 425-428 Función GenExp, 462,483 Función genStmt, 462,483 Función getNextChar para analizador

léxico de TINY, 78 Funcih MrrqvType, 331 Función mciin, en el lenguaje C-Minus,

27 Función rcod, 297, 298. Véase turnhién

Módulo entero Función módulo, 297-298. Véase tumbiért

módulo entero Función newtemp, 408-409 Función ord. 297 Función printf, 27,89,361 Función reid, 27,400-401 Función r e i l l l o c , 80 Función scanf, 27 Función sprintf, 41 1 Función SQRT. 350n, 352 Función st-insert, 387-388 Función st-lcokup, 387-388 Función travrirse, 337 Función fypeEqua1, 323-327, 342e

pseudocódigo para, 325ilu.v.. 326, 341 e verificación de tipo con, 329-331

Función void, en el lenguqje C-Minus, 27n

Función wr ite, 27,400-401 Funciones de cálculo de dirección de

mínimo perfecto, 80, 94r, 296, 297-298, 298ihrs., 336

Funciones de conjuntos, 130 ' Funciones de transición, 49, 50

en autómatas finitos no determinísticos, 57-38

Funciones miembro, 320 Funciones recursivas, 27. 307-308,

153-354 Funcioues. V2irsc tnnibih Secuencias

de Llamada; Función Iactorial; Funciones de ~~iilciilo de direccih:

Functones miembm, Fuiición mó- dulo: Procedim~enlos; Funtiones iecursiva; Funciones de conjunto; Funciones de transición

an~dadaa, 303 constructores de tipo pan, 319-320 generación de código de llamadas a,

A?h.ilii? . ." secuencias de llamadas de, 349.

359-361, 369-370 Fusión, de bloques de memoria, 377

GAG, 344r Gama$ de caracteres, en expresiones

regulares, 42 Generación autom&~ca de generadores

de código, 490r Generación de analizadoreq léxicos, con

Lex, 81-91 Generación de código, 4, 11-12.260-261,

345,397-484,k0r. Véase tantbiéit Código ensamblador; Cddigo C ; Código intermedio; Código objeto; Código P: Código objetivo; COdigo en tres direcciones

de código ensamblador, 397-398 de llamadas de procedimiento y de

función, 436-443 de referencias de estructura de datos,

416-428 durante la optimización, 397,368-451 cn compiladores comerciales,

443-453 estructuras de datos para, 398-407 exactitud de, 465 optimizaciones para el generador de

código 'TINY, 48 1-484 para la mQquina TINY (TM), 453-459 para secuencias de llamada de proce-

dimiento, 349 para sentencias de control y expresio-

nes Idgicas, 428-436 práctica, 410-41 1 ttcnicas básicas para, 407-416

Generador de analizador sintáctico Gnu Bison, 226,232n, 256r

Generador de analizadores sintácticos LLGen, I96r

Generador de código TINY archivo de código muestra TM gene-

rado por, 465-467,466-467ilus. generación archivos de código TM

de, 464-465 inetkieiicias de, 481 interfaz TM para, 459-461 optimizaciones simples p m ,

48 1-484 Generador de sintetizador, 344r Generadores de aualizadores ISxicos. 4

Analizadores ISxicos (rastreadores). 8 compürudm coi1 analizadorcs sinticti-

cos. 95-06 dccarrollo de cí>dig« para, 75-80

sintáctico Bison: generador de analizadores sii~ícticos Bison Gnu; generador de analizadores sintácticos LLGen; Yacc (yet another compiles-compiler, "otro compilador de compilador más")

q !abais . h uchivo de cabecera, 23, 77,78. 135,244245

Gráficas acíclicas dirigidas (DAG. por sus siglas en inglés), 274-275, 275ilus., 276-277,487e

de bloques básicos, 416n. 477-481, 478ilta

Graficas de dependencia, 271-277,340e asociadas, 271 asociadas para reglas de gramatica,

27 1 atributos sintetizados y, 277 de gramáticas de números con base,

273-275,282-283 de gramáticas de números sin signo.

27 S-272,-276-277 dependencias "en reversa" en,

nodos raíz de, 275ilus, para atributos heredados, 278-279 para cadenas, 271 para expreizones antméticas, 272-273

Gráficas de flujo, 475-477,47661~s. Gráficas de herencia, 375, 394e, 396r Gramáticas, 3, 138-140e, 189- IYOe,

I94e. Véase tambi6n Gramáticas

cas libres de contexto; Gramáticas cíclicas; Gramáticas LALR(1); Lenguajes; Gramáticas LLCI); Gramáticas LR(0); Gramáticas LR(1); Gramáticas regulares; Gramáticas SLRf lf; Gramáticas no restringidas

ambiguas, 114-1 18 con atributos L, 290 constnicción de autómatas finitos

determinísticos para, 250-252e de declaración de variables, 265-266 de números con base, 273-275,

285-286,287-288 de números sin signo, 262-264.

266-268. 271 -272,276-277 dependencia del cilculo de atributos

en la sintaxis de, 293-295 diminación de rccursiún por la iz-

quierda en, 157- 162 iactorirxidn por la izquierda m.

162- 166

Page 572: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

~nodificación de, 293-295 p:m mcglos siibindizaJos, 42 1-423 para expresioties de tipo, 373ilii~..

323-324 para leiigu~jcs de programacióii.

101-102 liara llai~i:idas y definiciones de fun-

cióii, 439-440 tiara verificación de tipo, 329iios..

3301 Cjranuíticas ambiguas, 114-1 18

con Yacc. 234-238, 237i/i1~., 23Hiiirs. defiiiidas, 116

C;rnmáticas atimentatlas, 199, 203 í.;raitxíticas cíclic:rs, L J le, 159

cm aii-ihutos con, 308-3 13 declaraciones y, 295, 298-301

Grarnátici~~ con atributos, 258, 259, 261-270, 339-341)~ 340-34 le, 3 4 7 ~ 343-344c V&i.w irimbihi Graináticas; Gran~áticüs con atribli- tos L; Graináticas con :~tribut«s S

cilculo de atributos con, 270-27 1 con tablas de símbolos, 308-313 de declaraciones de variable, 265-266 de cxpresiniies aritinéticas enteras,

264-265 de iiúiiieros con base, 273-275,

282-283. 285-286,287-288 tic riúineros sin sigilo, 263-264.

266-?bX, 267t definición de código intermedio y

ohjetivo con, 407-410 definiihs, 261 -262 wtensiones y s~mplificaciones de,

268-270 n o circular y altameiite no circiikir,

276 par3 código en tres direcciones, 407,

408-4 10, 4091, 485e para código P, 407-408, 408r,

4 L 2 i h , 48% para verificación de tipos, 329-310,

330t Gratn6iicas con atributos

firertemeiiie iio circttlares, 276 L, 290, 291-293 no circular, 276 S . 277

Gramáticas LhLK(1). 225 C;ramáticas lihres de contexto, 3, 95. 06,

97-106. 128-138, I42r definición f«rin;il dc las, 178-129 elemetitos l.IC(0) para, 201-206 elententos L.R( 1 ) para, 2 17-20 1erigu;rjes definidos por, 100- 106,

178- 130 iio1;tciones p m , 07-08 [>ara Ieiigriajc TINY. ! 33-1 34 rcgl;rs gr:im;iticiiles para, OX- lilO

(iraiiiátic:is LL(I), 155- 156, 178 (ir:iiniticns L.II((I). 207 Gr;trii;itic;is L.K( I ), 221 (~~1ill.ílicas l~,~g~~l;iscs. 13 l

(;ramáticas sin restricción, 132- 133 C;raiitliticiis SLR(I), 2 10-2 1 1

Herencia, 375, 37% tlerencia iiiultiple, 3751, 396r Hercncia simple, 37511 tluecos de hb i to , 307

Iileiitific~dor DFA, irnplci~ientación de, usando variables de estado y sen- tencias "case" anidadas, 60-6 I

Idiomas de in.iqiiiris, optimización de código al utilizar, 471

Iiidi~adii de arreglos, 12, 486e Irikrencia de tipo, 313, 329-331,

343-344r Infei-encia de tipos de Hitidley-hlilner,

344, Inkorniación de tipo diriáinico. 3 13 Itthrniación de tipo estático, 3 13 Información de tipo implícita, 314 Infbrmüción explícita (le tipo, 3 14 I~iseirión, de pilas, 152-153, 167,

186- 187, 352 liistrucción adi, 405, 414-415,416 Instrucción ürg, 438, 486c Instriiccióii I;rqi.n..ar g s . 438 lnstrucciói~ <:di 1, 438, 447 Instrucción csp, 439 Instruccitín r-p, 439,442 Instrucción m i , 438-439 Instrucci~in er:r ;-y, en código de tres

clirccciones, 4:18 Iiistrucción -.que 406 Iiisirucción C j u, 405.430-43 1 Lnsiriicción ggto, 430 Instrucción g r t , 406 Instrucción I i i i l ' ~ ~ , en el simulador Thl,

457 Instrucción i E-la i SO, e11 código de

tres direcciones. 40 1, 329-430 Instrucción id, 4 17-4 18, 420-42 1 Instrucción ixa. 417-41 8,320-421 Instrucción JEQ, 457 Instrucción .lX. 457 Iiistnicción Labe.;, en el código en tres

direcciones, 401. 438 Instrucción I,D, para fa mhquina TM.

25,456,457 Insti?rccióii Lc!a, 414, 415, 418,419 Iii~trttcción ii;.\, püra la nihquina TIC1

IClachine, 25, 456,457 Instrucción ldc, 404-405 Instrriccióii LDC, para la niiíquiiia TM.

25, 26n, 156 Iristrucciórt Lo& 405, 415-416 li~\rrucciúii ?il;ircar-pila (Mark-skich).

-4.39 Iiistr~cciiln i i i i ~ . 4:iO, 442 Iiiitniccióii :;<;p, 45 1-452 I~iwiicci<in ::.e! i. 405 111~ii-uccidn "(e:: . -118-430

Instrucción retiirn, en código de tres direccii~nes, 438

Instrucción sbi, 405-4íhS In\trucción s.: r , 307-408,416 Instrucci<jri :;ti:, 407, 416 Instrucción s t p , 406 lostrucción u j p. 430-43 1 Instnicción wr- i, 405 In~trucci«nes Copy, en c6tligo de tres

direcciones, 40 1-407 Iiistruccioiies de almacenamiento no

destructivas, 407 Instrucciones de salto condicioital, 429,

429iltr.s., 431ilus., 486e. Vri.r.rr iciriihiCrz Instrucciones de . : o i ; . - : Instrucciones de r;ilt» i11c~:i-ilrci«iial

en código de tres direcciories, 301 optiinización de código con, 482-484,

4X4ilus. para la tn6quii~a TM, 457

I~istriicciones para la máquina T'l, 454-457, J55t

Iiistrucciones para saltos, 428-430, 448. Vlrrse tunibilrt Sentencias de ruptura; Instrucciones de stilto condicioital: Sentencias de coiitrol; Accioiies Gato; Sentenci:is Goto; Instrucciones de salto incondicional

cn el simulador TM, 458 optiriii~ación de secuencias de código

sin, 474 separación de bloques básicos de c<í-

digo mediante, 475-476, 476ilu.s. Jiiterf:ites, entre conipiladores 1 sistc-

mis operativos, 17 lrrterfa~ TM. para geiieriidor de codigo

TINY, 459-461 Interpretación abstracta, 413 Int&rpretes, 4-5, IiiiCi-pretcs de cotri;i~idos, 1 lriterrupción ("Thrink"). 386 ISO (Iriternational 0rgaiiiz;rtioir ior

Standardiration), est8ndares del lenguaje adoptados por, 16

Jeraquía de Chomrkq, 3, 131-1 33, 142r Jerarquía ile clase, 320 Joltnion, Steve. 4

Lenia de la extracci6n (purnping Irni- rria), 39

Letiguaje ,\da, 16, IJle, 58Yr mbiente de ejecución bas;tdu en pilas

para, 362-363, 365 ambiente de +jeciición de, 345 ámhito cn, 305 ;iniJisis serriliiitico en, 257 Arreglos con subíndices en, 418-419 <:i>rnentarios cit. 44-15 tlccl:ir:tcitiii ile c<instirote cii. 300 <Ieclaracio~ies :il iriisnio nibcl de xti-

d:ici6ii en, 306

Page 573: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

equivalencia de tipo en, 344r infbrmació~t de tipo estritico en, 313 lcnguaje TlNY y, 22 iiiecanismo de paso de parámetros de,

382 mecaiiismo de paso por valor de, 382,

391e rnecanismo de paso por valor-resultüdo

de, 384-385 operadores sobrecargados en, 332 psando procedimientos como pará-

metms en, 373 problema del else ambiguo con, 122 procedimientos y funciones anidados

en, 303 tipo de datos de función en, 3 19 tipo de datos de unión en, 3 19

Lenguaje Algol60, 142r ambiente de ejecución basado en pilas

para, 396r eqtiivalencia estructural en, 344r mecanismo de paso por nombre de,

386 problema de else ambiguo con, 122 recursión en, 396r reglas gramaticales para, 98

f.enguaje Algo168. 122, 344r Lenguaje BASIC, 4-5,299 Lenguaje C, 139e, l4Oe

arnbiente de ejecucióo basado en pilas para, 353-361, 363-365

ambiente de ejecución para, 17, 345, 388-389e, 393e, 39439%

ámbito dinrimico contra Bmbito estáti- co en, 305-306, 306iliis.

ámbitos de ünidación en, 302iEus. análisis semántico en, 257 arreglos con subíndices en, 421-423 asignación de memoria en, 347 comentarios en, 14-45 constructores de tipo amglo en,

315-316 convertión y coerciúii <fe tipo en,

332-333 declaración antes del uso en, 301-302 declaración de constates en, 299,

300 declaración de equivalencia en, 328 declaración de tipo de datos en, 1 1 1 declaraciones al mismo nivel de tini-

dación en, 306-307 ~leclaracio~ies de tipo en, 299, 320-322 decluraciones de variables en, 299,

300-30 I declaraciones para funciones recursi-

vas en, 307-308 definiciones contra declrtraciones en.

30 1 equivüleiicia de tipos en, 344,. estructura de bloques de, 302 estructuras de &t«s para código en

tres direcciones cuádntples en, 4O:iiiit.s.

li~rnxr Rackus-Nmr jura, 102

función de tipo de datos en, 320 í'unciones y procedimientos anidados

en, 303 gramática de sentencias (le control en,

134-436 información de iipo estático en, 3 13 lenguaje TINY y, 133-134 Lex y, 81 tnecanismo de paso de par,imetr«s de,

382 mecanismo de paso de parhetms de,

tnecanismo de paso por valor-rcsul- tado, 384-385

mecanismo de paso de pwámetros de, paso por nombre, 385-386,395e

mecanismo de paso de parimetros de, paso por valor, 382-383

nombres de tipo en, 320-322 opwaciones con estnicturas dinámicas

íheap) en, 377-380, 379ilus. operaciones lógicas de cortocircuito

en, 433 operadores sobrecargados en, 332 paso de procedimientos como perá-

metros en, 373 referencias colgantes en, 373-374 referencias de apuntador y estructuras

de registro en. 423-425, 426 registro de activación de procedi-

miento en, 348 subíndices de arreglo en, 418-419 tipo de datos apuntador en, 3 19 tipo de datos de unión en, 3 17-3 18 tipos de datos indirectamente recursi-

vos en, 322 veriticación de tipo en, 260

Lenguaje C++ ambiente de ejecución del, 345,

393-394e ámbito de resolución del operador en,

305 coacción en, 333 mecanismo de paso por referencta en,

383-384 mecanismos de paso de parámetros.

? X i . .-

memoria dinámica p ~ m , 375-376 operadores sobrecargados en, 332 tablas de función virtual en, 396r

Lenguaje C-Minus, 2,26-27,491-500 ambiente de ejecuci6n de la máquina

'"TM" para, 497-500 convenciones léxicas de, 491-492 gramática de la forma Backus-Naur

para, 492-496 pmgramas de muestra en, 496-497 propósito del, 191 proyectos de programación en,

500-501e sintaxis y sern6ntica de, 492-496

1.ciiguaje de alto nivel, I I.i.nguaje de niáquina, 2 I.cnguaje cnsairibl~dor 'TM. 25-26 I.mguilje est.',o<lar, 16

Lenguaje FORTRAN, 16,47 declaraciones implícitas en, 299-300 desarrollo de, 3 ilescuhrimiento de ciclo en, 475 preprocesador para, 5 problema% de buscar hacia adelante

con, 47 Lenguaje MKTKAN77

ambiente de ejecución para, 17, 345, 349-352,388e, 395e, 396r

asignación de niemoria en. 347 asignación de variables en, 260, 391e equivalencia estructural en, 3441. mecanismo de paso de parámetros.

382. mecanismo de paso por referencia,

383-384 programa de muestra en, 350-352 registro de activación de procedi-

miento en, 348 Lcnguaje fuente, 1 Lenguaje Haskell, 14% 344r, 386 Lcnguaje LISP, 5' 139e. l4Oe, 48%

ambiente completamente dinámico para, 374

ambiente de ejecucitín para, 17 :inálisis semántico en, 257 información de tipos dinámicos en,

313 memoria dinámica para, 375 registro de activación de procedi-

miento en, 348 verificación de tipos en, 260

Lenguaje Miranda, evaluación descui- dada en, 386

Lenguaje ML, 140e ambiente completamente dinBmico

para, 374 declaraciones colaterales en, 307 equivalencia de declaración en,

328-329 equivalencia de nombre en, 344r tipos de datos recursivos en, 321-322

Lenguaje Modula-2, I4le, 489r imhitos de procedimientos y Funcio-

nes en, 308 comentarios en, 45 Conversión de tipo en, 332 declaración antes del uso en; 301 -302 declaraciones constantes en, 3íX1 paso de procedimientos como pará-

metros en, 373, 374 iipo de datos función en, 319-320

Lenguaje natural: 3 Lenguaje objetivo, 1 Lenguaje Pascal.

ambiente de ejecución basado en pilas para, 365-370. 370-373. 389-39 le

:unbiente dc ejecución para. 17, 345 iinbitos anitlados en. 303iicts. mílisis semintico en, 257 arreglo con stibíndices en, 4 18-4 19 asignación de mernoria en, 347 cr~rrirnt~irios en, 44-45

Page 574: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

consrriici~ires tipo arreglo en, 3 15 declaración entes del uso en, 301-302 dzclaraciones :II mism« nivel de wii-

ilación en. 106 dccl:ir:iciories de constantes en, ,300 ileclaraciones de tipo en. 299 ileciaracio~ies de tipo explícito e im-

plíciio en, 3 14 ileciaraciones fOrw:ud (hacia adelante)

en, 308 dcclaraciunes y nonibres de tipo cii,

320 equivalencia de dec1ar:iciSn en, 318 estructura de bloqucs de. 302 forma Backtis-Nwr para, 102 i'itnciontx y procetlimientoi anidados

en, 303 generttción de código P y, 404 iuformación de tipo est6tico en. 313 lengwje TINY y. 22, 133- 134 rnec:mism« de paso por valor de, 382 ntecanismos de paso de parhetros.

382 niecanismos de paso por referencia

de, 383 operiiciones de estructuras dinjniicas

íheap) en, 377 operadores de sobrecarga en, 312 registro de activación de proccdi-

miento en, 348 tipo de datos apunti~dor en, 3 19 tipo de datos de función en, 319 tipo de datos de unión en. 11 8-3 19 tipos enunierado y de subrango en, 115 verificación de tipos en. 260

!..enguaje Scherne, declaraciones colate- rales m , 307

I.enguaje Smalltalk, 17. 257, 3'75. 306r Imguaje IINY, 2, 22-26, 586-487e,

488-48% :unbiente de ejecución para, 386-388,

387ilus. ;inilisis semántico de, 334-338 malizador senlántico para, 259,

336-318,343e compilador TlNY y, 22 expresiones booleanas en, 22, 133- 1 %

192r hrma Backus-Naur extendida para.

180-181 generador de código ensambladt)r pa-

ra, 398 generador de código para, 459-467 gramática libre de contexto pma,

133-134 implementación de mi analimdor

1t"xico para. 15-79 programa de inuestra en, 78,

137- 138iius. iiiitaxis de, I3:Ll:IX tipos cn, 334-3.3 :::hla de símbol«s para, 335-336

L~ i igu~~ jc TINY, imiilimdor d k t i c o :iscentlcnte pirra. 197.- i ' i X

1-enguaje TINY, :inali,:rtior iiritáctico recursivo descendente para. 1 SO- 182

Lingtiajes antitriones, 18-2 1 Lenguajes de formrrio libre, 46 Lcngriajzs de progr:iniacih Vksu t<iin-

l i ih Lenguaje Adx leiiguitjes tipo Algol; Lenguaje ens;unhlador; Lcn- guaje RASIC: Lenguaje C: 1.crigiia- je C-Miniis; languaje FORTRAN: t.engiiüJe llaskeii: Lengwije de i t l r t ~

nicel: Código intermedio: Lengua- jes: Lenguaje LISP: Letiguaje de niáquinir; Lengu3je Mir:inda: Len- gu.je ML; Lenguaje Pascal; Código P: Lcnpiiaje Smallrlilk; 1.engurrje IUcute; Lenguaje objetivo: Código cn tres direcciones; Lengu:tje TINY: lenguaje ensamblador TM

estructura de bloques de, 302 estsuctiirii ldxica de, 31 grdmlíticils para, 10 1 - 102 iuiplenicntación de atributos y gramá-

ticas con titributos para, 259-295 tablas de símbolos para, 295-31.3 tipos de datos y verificación de tipos

para, 313-334 1aiguajes estructurados en blíxpes,

302 1,enpuajes iuherentemeote ambiguos,

133 Ixnguajes libres de contexto, 128- 133 Lenguajes rnonomórficos, 333 L.cnguajes orientados a objetos, 333,

375-376 L.enguajes polimórficos. 333 1-cnguajes tipo Algol, ambientes de qjc-

cución para, 17 L.engrrajes. Ví'ase tamhiét~ Lenguajes de

programación definidos por gramáticas. IOíl-106 generados por expresiones regulares,

34-35.91e gerteradtis por gramáticas libres de

contexto, 129 para autómatas finitos, 39, 57-58 unión de, 35-36

Lesk, blike, 4 Letras; 44.47-48, 50. VPuse tami~ién

Caracteres; i\,ieracaracteres; Cadenas Lex, 4, 32,4t, 81-91, 891, 04r I.exemüs, 32-33 LJFO (ultimo en cntrx-primero en salir).

protocolo, 3J7 Liradores. 5

&

Linealización, de jrboles sintdcticos, 198. 199--$O0

I : , -L L . 8 ,,L., 336 LINGUIST, 344r Listas lineales, 80, 296 !.i/es:~Ies. 8 , 43, 347 I..irerales i i i c:idcnn. V<;<i,se [Literales I.i:imatlas .i proceilimiento recursi\'o,

:iinhicnte de ej'jicucitin h:is;ido en pila [?:ira, 357.353

L.lümad;rs, 4%. V&se tumhih Ftincio- nes; Llarniidas de pr«cedimieiito; Prncedimicntos

código intelmedio para, 47-539 código para coriipiliidor C de Siln,

452-453 código para el Compilador Rorland C

4Ji-448 proccdiiniento pura generación de

código,J39-443 L,iariiadas de procedimiento. 486e. Vlirr.w

tumbic'n Argumentos: L.laniatias; Fitnciones; Parametros

código del compilailor C de Norlantf para, 447-448

cidigo del compilador C de Sun p:. ,i, 452-453

con número variable de quinentos, 361, 391e, 5 8 6 ~

en bloques básicos de código, 47% generación de código de, 436-443 uptirnización de código mediante

eliminación de iniiecesarias. 470-47 1

organización de la pila de llamadas para. 352-353

sin parzímetros. 215-216 L1avi.s ( ( ) )

cn L.ex, 82-83 i~peración de repetición y, 123- 124 para comentarios en el lenguaje

TINY, 75 75 regl:ts ile la forma Rackus-Naur ex-

tendida y, 136-147

"Il:icro E C O , 86, 87 Macro yycleurin, 237 >lacro yyerrok, 247, 248, 254e blacros, 5 Manejadores, de formas de sentencias

por la derecha, 201,252e Manejo de errores, 7ilrrs.. 8. 18, 183- 189

Conjutitos Primero y Siguiente en, 152

en el analizador lexicogrzífico de TINY. 76-77

en veriticadores de tipos, 33 L en Yace, 230, 234-235 para nílnieros octales, 267 por analizadores sintácticos. 9fi-97

Manuales de referencia de lenguaje, 16-17

MAquina P, 404-305 Máquina TINY JTM), 22.486-487e,

488-4X9e. Vkse imnbién üener:id«r de código cnsamblador para simuk- &ir TM. 1%, 453-4.59 22,486-487~. 488-48'3e

:rrcluitectura básica de, 454-457 ciitnpilador 'T'INY y, 25-26 conjunto ct~mplcto de iristruccioiies tte.

.!m list:r<l« para, .<$S-5-37

Page 575: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

h1;iquiiras de estado finito. Wase Autó- matas finitos

Marca ?EB%G, código inaccesible y, 469-470

klarcd Eci.oSc;ot?rce, para el conipiia- d a TINY. 25

Marca NO-.&VALY%3, 24, 464 Marca N O - ~ 6 ~ 1 ~ 0 . 24,464 Marca ?iü-PARCZ, 24,464 Mrirca IrnceAnalyze, 25, 335,338 Marca Tracecódigo, 25.461.465 Rlarca 'ir .iceParcc., para el compllador

TINY, 25 Marca TracrCcan, para el compilador

TINY, 25 Marca Y'I?ARSi31?, 243,245 .%arcas o banderas, para el coinpilador

TINY, 24-25 Marcos de pila, 348 ?vlecanismo de evaluación retardada,

382,385 Mt.c&nismo de nombrado de tipo type-

def, del lenguaje C, 320-321 Llecanismo de paso por nombre, 382,

385-386,395e Mecanismo de paso por referencia, 382,

383-384 Mecanismo de paso por texto, 39% ;Mecanismo de paso por valor' 382-383,

3Yle Mecanismo de paso por valor-resultado,

382,384-385 Mecanismos de detiuición, en Yacc,

242t Mecanismos de definición y nombres

internos de Yacc, 242t blecanismos de paso de parámetros, en

ambientes de ejecución, 38 1-386 blecanismos p m manejo de excepciones,

18 Memoria de acceso aleiitorio (RAM, por

sus siglas en inglés), organización de la, 346

Memona de instrucción, para la máquina TM. 454

blernoria de ~egistro (RM, por sus siglas en inglés), instrucciones de, para la máquina TM, 454,455-456,46 1

Memoria dinámica, 373-381, 396r udministrricidn automittica de estnic-

turac dinámicas p m , 350-381 administración de estructuras dinzíini-

cas para, 376-380 cn lenguajes orientados a objetos, ~

375-376 para ambientes completamente dini-

micos, 373-375 Memoria, para la máquina Tiny,

454-457 Memorización, 386, 305c ,Menos unario, 1 JOr, 193r Mensajes de mor, en Yace, 2 5 4 ~ &li.t;icarkter rle signo de interrogacih

('9, 4.3

Metacarácter de signo nuniCrico (#J, en pilas- por valor, 167

Met;ic;mícter más (+), 42 Metacarácter punto (.). 42. 201-202 Metacaracteres, 35-37.42-43, 81-82,

V&e también Catacteres; Letras: Cadenas

h1ct;~aracteres "Not". V4asc Cüricter Cara ("); Caricter tilde (-)

iMetalenguajes. 268 Metasímboliis, 35, 98-99 Método basado cn reglas, de construc-

ción de griftcas de dependencia, 276

Método de irbol de análisis gramatical, de cons~rucciói~ de gráficas de tle- pendencia, 276

Métodos, 320, 375 Métndos algorítmicos contrnladt~s por

tabla, 63,408-409, 409t Modificadores del ámbito, 308 Modos de direccionamiento. 12 Módulo entera, 193e. Vbase también

Función morl; Función módulo Moviniiento de código, 475 Multiplicación, 117-1 18, 118-1 19, 470

Naur, Peter, 98 Niveles de anidttción, 368-369. 399 No asociatividlid, 1 17- 1 18 no temilial comntand, en Yacc, 229.

234,251e No terminal factor

en especificaciones de Yacc, 229-230 en listados de Yacc, 234

No termioal terrn, 729, 234 No terminales

apilruniento de, 152-153 coniuntos Primero de, 168- 169,

170-173 conjuntos Siguiente de, 173-174 e~imi~iación de recursión por la iz-

quierda y, 158-160 en análisis sintáctico ascendente,

198-199 en an61iqis sintáctico LR(1 j, 217-2 18 en árbolea de análisis gramatical,

107-108 en dragramas de mtauis, 125 en especificaciones de Yacc, 229-230 m ~ramáticas libres de contexto, 102,

iza- 129 en listados de Yacc, 233-234ilus.,

234-235. 237ilus. en tablas de análisis sintáctico LL(I).

154-155, 178-180 iriútiles, 191e nulificables. 169

Nodo < la l ' ,K , 440. 441, 441--442ili1s., 442

Nodo Ccr.r t n, 44 1, 14 1 -442iirr.s. Kodo rnK . 440, 41 1. 441 -442ilos., 442 No<lo ri!K. 44 1, 44 1--147iliis.

Ncido ParaiiK, MO,Mt, 441-1.42j(Ir,s., 442

Nodo Plfis;C. 441, W2.441-43.2&ts. Nodo ?ryK. 440,441, 441-&Zilu,s. Nodos

de árboles de activaci6n. 356 de árboles de análisis gramatiea1 y

irboles sintácticos, 9, 107-1 10, 129 de Arboles sintácticos para definiciones

y llamadas de función, 450-441 de estriictura de árbol sinrictico de

'TINY, 134-138, L3Silris. de estructura sintáctica de TINY,

243-244,462 de grificas de dependencia, 275-276.

27Silus. de gráficas de flujo, 475-477 en tabla de síinhol«s de TINY,

337-338 Nudos de tipo. 330 Nodos hoja, 9, 107-108, 110, 129 Nodos interiores, de zírboles de anilisis

eramatical, 107- 108, 129 - Nodos raíces

de Arboles de análisis grümatical, 107, 129

de árboles sinticticos, I 10 de árboles si~rtácticos para llama~tas y

clethGcimes de funcitin, 440 de grhficas de dependencia, 275iiu.s.,

275n, 275-276 Nombres, 37-38 Nombres de tipos base, 328 Nombres internos, en Yacc, 242t Notación punto, 317 Numeración, de nodos de &bol de an:ili-

sis gramatical, 108- 109 Numerüción postorden, de nl~dos de irhol

de análisis gramaticrtl, 108- 109 Numeración preorden, de nodos de irbol

de análisis gramatical' 108 Nítmeros binarios, como cadenas. 41 -42 Núineros con base, Véa.se furttbién Nú-

meros binarios; Números decimales: Números hexadecimales; Números; Números octaIes

sin signo, 273-275. 282-283.285-288 Números de línea, agregar al texto,

84-85 Números de punto flotante, 488e

asignación de memoria para, 347 mtómatas finitos determinísticos

para, 52ifus. cn código Yacc, 241-242 en expresiones aritméticas, 283-254 grmAtica de, 339e

Números decimales conversión n números hexadecitnales.

87-88 gr:~mática de, 266-268, 273-275, .330r notación parü, 43

Núincros hexadecirnales. son\ersii,n de nilmeros decimales a, 87-88

Nhierns i1ntur3Lcs. 51-52, 130

Page 576: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]
Page 577: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Parámetro oi:t, 384 partietros in, 382 P:irámetr»s in o u t , en lengicaje Ada,

384-385 Parámetros sychset. 186 I'ur2ímetros. Véase tumbién Argumentos;

Llamadas; Funciones; Llnniitdas de procedimiento

atributos como, 285-286 cn ambientes de ejecución basados en

pilas, 356-357 en llamadas de función y procedi-

mientos, 38 1-386, 394-395e paso de procedimientos corno. 370-373,

37 1 iltrs., 372ilus., 373ilus. Paréntesis, 102. Véuse también Prece-

'1 ores dencia de oper 3 :inalizador sintáctico ascendente p:ira

balance, 1 B t analizador sintáctico descendente para

balance, 152- 154 anidi7ador sintáctico LU(0) para

balance, 208, 208ilus., 209t analizador sintáctico SI.K(I) para

balance, 2 12-213, 212-213t auiómata finito detenninístico de

elementos LR(1) para balance. 21 8-220, 22Oilus.

autómata finito determinístico para la gramática de balance, 204-205, C0Silu.s.

autómata finito no determinístico para la gamática de balance, 204,204ilus.

balanceados, 105, 154-155, 199 como metacaracteres, 37 corno metasímbolos, 99 eliminación de anibigüedndes y

especificación de precedencia con, 118

gramática de balance, 202 tabla de análisis sintictieo LR(1) pttra

la gramática de balance, 22 1-222, 222t

Pasadas, durante la compilueión, 15-16 Pasos de derivación, en gramáticas libres

de contexto, 128- 129 Patrones, 34, -)3r

de tipo, 333 Perfiladores, 6 Pila de máquina P, 414-416,417-418 Pila de registros de activación. 352,

389-39Le. Viase tumbi4n Pilas de ejecución, 448, 352, 363il1~.s., 364iiir.s., 365ilit,s., 366ilus, 367ilas., 369iitis.. 370ilns., 372ilt~s.. 373ilus., 392e. Véase tnrnhiL:rt Pilas de llamada

Pilas de Ilaniadas, 352, 653-354, 354ilus.. 355-356, .35hilus., 658ilus., 359ilus., 3hOil1r.s.. 6 l ilrr.r., 362ilu.s., ,389-39 1 e. VZrsr r,urrhiin Pilas de tiempo de ejecu- ción; Registros dz iictivxión de pi- la: Pihs de v:ilorcs

Pilas de valores, 167, l67r en el 2álcnlo de atributos herediidos,

291-293 cn el cálculo de atributos sintetizados,

290-291,29 11 para Yücc. 229-230, 240-241

I'ilas del iinálisis sintáctico, 95. 252e análisis sintáctico ascendente con,

198- 199, 200-201 anátisis sinttictico LL( 1) con, 152- 154,

168167, 186.187 andlisis sintáctico LR(0) con, 206-210 :itiálisis sintáctico SLR(1) con,

210-213 con acciones con pila de valores, 1671 en el c5lculo de atributos heredados,

29 1-293 en el cálciilo de atributos sintetizados,

290-29 1,29 1 t eti recuperación de errores en modo

de alarma, 246-247 prociucciones de errores en Yacc y,

247-250, 250n recuperación de errores con, 186- 187

Pilas. Véase Pilas de llamadas; Pilas de análisis sintáctico; Pila de máquina P; Pilas de valores

Ponabiiidad, de compiladores. 15 F'nigmas. 17 Pragmática, 17 Precedencia, 253e Precedencia de operaciones, 37. 38

en expresiones aritméticas, 117-1 18 en expresiones booleanas, 139e eii expresiones regulares. 38-41 en Yacc, 236-238, 238ili1s. resolución de ambigüedad con,

115-1 19 Precedencia en cascacitas, 1 18- 119 Prefijos viables, 201 Preludio estándar, para la máquina TM,

161.462 Preprocesadores. 5 Preprocesador Ratfor, 5, 30r Primer principio de análisis sintáctico

LALR( 11,224 Principio de la subcadena más Iürga, 46 Principio de subtipo, 333 Principio del "máximo bocado", 46 Problema de errores de cascada, 183,

188-189 Problcma del análisis sintáctico, 3 Problema del else ambiguo, 120-122,

1 3 9 ~ 142r análisis sintáctico LL(1) y. 156 definición, 12 1 para analizadores sintácticos SLR(I),

213-215 rcglas de no :irnbigüed~d para, en

Yacc, 235-236 Prohleinas <le fase 173

I'rtrccdiniiWr,

código para, 283 Procedimiento copySt rins, 80, 182.

244 Procedimiento delete, 288,295, 29511

ámbito dinámico y, 306 hb i tos anidados y, 303, 30.1 declaraciones y, 298-209 estnictura de tablas de símbolos y.

295-296 para tablas de símbolos de TINY, 3 3

Procedimiento erni tCóaigo, 41 1. 435ila.s., 436

Procedinliento error, 144- 145 Procedimiento EvcrlBu.se<lNrrnt, pseudo-

código para, 286 Procediniiento EwdNunz, pseudocódigo

para, 286 Procedimiento Evul'Tyf)e, 280-282, 289 Procedimiento EvulWitliBase, pseudoch

digo para, 282-283.285-286,287, 342e

Procedimiento exp, 145, 150- 151 Procedimiento fixtor, 144- 1-15 Procedimiento f get S , 78 Procedimiento gencode, 424-425ihrs.,

486e para llamadas y definiciones de fun-

ción, 441 -342ilats. para sentencias de control. 435iIu.s. procedimiento ~enCode, 410-41 1.

412ilus., 413ilrts. Procedimiento i;en¿abe:., 435ilus.. 4% Procedimiento yecchar, 88 Procedimiento qetToken, 33-34, 77,

78, 90-91, 96, 181, 244 procedimiento input, en Lex, 88 procedimiet~tu insert, 288-289, 295,

3 LO, 329 jmbitos anidados y, 303 declaraciones y, 298-299 estructura de tablas de símbolos y,

295-296 para tablas de stmbolos de TINY, 33C

procedimiento i~sertXode, 337, 337-338,

Procedimiento. lookup. 288,295,306, 3 10,329,332

ámbitos ünidiados y, 303,304 estructura de tabta de símbolos y,

295-296 para la tabla de símbolos de TINY, 33(

Procedimiento ~rtrikeljpeiVrtúe, 330 Procedimiento murch. 144-145 Pr~~edimientomüzch, 181. 1x7- 188. 103< Procedirnielito NewZxpNofie, en el ana-

lizador sintáctico TINY, 182, 243 Procedimiento -.ewSi::rtI:ade, en el

:i~ül¡ZXk~r sirltiictici) TINY. 182. 2.1: Pr«ccdimient« c.1 l :F roc, 337 i'rocedirniento Pn.srEvu1, 277-278,

278ilr1.s. t'n)cediniientri p7:- t- piii<,. 3.37

Page 578: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]
Page 579: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Redirección de objetivo. 2% Reducción en intensidad, optimización

de código, 470 Rel'erencias colgantes, 373-374 Rekrencias de apuntador

cilculo de; en generación de código, 423-428

ctidigo compilador C de Borland para. 445-446

c6digo Compilador C de Sun para, 450-45 1

Referencias de arreglos cálculo de, en la ge~ieraciún del códi-

go, 417,418-423 aídigo para el Compilador Rorland C.

444-445 código para el Compilador Sun C,

449-450 Regisrn de apuntador base, en el código

para expresiones aritméticas, 443-444

Registros, 48 1-482, 483ilus., 484ilus. Registros de activación

con arreglo de visualización, 392e con datos de longitud variable,

362-363, 362iIus., 391e con declaraciones anidadas, 364-365,

364iEus., 365ilus. con referencias no globales y no loca-

les, 365-366, 366ilus. con temporales locales, 363, 363ilus. con vínculos de acceso, 367ilus.,

369ilies,, 370ilus., 372iIus., 373ili~s. en ambientes de ejecución basados en

pilas, 352-353, 354, 354ilus., 355-356, 356ihs., 358, 358ilus.

~rganización de, 348 para FORTRAN77,350 para secuencias de llamadas a proce-

dimientos, 359-361, 359ilus., 360ilus., 36 l ilus.

parámetros en, 38 1-382 Registros de activación de procedimien-

to. Véase Registros de activación Registros de acumulador, 443454,

460-461 Registros de entrada, en la SpwcStarion,

453, 453n Registros de procesador. 449n, 449-450,

454-457. Véase también Área de registro

Rcgistros de salida, en la SparcStation, ,453,453n

Registros de tokcn, 33 Registros vluiantes, 3 18 Registros. Véase tumhién Constructores

del tipo de registros de activación para, 3 17

Regla de nnidación mis pl-óxima, I 1 1, 156. 1571

pxa esiructuras de hloque, 302. 30.3 Regla de liieci dc lugar, 46 !Icgias de contexto. 13 1-1.32 Rcgl;is dc iopi;i, 291.203

Reglas de no ambigüedad, 45-46, 1 16-1 18. Véme rumbiGn Ambigue- dad: Gramáticas ambiguas

definición del problema de else ambi- guo, 1 16

para conflictos de análisis sintictico SLR!I), 213-215

para conflictos (le análisis sintáctico de Yacc, 235-238

p m sentencias "if ', 120-122, 156, 157t Reglas de recursividad

en gramáticas libres de contexto, 95. 97, 129- 130

gra~náticas con, 102- 103 operación de repetición y, 104-105

Reglas de semántica, 258, 261n Véase rurnhiJn, Ecuaciones de atributos

definición, 261-262 para expsesionei aritmSticas, 2@a,

270t, 2Y4t, 292-293,294 para expresiones aritméticas enteras,

2 6 4 para gramáticas de declaración de

variable, 2661, 2881 para gramáticas de números con base,

2882 para gram6iicas de números sin signo,

262-263.2631, 266-268,276t Reglas gramaticales

atributos sintetizados y, 277 como ecuaciones, 129-130 contra derivaciones, 101 derivaciones de, 102- 106 en archivos de especifi~ición de

Yacc, 227.229 gráficas de dependencia asociadas

para, 27 1 p a expresiones aritméticas, 269t,

2701, 272-273, 284t. 292-293, 294 para expresiones ;uitniéticas de ente-

ros, 2641, 264-265 para gramáticas basadas en números,

273-275, 288t para gramáticas de declwación de

variable, 266t, 7881 para gramáticas de números sin signo,

262-264,263t, 266-268,271-272, 276t

para gramáticas libres de contexto, 95, 97-98.98-100, 128- 129, 129- 130

sensibles al contexto, 132 traducción de, para la forma Backus-

'Jmr extendida, 141- 15 1 Reglas recursivas por la derecha,

104-105, 123-124, 125 Reglas recursivas por la izquierda,

104-105, 123-124 Rehospe<laje. 29e !leparación de errores. 06-97, 18.3 IZeprcszntaciún de árbol con hermano a

la derecha t. hijo extremo i~qnier- do, 1 14

17cpresentacidn intermedia !IR, pw sus s i$ i s en in;lGs). I l . 398

Resolución de colisión, en tablas de cálculo de dirección. 296, 297iiiis.

Retrciseguiiniento (hacia atris), 46-47, 53; 58.63-64, 102e

Rutina parse, 181, 188, 234 liutinas de usuario, en Lex, 83

Secci6n de definiciones. 83-84, 90, 227-230

Sección de reglas, 83-84, 227-230 Sección de rutinas auxiliares

de archivos de entrada en Lex. 83-84 de archivos de especificaci011 en

Yacc, 227-230 Sección de nitinas de usuario, de !os ' archivos de entrada de Lex, 83-84 Secuencia de retorno, 349

para FORTRAN77,350 Secuencias de llamada, 349.359-36 l .

369-370,437439 Secuencias de sentencias

Cálculo de conjuntos Primero p m , 173 Cálculo de conjuntos Siguiente para,

177 construcción de tablas de análisis

sintáctico LL!I) para, 179- 180 factorización izquier&a de, 163, 164 gramáticas para. 106, 11 3- 1 14

Segmentos de código en línea recta, optimización de, 473-474

Segundo principio de análisis sintactico LALR(1). 224

Selección de instrucción, optimización de código a traves de una apropiada, 47 1

Selecciones de producción, en tablas de análisis sintactico LL(l), 154

Semántica, 16, 492-496 Semintica de sint;utis dirigida, 258 Semhtica del menor punto fijo, 130 Semántica denotacionaf, 16, I 42r Semhtica dinámica, 9- 10' Semiintica estática, 9-10, Sentencia ,Assigr.:i, 338 Sentencia break, 434 Sentencia Z f X , 338 Sentencia ReadK, 338 Sentencia RepeatX, 338 Sentencia vacía, en tenguaje TINY,

t 33-1 34 Sentencia Write, en lenguaje TINY,

133-134, 134-137. 243 Sentencia Writeii, 338 Sentencias, 101, 129 Sentencias Case, 60. 485e, 1S6e Sentencias compuestas, 302, 364-365 Sentencias de ;riignxi6ri

cn prümiticas LL( I ), 165- 166 en lenguaje TINY. 133- 134. 134-136,

i36ilir.s., 243-244 incluyendo gr;irn.itica :;im?le. ?!5-7: h.

17 7 3 1 3 --- --. .li.i.ific:ici<:ri de tipo.; de, 331

Page 580: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]
Page 581: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

T:hl:is de símbolos, 7ihu., 8, 13, 257, 288-289, 295-3 13, 304ilus., 305ilu.s., 343r. Véase rurnhién Estructuras de datos

como atributos heredados, 195 definición, 288 estructura de, 295-298 funciones de c,ilculo de dirección para.

296-298, 298ilu.s. generación de código intermedio y,

398-'399 - ~ ~~~

gramáticas con atributos con, 308-3 11 declaraciones y, 298-301

operaciones principales para, 295 para palabras reservadas del compila-

dor TINY, 80 reglas de ámbito y estructura de

bloques, 301 -306 Tablas de transición, 61-63 Ticnicas de aiiálisis sintáctico LL., vía

análisis sintictico durante cUlculo de atributos. 289-290

'Técnicas de mejoramiento de código, 4 Técnicas de ctptimización, 4.468-48 1 ,

489-490r clasificacicin de las. 472-475 fuentes de optimización del código

en, 469-472 implementación de las. 475-481 n:ituraleza de las, 468 para el generador del código TINY,

48 1-484 Temporales locales, 363, 39 le. Véase

tumhién Temporales 'Ternpordes. Vkse ruinbién Temp«r~les

locales conservación en registros, 48 1-482,

483ilras., 484iliís. en código de tres direcciones, 400,

408-409 en código P, 407 prevención del uso innecesario de,

478-479 . . .

Teoría de autómatas, autómatas tinitos determinísticos de estados iníriimos o mínimos en. 73

Terminadores, 106 Terminales. Véuse tumhién Tokens

en análisis sini6etico LR(1). 2 17-2 18 en árboles de análisis gramatical,

107- 108 en conjuntos Primero, 168- 169 en conjuntos Siguiente, 173- 174 en diagramas de sintaxis, 125 en gramiticas libres de contexto, 102,

128-129 en tablas de análisis sintictico LL(I),

154- 155 T~xto , agregar números de línea a,

84-45 'Tiempo de fijación, de un ~itrihiito,

259-260 Tieinpo de villa. de <leclar:icioncs. 300 Tipo de datos ,\ i i . j r i , 378

Tipo de datos s t r u c t , 315, 317 Tipo de datos void, en el lenguaje C.

315 Tipos de componente, para arreglos,

315-316 Tipos de datos, 3 13-334, 343-344r. Vécr-

se tizmbién Arreglos; Tipo de datos booleano; Funciones; Tipo de datos apuntador; Procedimientos; Cam- pos de registro; Registros; Tipo de datos recursivo; Cadenas; Tipo de datus estructurado; Tipo de datos de unión

constructores y expresiones para, 3 14-310

decltuaciones, nonibres y recursión de, 320-322

definición, 3 13-3 14 equivalencia de, 322-329 predetlnidos, 3 14-315 kmaños de, en la Sun SparcSthtion, 450 ~erificacicín c inferencia de, 329-334

Tipos de datos indirectamente rectirsivos, 3 - - .>AL

Tipos de datos yrcdefinidos, 314-3 15 'Tipos de índices, para arreglos,

315-316 Tipos de valores booleai~os, en lenguaje

TINY, 334,338 Tipos de valores de punto flotante, 240,

265-266, 272-273, 279-281, 202-293, 294-295

mezclados con tipos de valores ente- ros, 283-284

Tipos de valores enteros en lenguaje TINY, 334,338 mezclados con tipos de valores de

punto flotante, 283-284 'Tipos de valores, en Yacc, 239-241 Tipos de valores enteros. 239-240,

265-266,292-293.294-795 Tipos enumerados, 3 15 Tipos estructurc~dos, 3 15 Tipos inonomórticos, 334 Tipos ordindes. 3 15 ripm iubrango, 3 15 Tipos. V h e Tipo de d;itos I'okcn de fen de iirchivu, 77 Tokcn i x ; i . 32

c:ttegorías de, 43 de lenguaje TINY. 75t definición. 32n en anúlisis siiithctico ascendente,

198-199 en análisis sintúctico LR(I), 217-218 en archivos de especificación de

Yacc, 227,229,230 en gramáticas libres de contexto.

97-98, 100 en Lenguajes de programación. 43-47 para tln de archivo, 77 sinctnnización de, 184-185 valores de cadena de, 32-33, 407 Véase tainbién Terminales

Tr~ducción, 7- 12, 1 LO Traducción de sintaxis dirigida, princi-

pio de, 110 transformaciones para niejorarniento de

c~idigo. 468 Transición any, .50ilrís., 51 Transiciones de error, 50 Transiciones E , 56-57,OJ-69, 2 17-218 Transiciones LR(1). 2 17-2 18, 222-223 Transiciones múltiples, eliminación, h Y Transiciones other, 50, 50iliis.,

52iltts., 52-53 Transiciones que no consumen recursos

de enbada. 62 Transiciones. V<:rrse tmbiin Trarisicioiie:

E; Transiciones de error; Transicio~. nes LR(1 ); transiciones ot!isr

en autómatas finitos, 48,19-5 1 en ai~tómatas finitos no determinísticos.

63-64 Transportabilidad, 18-2 1. 2 l ilus. Triples, para código en tres direcciones,

402-404. 404'1~1s.

Unidades de compilación, 102 Uiiión, 1311

disjunta, 3 17 (te cadenas, 36 de conjunto infinito, 36 de conjuntos, 3 17-3 18 cIc lenguajes, 35-36 de u11 conjunto de esta<l«s, 69-70

l.;iilerh ,7p,.? '?f. Y&-

Page 582: Libro - Construccion de Compiladores Principios y Practica [Kenneth C Louden]

Variables no locales, 358-359, X5.366, .360ilir,s., 302r

Viiriablzs. Vi<ise t<rnrDi6n Variables hoolcanas; Variables de iniliicciún: Pscudov~uiaiiles: Vari;ihics ile cstnclo

asignación de ~rriemoria pira, 259, 260, 300-301

ci~i~servaciún eii registros. 482, .4X3ilirs.. 1H4ilrr.s.

dcscript»res dc registro j dii-eccitin para, 47'2, 479-481, 4XOt, 18 l t

cn lenguaje TINY, 22, 133- 134 obrención de delíriiciones de. 476-477

Vctitena de registro, en la SparcStation de Sun, 453n, 47lrr

Vdicación de tipos, l O, 300, 301, 7 13, 329-334, 343-344r

Verificación de tipos de Hinilley-Miliier. 4, 30r

Verificadores <le tipo, 260, 329-031 Viticulos de acceso, .767-370, 369ihrs.,

370ilrrs., 372ibu., 373iliis., 3926 Vínculos de control, 353, 354iius.,

156iilrs., 357il~rs., 35Xiirr.s., 35ifilrr.s., 360ilus,, 361 ilus., 362ilrrs., 363iIirs., 30lilri.r., 365iltrs.. 366iltu., 307ilus., ?C>'AIrrs., 37Oilu.s.. 372iItrs., 313ili<.s., 13X,448ihis.

Vírtculos dinimicos, 351 Vínwl«s estáticos, 367. Vkse /iinihiPtr

Vínculos de acceso Viiici~los heniianos, 114, 135-138,

278-27') Vínculos hijo (hijos)

atrihiitos tiercdad<i\ por, 278-27') cle ntidos de árbol de aiiálisis >iiitáctico,

Y:icc ( y 1 anotller coiiipiler-conipilcr, "otro cornpiladiir de ~oinpilad<ir rnás"). 4, IYhr, 216-242, 253-254p, 256r, 290, 292, 293, 342-343e. 48%

ttcciones integradas en, 241-242 descripci5n cie la ejecuci6n de anali-

,aclr)res bin?&ctic«s generados irle- tliaiite, 238-239

ejercicios de programación cm, 254-256r

gci~wacih de cinaiir~idores sintácticos asccndentes con, 197- 198

geiieración de uri analizador sintictico TINY con, 243-245

nombres intenios y mecanismos de cleí'iniciiin de, 2421

cipciorici de, 230-235 orpdnizrición básica de, 226-230 procedimiento gritCode en, 441 i , 4 I Jiliis.

reciiperaciAn de errores en, 247-250 reglas de n o nrribigüedad y rcscilacitin

de conflictos de anilisis sintictico con, 235-238

tablas de anilisis sintácrico de, 234-235, 2331

tipos de valor arhitrerici en. 239-241 Y iixt;ip«sicidii. 35, 36. C'<;ci.sr tnmbi2n

Operación de ~«ncaten:icih