Está en la página 1de 375

Compiladores e interpretes:

teora y prctica
www.librosite.net/pulido

Manuel Alfonseca Moreno Marina de la Cruz Echeanda Alfonso Ortega de la Puente Estrella Pulido Caabate

Compiladores e intrpretes: teora y prctica

Compiladores e intrpretes: teora y prctica


Manuel Alfonseca Moreno Marina de la Cruz Echeanda Alfonso Ortega de la Puente Estrella Pulido Caabate
Departamento de Ingeniera Informtica Universidad Autnoma de Madrid

Madrid Mxico Santaf de Bogot Buenos Aires Caracas Lima Montevideo San Juan San Jos Santiago So Paulo Reading, Massachusetts Harlow, England

Datos de catalogacin bibliogrfica ALFONSECA MORENO, M.; DE LA CRUZ ECHEANDA, M.; ORTEGA DE LA PUENTE, A.; PULIDO CAABATE, E. Compiladores e intrpretes: teora y prctica PEARSON EDUCACIN, S.A., Madrid, 2006 ISBN 10: 84-205-5031-0 ISBN 13: 978-84-205-5031-2 MATERIA: Informtica, 004.4 Formato: 195 250 mm Pginas: 376

Queda prohibida, salvo excepcin prevista en la Ley, cualquier forma de reproduccin, distribucin, comunicacin pblica y transformacin de esta obra sin contar con autorizacin de los titulares de propiedad intelectual. La infraccin de los derechos mencionados puede ser constitutiva de delito contra la propiedad intelectual (arts. 270 y sgts. Cdigo Penal). DERECHOS RESERVADOS 2006 por PEARSON EDUCACIN, S. A. Ribera del Loira, 28 28042 Madrid (Espaa) Alfonseca Moreno, M.; de la Cruz Echeanda, M.; Ortega de la Puente, A.; Pulido Caabate, E. Compiladores e intrpretes: teora y prctica ISBN: 84-205-5031-0 ISBN 13: 978-84-205-5031-2 Depsito Legal: M. PEARSON PRENTICE HALL es un sello editorial autorizado de PEARSON EDUCACIN, S.A. Equipo editorial Editor: Miguel Martn-Romo Tcnico editorial: Marta Caicoya Equipo de produccin: Director: Jos A. Clares Tcnico: Jos A. Hernn Diseo de cubierta: Equipo de diseo de PEARSON EDUCACIN, S. A. Composicin: JOSUR TRATAMIENTOS DE TEXTOS, S.L. Impreso por: IMPRESO EN ESPAA - PRINTED IN SPAIN
Este libro ha sido impreso con papel y tintas ecolgico

Contenido

Captulo 1.

Lenguajes, gramticas y procesadores 1.1. Gdel y Turing 1.2. Autmatas 1.3. Lenguajes y gramticas 1.4. Mquinas abstractas y lenguajes formales 1.5. Alfabetos, smbolos y palabras 1.6. Operaciones con palabras 1.6.1. Concatenacin de dos palabras 1.6.2. Monoide libre 1.6.3. Potencia de una palabra 1.6.4. Reflexin de una palabra 1.7. Lenguajes 1.7.1. Unin de lenguajes 1.7.2. Concatenacin de lenguajes 1.7.3. Binoide libre 1.7.4. Potencia de un lenguaje 1.7.5. Clausura positiva de un lenguaje 1.7.6. Iteracin, cierre o clausura de un lenguaje 1.7.7. Reflexin de lenguajes 1.7.8. Otras operaciones 1.8. Ejercicios 1.9. Conceptos bsicos sobre gramticas 1.9.1. Notacin de Backus 1.9.2. Derivacin directa 1.9.3. Derivacin 1.9.4. Relacin de Thue 1.9.5. Formas sentenciales y sentencias

1 1 2 3 3 5 5 5 6 7 7 7 8 8 9 9 10 10 10 11 11 11 12 13 13 14 14

vi

Compiladores e intrpretes: teora y prctica

1.10.

1.11.

1.12.

1.13.

1.14. 1.15. Captulo 2.

1.9.6. Lenguaje asociado a una gramtica 1.9.7. Frases y asideros 1.9.8. Recursividad 1.9.9. Ejercicios Tipos de gramticas 1.10.1. Gramticas de tipo 0 1.10.2. Gramticas de tipo 1 1.10.3. Gramticas de tipo 2 1.10.4. Gramticas de tipo 3 1.10.5. Gramticas equivalentes 1.10.6. Ejercicios rboles de derivacin 1.11.1. Subrbol 1.11.2. Ambigedad 1.11.3. Ejercicios Gramticas limpias y bien formadas 1.12.1. Reglas innecesarias 1.12.2. Smbolos inaccesibles 1.12.3. Reglas superfluas 1.12.4. Eliminacin de smbolos no generativos 1.12.5. Eliminacin de reglas no generativas 1.12.6. Eliminacin de reglas de redenominacin 1.12.7. Ejemplo 1.12.8. Ejercicio Lenguajes naturales y artificiales 1.13.1. Lenguajes de programacin de computadoras 1.13.2. Procesadores de lenguaje 1.13.3. Partes de un procesador de lenguaje 1.13.4. Nota sobre sintaxis y semntica Resumen Bibliografa

14 14 15 15 15 16 17 17 18 18 19 19 21 21 22 23 23 23 23 24 24 24 24 25 25 26 26 28 29 30 31 33 33 34 35 35 37 37 38 38 39 40 44

Tabla de smbolos 2.1. Complejidad temporal de los algoritmos de bsqueda 2.1.1. Bsqueda lineal 2.1.2. Bsqueda binaria 2.1.3. Bsqueda con rboles binarios ordenados 2.1.4. Bsqueda con rboles AVL 2.1.5. Resumen de rendimientos 2.2. El tipo de datos diccionario 2.2.1. Estructura de datos y operaciones 2.2.2. Implementacin con vectores ordenados 2.2.3. Implementacin con rboles binarios ordenados 2.2.4. Implementacin con AVL

Contenido

vii

2.3. Implementacin del tipo de dato diccionario con tablas hash 2.3.1. Conclusiones sobre rendimiento 2.3.2. Conceptos relacionados con tablas hash 2.3.3. Funciones hash 2.3.4. Factor de carga 2.3.5. Solucin de las colisiones 2.3.6. Hash con direccionamiento abierto 2.3.7. Hash con encadenamiento 2.4. Tablas de smbolos para lenguajes con estructuras de bloques 2.4.1. Conceptos 2.4.2. Uso de una tabla por mbito 2.4.3. Evaluacin de estas tcnicas 2.4.4. Uso de una sola tabla para todos los mbitos 2.5. Informacin adicional sobre los identificadores en las tablas de smbolos 2.6. Resumen 2.7. Ejercicios y otro material prctico 2.8. Bibliografa Captulo 3. Anlisis morfolgico 3.1. Introduccin 3.2. Expresiones regulares 3.3. Autmata Finito No Determinista (AFND) para una expresin regular 3.4. Autmata Finito Determinista (AFD) equivalente a un AFND 3.5. Autmata finito mnimo equivalente a uno dado 3.6. Implementacin de autmatas finitos deterministas 3.7. Otras tareas del analizador morfolgico 3.8. Errores morfolgicos 3.9. Generacin automtica de analizadores morfolgicos: la herramienta lex 3.9.1. Expresiones regulares en lex 3.9.2. El fichero de especificacin lex 3.9.3. Cmo funciona yylex()? 3.9.4. Condiciones de inicio 3.10. Resumen 3.11. Ejercicios 3.12. Bibliografa Anlisis sintctico 4.1. Conjuntos importantes en una gramtica 4.2. Anlisis sintctico descendente 4.2.1. Anlisis descendente con vuelta atrs 4.2.2. Anlisis descendente selectivo

44 45 45 46 50 50 50 55 56 56 58 60 60 62 62 62 63 65 65 67 68 71 73 75 76 77 78 78 79 80 83 85 86 87 89 90 93 93 99

Captulo 4.

viii

Compiladores e intrpretes: teora y prctica

4.3.

4.4.

4.5. 4.6. Captulo 5.

4.2.3. Anlisis LL(1) mediante el uso de la forma normal de Greibach 4.2.4. Anlisis LL(1) mediante el uso de tablas de anlisis Anlisis sintctico ascendente 4.3.1. Introduccin a las tcnicas del anlisis ascendente 4.3.2. Algoritmo general para el anlisis ascendente 4.3.3. Anlisis LR(0) 4.3.4. De LR(0) a SLR(1) 4.3.5. Anlisis SLR(1) 4.3.6. Ms all de SLR(1) 4.3.7. Anlisis LR(1) 4.3.8. LALR(1) Gramticas de precedencia simple 4.4.1. Notas sobre la teora de relaciones 4.4.2. Relaciones y matrices booleanas 4.4.3. Relaciones y conjuntos importantes de la gramtica 4.4.4. Relaciones de precedencia 4.4.5. Gramtica de precedencia simple 4.4.6. Construccin de las relaciones 4.4.7. Algoritmo de anlisis 4.4.8. Funciones de precedencia Resumen Ejercicios

99 111 114 114 116 127 138 140 147 148 159 168 169 170 171 175 176 177 177 180 183 183 191 191 191 192 194 196 199 199 199 203 205 208 211 217 217 218 221

Anlisis semntico 5.1. Introduccin al anlisis semntico 5.1.1. Introduccin a la semntica de los lenguajes de programacin de alto nivel 5.1.2. Objetivos del analizador semntico 5.1.3. Anlisis semntico y generacin de cdigo 5.1.4. Anlisis semntico en compiladores de un solo paso 5.1.5. Anlisis semntico en compiladores de ms de un paso 5.2. Gramticas de atributos 5.2.1. Descripcin informal de las gramticas de atributos y ejemplos de introduccin 5.2.2. Descripcin formal de las gramticas de atributos 5.2.3. Propagacin de atributos y tipos de atributos segn su clculo 5.2.4. Algunas extensiones 5.2.5. Nociones de programacin con gramticas de atributos 5.3. Incorporacin del analizador semntico al sintctico 5.3.1. Dnde se guardan los valores de los atributos semnticos? 5.3.2. Orden de recorrido del rbol de anlisis 5.3.3. Tipos interesantes de gramticas de atributos

Contenido

ix

5.4.

5.5.

5.6. 5.7. 5.8. Captulo 6.

5.3.4. Tcnica general del anlisis semntico en compiladores de dos o ms pasos 5.3.5. Evaluacin de los atributos por los analizadores semnticos en los compiladores de slo un paso Gramticas de atributos para el anlisis semntico de los lenguajes de programacin 5.4.1. Algunas observaciones sobre la informacin semntica necesaria para el anlisis de los lenguajes de programacin de alto nivel 5.4.2. Declaracin de identificadores 5.4.3. Expresiones aritmticas 5.4.4. Asignacin de valor a los identificadores 5.4.5. Instrucciones condicionales 5.4.6. Instrucciones iterativas (bucles) 5.4.7. Procedimientos Algunas herramientas para la generacin de analizadores semnticos 5.5.1. Estructura del fichero fuente de yacc 5.5.2. Seccin de definiciones 5.5.3. Seccin de reglas 5.5.4. Seccin de funciones de usuario 5.5.5. Conexin entre yacc y lex Resumen Bibliografa Ejercicios

223 227 228

229 230 231 231 232 233 233 233 234 235 237 239 240 240 241 241 243 243 246 249 253 254 254 254 256 258 258 259 267 282 282 285 286 286

Generacin de cdigo 6.1. Generacin directa de cdigo ensamblador en un solo paso 6.1.1. Gestin de los registros de la mquina 6.1.2. Expresiones 6.1.3. Punteros 6.1.4. Asignacin 6.1.5. Entrada y salida de datos 6.1.6. Instrucciones condicionales 6.1.7. Bucles 6.1.8. Funciones 6.2. Cdigo intermedio 6.2.1. Notacin sufija 6.2.2. Cudruplas 6.3. Resumen 6.4. Ejercicios Optimizacin de cdigo 7.1. Tipos de optimizaciones 7.1.1. Optimizaciones dependientes de la mquina

Captulo 7.

Compiladores e intrpretes: teora y prctica

7.1.2. Optimizaciones independientes de la mquina Instrucciones especiales Reordenacin de cdigo Ejecucin en tiempo de compilacin 7.4.1. Algoritmo para la ejecucin en tiempo de compilacin 7.5. Eliminacin de redundancias 7.5.1. Algoritmo para la eliminacin de redundancias 7.6. Reordenacin de operaciones 7.6.1. Orden cannico entre los operandos de las expresiones aritmticas 7.6.2. Aumento del uso de operaciones mondicas 7.6.3. Reduccin del nmero de variables intermedias 7.7. Optimizacin de bucles 7.7.1. Algoritmo para la optimizacin de bucles mediante reduccin de fuerza 7.7.2. Algunas observaciones sobre la optimizacin de bucles por reduccin de fuerza 7.8. Optimizacin de regiones 7.8.1. Algoritmo de planificacin de optimizaciones utilizando regiones 7.9. Identificacin y eliminacin de las asignaciones muertas 7.10. Resumen 7.11. Ejercicios 7.2. 7.3. 7.4. Captulo 8. Intrpretes 8.1. Lenguajes interpretativos 8.2 Comparacin entre compiladores e intrpretes 8.2.1. Ventajas de los intrpretes 8.2.2. Desventajas de los intrpretes 8.3. Aplicaciones de los intrpretes 8.4. Estructura de un intrprete 8.4.1. Diferencias entre un ejecutor y un generador de cdigo 8.4.2. Distintos tipos de tabla de smbolos en un intrprete 8.5. Resumen 8.6. Bibliografa Tratamiento de errores 9.1. Deteccin de todos los errores verdaderos 9.2. Deteccin incorrecta de errores falsos 9.3. Generacin de mensajes de error innecesarios 9.4. Correccin automtica de errores 9.5. Recuperacin de errores en un intrprete 9.6. Resumen

286 286 287 288 289 292 293 300 301 301 302 304 305 309 309 311 313 314 315 317 318 319 319 321 322 322 323 324 326 326 327 327 329 330 331 333 334

Captulo 9.

Contenido

xi

Captulo 10.

Gestin de la memoria 10.1. Gestin de la memoria en un compilador 10.2. Gestin de la memoria en un intrprete 10.2.1. Algoritmos de recoleccin automtica de basura 10.3. Resumen

337 337 347 348 353 355

ndice analtico

Captulo

Lenguajes, gramticas y procesadores


1.1 Gdel y Turing
En el ao 1931 se produjo una revolucin en las ciencias matemticas, con el descubrimiento realizado por Kurt Gdel (1906-1978) y publicado en su famoso artculo [1], que quiz deba considerarse el avance matemtico ms importante del siglo XX. En sntesis, el teorema de Gdel dice lo siguiente: Toda formulacin axiomtica consistente de la teora de nmeros contiene proposiciones indecidibles. Es decir, cualquier teora matemtica ha de ser incompleta. Siempre habr en ella afirmaciones que no se podrn demostrar ni negar. El teorema de Gdel puso punto final a las esperanzas de los matemticos de construir un sistema completo y consistente, en el que fuese posible demostrar cualquier teorema. Estas esperanzas haban sido expresadas en 1900 por David Hilbert (1862-1943), quien generaliz sus puntos de vista proponiendo el problema de la decisin (Entscheidungsproblem), cuyo objetivo era descubrir un mtodo general para decidir si una frmula lgica es verdadera o falsa. En 1937, el matemtico ingls Alan Mathison Turing (1912-1953) public otro artculo famoso sobre los nmeros calculables, que desarroll el teorema de Gdel y puede considerarse el origen oficial de la informtica terica. En este artculo introdujo la mquina de Turing, una entidad matemtica abstracta que formaliz por primera vez el concepto de algoritmo1 y result ser precursora de las mquinas de calcular automticas, que comenzaron a extenderse a partir de la dcada siguiente. Adems, el teorema de Turing demostraba que existen problemas irresolubles, es decir, que ninguna mquina de Turing (y, por ende, ninguna computadora) ser
1 Recurdese que se llama algoritmo a un conjunto de reglas que permite obtener un resultado determinado a partir de ciertos datos de partida. El nombre procede del matemtico persa Abu Jafar Mohammed ibn Musa al-Jowrizm, autor de un tratado de aritmtica que se public hacia el ao 825 y que fue muy conocido durante la Edad Media.

Compiladores e intrpretes: teora y prctica

capaz de obtener su solucin. Por ello se considera a Turing el padre de la teora de la computabilidad. El teorema de Turing es, en el fondo, equivalente al teorema de Gdel. Si el segundo demuestra que no todos los teoremas pueden demostrarse, el primero dice que no todos los problemas pueden resolverse. Adems, la demostracin de ambos teoremas es muy parecida. Uno de esos problemas que no se puede resolver es el denominadol problema de la parada de la mquina de Turing. Puede demostrarse que la suposicin de que es posible predecir, dada la descripcin de una mquina de Turing y la entrada que recibe, si llegar a pararse o si continuar procesando informacin indefinidamente, lleva a una contradiccin. Esta forma de demostracin, muy utilizada en las ciencias matemticas, se llama reduccin al absurdo.

1.2 Autmatas
El segundo eslabn en la cadena vino de un campo completamente diferente: la ingeniera elctrica. En 1938, otro artculo famoso [2] del matemtico norteamericano Claude Elwood Shannon (1916-2001), quien ms tarde sera ms conocido por su teora matemtica de la comunicacin, vino a establecer las bases para la aplicacin de la lgica matemtica a los circuitos combinatorios y secuenciales, construidos al principio con rels y luego con dispositivos electrnicos de vaco y de estado slido. A lo largo de las dcadas siguientes, las ideas de Shannon se convirtieron en la teora de las mquinas secuenciales y de los autmatas finitos. Los autmatas son sistemas capaces de transmitir informacin. En sentido amplio, todo sistema que acepta seales de su entorno y, como resultado, cambia de estado y transmite otras seales al medio, puede considerarse como un autmata. Con esta definicin, cualquier mquina, una central telefnica, una computadora, e incluso los seres vivos, los seres humanos y las sociedades se comportaran como autmatas. Este concepto de autmata es demasiado general para su estudio terico, por lo que se hace necesario introducir limitaciones en su definicin. Desde su nacimiento, la teora de autmatas encontr aplicacin en campos muy diversos, pero que tienen en comn el manejo de conceptos como el control, la accin, la memoria. A menudo, los objetos que se controlan, o se recuerdan, son smbolos, palabras o frases de algn tipo. Estos son algunos de los campos en los que ha encontrado aplicacin la Teora de Autmatas: Teora de la comunicacin. Teora del control. Lgica de los circuitos secuenciales. Computadoras. Redes conmutadoras y codificadoras. Reconocimiento de patrones. Fisiologa del sistema nervioso. Estructura y anlisis de los lenguajes de programacin para computadoras. Traduccin automtica de lenguajes. Teora algebraica de lenguajes.

Captulo 1. Lenguajes, gramticas y procesadores

Se sabe que un autmata (o una mquina secuencial) recibe informacin de su entorno (entrada o estmulo), la transforma y genera nueva informacin, que puede transmitirse al entorno (salida o respuesta). Puede darse el caso de que la informacin que devuelve el autmata sea muy reducida: podra ser una seal binaria (como el encendido o apagado de una lmpara), que indica si la entrada recibida por el autmata es aceptada o rechazada por ste. Tendramos, en este caso, un autmata aceptador.

1.3 Lenguajes y gramticas


El tercer eslabn del proceso surgi de un campo que tradicionalmente no haba recibido consideracin de cientfico: la lingstica, la teora de los lenguajes y las gramticas. En la dcada de 1950, el lingista norteamericano Avram Noam Chomsky (1928-) revolucion su campo de actividad con la teora de las gramticas transformacionales [3, 4], que estableci las bases de la lingstica matemtica y proporcion una herramienta que, aunque Chomsky la desarroll para aplicarla a los lenguajes naturales, facilit considerablemente el estudio y la formalizacin de los lenguajes de computadora, que comenzaban a aparecer precisamente en aquella poca. El estudio de los lenguajes se divide en el anlisis de la estructura de las frases (gramtica) y de su significado (semntica). A su vez, la gramtica puede analizar las formas que toman las palabras (morfologa), su combinacin para formar frases correctas (sintaxis) y las propiedades del lenguaje hablado (fontica). Por el momento, tan slo esta ltima no se aplica a los lenguajes de computadora. Aunque desde el punto de vista terico la distincin entre sintaxis y semntica es un poco artificial, tiene una enorme trascendencia desde el punto de vista prctico, especialmente para el diseo y construccin de compiladores, objeto de este libro.

1.4 Mquinas abstractas y lenguajes formales


La teora de lenguajes formales result tener una relacin sorprendente con la teora de mquinas abstractas. Los mismos fenmenos aparecen independientemente en ambas disciplinas y es posible establecer correspondencias entre ellas (lo que los matemticos llamaran un isomorfismo). Chomsky clasific las gramticas y los lenguajes formales de acuerdo con una jerarqua de cuatro grados, cada uno de los cuales contiene a todos los siguientes. El ms general se llama gramticas del tipo 0 de Chomsky. A estas gramticas no se les impone restriccin alguna. En consecuencia, el conjunto de los lenguajes que representan coincide con el de todos los lenguajes posibles. El segundo grado es el de las gramticas del tipo 1, que introducen algunas limitaciones en la estructura de las frases, aunque se permite que el valor sintctico de las palabras dependa de su

Compiladores e intrpretes: teora y prctica

posicin en la frase, es decir, de su contexto. Por ello, los lenguajes representados por estas gramticas se llaman lenguajes sensibles al contexto. Las gramticas del tercer nivel son las del tipo 2 de Chomsky, que restringen ms la libertad de formacin de las reglas gramaticales: en las gramticas de este tipo, el valor sintctico de una palabra es independiente de su posicin en la frase. Por ello, los lenguajes representados por estas gramticas se denominan lenguajes independientes del contexto. Por ltimo, las gramticas del tipo 3 de Chomsky tienen la estructura ms sencilla y corresponden a los lenguajes regulares. En la prctica, todos los lenguajes de computadora quedan por encima de este nivel, pero los lenguajes regulares no dejan por eso de tener aplicacin. Pues bien: paralelamente a esta jerarqua de gramticas y lenguajes, existe otra de mquinas abstractas equivalentes. A las gramticas del tipo 0 les corresponden las mquinas de Turing; a las del tipo 1, los autmatas acotados linealmente; a las del tipo 2, los autmatas a pila; finalmente, a las del tipo 3, corresponden los autmatas finitos. Cada uno de estos tipos de mquinas es capaz de resolver problemas cada vez ms complicados: los ms sencillos, que corresponden a los autmatas finitos, se engloban en el lgebra de las expresiones regulares. Los ms complejos precisan de la capacidad de una mquina de Turing (o de cualquier otro dispositivo equivalente, computacionalmente completo, como una computadora digital) y se denominan problemas recursivamente enumerables. Y, por supuesto, segn descubri Turing, existen an otros problemas que no tienen solucin: los problemas no computables. La Figura 1.1 resume la relacin entre las cuatro jerarquas de las gramticas, los lenguajes, las mquinas abstractas y los problemas que son capaces de resolver.

Problemas no computables Gramticas tipo 0 de Chomsky Gramticas tipo 1 de Chomsky Gramticas tipo 2 de Chomsky Gramticas tipo 3 de Chomsky

Lenguajes computables Lenguajes dependientes del contexto Lenguajes independientes del contexto Lenguajes regulares

Mquinas de Turing

Problemas recursivamente enumerables

Autmatas lineales acotados Autmatas a pila Autmatas finitos deterministas

Expresiones regulares

Figura 1.1. Relacin jerrquica de las mquinas abstractas y los lenguajes formales.

Captulo 1. Lenguajes, gramticas y procesadores

1.5 Alfabetos, smbolos y palabras


Se llama alfabeto a un conjunto finito, no vaco. Los elementos de un alfabeto se llaman smbolos. Un alfabeto se define por enumeracin de los smbolos que contiene. Por ejemplo: 1 = {A,B,C,D,E,...,Z} 2 = {0,1} 3 = {0,1,2,3,4,5,6,7,8,9,.} 4 = {/,\} Se llama palabra, formada con los smbolos de un alfabeto, a una secuencia finita de los smbolos de ese alfabeto. Se utilizarn letras minsculas como x o y para representar las palabras de un alfabeto: x = JUAN (palabra sobre 1) y = 1234 (palabra sobre 3) Se llama longitud de una palabra al nmero de letras que la componen. La longitud de la palabra x se representa con la notacin |x|. La palabra cuya longitud es cero se llama palabra vaca y se representa con la letra griega lambda (). Evidentemente, cualquiera que sea el alfabeto considerado, siempre puede formarse con sus smbolos la palabra vaca. El conjunto de todas las palabras que se pueden formar con las letras de un alfabeto se llama lenguaje universal de . De momento se utilizar la notacin W() para representarlo. Es evidente que W() es un conjunto infinito. Incluso en el peor caso, si el alfabeto slo tiene una letra (por ejemplo, = {a}), las palabras que podremos formar son: W() = {, a, aa, aaa, ...} Es obvio que este conjunto tiene infinitos elementos. Obsrvese que la palabra vaca pertenece a los lenguajes universales de todos los alfabetos posibles.

1.6 Operaciones con palabras


Esta seccin define algunas operaciones sobre el conjunto W() de todas las palabras que se pueden construir con las letras de un alfabeto = {a1, a2, a3, ...}.

1.6.1. Concatenacin de dos palabras


Sean dos palabras x e y tales que x W(), y W(). Suponiendo que x tiene i letras, e y tiene j letras: x = a1a2...ai y = b1b2...bj

Compiladores e intrpretes: teora y prctica

Donde todas las letras ap, bq son smbolos del alfabeto . Se llama concatenacin de las palabras x e y (y se representa xy) a otra palabra z, que se obtiene poniendo las letras de y a continuacin de las letras de x: z = xy = a1...aib1...bj La concatenacin se representa a veces tambin x.y. Esta operacin tiene las siguientes propiedades: 1. Operacin cerrada: la concatenacin de dos palabras de W() es una palabra de W(). x W() y W() xy W() 2. Propiedad asociativa: x(yz) = (xy)z Por cumplir las dos propiedades anteriores, la operacin de concatenacin de las palabras de un alfabeto es un semigrupo. 3. Existencia de elemento neutro. La palabra vaca () es el elemento neutro de la concatenacin de palabras, tanto por la derecha, como por la izquierda. En efecto, sea x una palabra cualquiera. Se cumple que: x = x = x Por cumplir las tres propiedades anteriores, la operacin de concatenacin de las palabras de un alfabeto es un monoide (semigrupo con elemento neutro). 4. La concatenacin de palabras no tiene la propiedad conmutativa, como demuestra un contraejemplo. Sean las palabras x=abc, y=ad. Se verifica que xy=abcad yx=adabc Es evidente que xy no es igual a yx. Sea z = xy. Se dice que x es cabeza de z y que y es cola de z. Adems, x es cabeza propia de z si y no es la palabra vaca. De igual manera, y es cola propia de z si x no es la palabra vaca. Se observar que la funcin longitud de una palabra tiene, respecto a la concatenacin, propiedades semejantes a las de la funcin logaritmo respecto a la multiplicacin de nmeros reales: |xy| = |x| + |y|

1.6.2. Monoide libre


Sea un alfabeto . Cada una de sus letras puede considerarse como una palabra de longitud igual a 1, perteneciente a W(). Aplicando a estas palabras elementales la operacin concatenacin, puede formarse cualquier palabra de W() excepto , la palabra vaca. Se dice entonces que es un conjunto de generadores de W()-{}. Este conjunto, junto con la operacin concatenacin, es un semigrupo, pero no un monoide (pues carece de elemento neutro). Se dice que W()-{}

Captulo 1. Lenguajes, gramticas y procesadores

es el semigrupo libre engendrado por . Aadiendo ahora la palabra vaca, diremos que W() es el monoide libre generado por .

1.6.3. Potencia de una palabra


Estrictamente hablando, sta no es una operacin nueva, sino una notacin que reduce algunos casos de la operacin anterior. Se llama potencia i-sima de una palabra a la operacin que consiste en concatenarla consigo misma i veces. Como la concatenacin tiene la propiedad asociativa, no es preciso especificar el orden en que tienen que efectuarse las operaciones. xi = xxx...x (i veces) Definiremos tambin x1 = x. Es evidente que se verifica que: xi+1 = xix = xxi (i>0) xixj = xi+j (i,j>0) Para que las dos relaciones anteriores se cumplan tambin para i,j = 0 bastar con definir, para todo x, x0 = Tambin en este caso, las propiedades de la funcin longitud son semejantes a las del logaritmo. |xi| = i.|x|

1.6.4. Reflexin de una palabra


Sea x = a1a2...an. Se llama palabra refleja o inversa de x, y se representa x-1: x-1 = an...a2a1 Es decir, a la que est formada por las mismas letras en orden inverso. La funcin longitud es invariante respecto a la reflexin de palabras: |x-1| = |x|

1.7 Lenguajes
Se llama lenguaje sobre el alfabeto a todo subconjunto del lenguaje universal de . L W() En particular, el conjunto vaco es un subconjunto de W() y se llama por ello lenguaje vaco. Este lenguaje no debe confundirse con el que contiene como nico elemento la palabra

Compiladores e intrpretes: teora y prctica

vaca, {}, que tambin es un subconjunto (diferente) de W(). Para distinguirlos, hay que fijarse en que el cardinal (el nmero de elementos) de estos dos conjuntos es distinto. c() = 0 c({}) = 1 Obsrvese que tanto como {} son lenguajes sobre cualquier alfabeto. Por otra parte, un alfabeto puede considerarse tambin como uno de los lenguajes generados por l mismo: el que contiene todas las palabras de una sola letra.

1.7.1. Unin de lenguajes


Sean dos lenguajes definidos sobre el mismo alfabeto, L1 W(), L2 W(). Llamamos unin de los dos lenguajes, L1 L2, al lenguaje definido as: {x | x L1 x L2} Es decir, al conjunto formado por las palabras que pertenezcan indistintamente a uno u otro de los dos lenguajes. La unin de lenguajes tiene las siguientes propiedades: 1. Operacin cerrada: la unin de dos lenguajes sobre el mismo alfabeto es tambin un lenguaje sobre dicho alfabeto. 2. Propiedad asociativa: (L1 L2) L3 = L1 (L2 L3). 3. Existencia de elemento neutro: cualquiera que sea el lenguaje L, el lenguaje vaco cumple que L=L=L Por cumplir las tres propiedades anteriores, la unin de lenguajes es un monoide. 4. Propiedad conmutativa: cualesquiera que sean L1 y L2, se verifica que L1 L2 = L2 L1. Por tener las cuatro propiedades anteriores, la unin de lenguajes es un monoide abeliano. 5. Propiedad idempotente: cualquiera que sea L, se verifica que LL=L

1.7.2. Concatenacin de lenguajes


Sean dos lenguajes definidos sobre el mismo alfabeto, L1 W(), L2 W(). Llamamos concatenacin de los dos lenguajes, L1L2, al lenguaje definido as: {xy | xL1 yL2}

Es decir: todas las palabras del lenguaje concatenacin se forman concatenando una palabra del primer lenguaje con otra del segundo.

Captulo 1. Lenguajes, gramticas y procesadores

La definicin anterior slo es vlida si L1 y L2 contienen al menos un elemento. Extenderemos la operacin concatenacin al lenguaje vaco de la siguiente manera: L = L = La concatenacin de lenguajes tiene las siguientes propiedades: 1. Operacin cerrada: la concatenacin de dos lenguajes sobre el mismo alfabeto es otro lenguaje sobre el mismo alfabeto. 2. Propiedad asociativa: (L1L2)L3 = L1(L2L3). 3. Existencia de elemento neutro: cualquiera que sea el lenguaje L, el lenguaje de la palabra vaca cumple que {}L = L{} = L Por cumplir las tres propiedades anteriores, la concatenacin de lenguajes es un monoide.

1.7.3. Binoide libre


Acabamos de ver que existen dos monoides (la unin y la concatenacin de lenguajes) sobre el conjunto L de todos los lenguajes que pueden definirse con un alfabeto dado . Se dice que estas dos operaciones constituyen un binoide. Adems, las letras del alfabeto pueden considerarse como lenguajes de una sola palabra. A partir de ellas, y mediante las operaciones de unin y concatenacin de lenguajes, puede generarse cualquier lenguaje sobre dicho alfabeto (excepto y {}). Por lo tanto, el alfabeto es un conjunto de generadores para el conjunto L, por lo que L se denomina binoide libre generado por .

1.7.4. Potencia de un lenguaje


Estrictamente hablando, sta no es una operacin nueva, sino una notacin que reduce algunos casos de la operacin anterior. Se llama potencia i-sima de un lenguaje a la operacin que consiste en concatenarlo consigo mismo i veces. Como la concatenacin tiene la propiedad asociativa, no es preciso especificar el orden en que tienen que efectuarse las i operaciones. Li = LLL...L (i veces) Definiremos tambin L1 = L. Es evidente que se verifica que: Li+1 = LiL = LLi (i>0) LiLj = Li+j (i,j>0) Para que las dos relaciones anteriores se cumplan tambin para i,j = 0 bastar con definir, para todo L: L0 = {}

10

Compiladores e intrpretes: teora y prctica

1.7.5. Clausura positiva de un lenguaje


La clausura positiva de un lenguaje L se define as: L + = Li
i=1

Es decir, el lenguaje obtenido uniendo el lenguaje L con todas sus potencias posibles, excepto L0. Obviamente, ninguna clausura positiva contiene la palabra vaca, a menos que dicha palabra est en L. Puesto que el alfabeto es tambin un lenguaje sobre , puede aplicrsele esta operacin. Se ver entonces que + = W()-{}

1.7.6. Iteracin, cierre o clausura de un lenguaje


La iteracin, cierre o clausura de un lenguaje L se define as: L * = Li
i=0

Es decir, el lenguaje obtenido uniendo el lenguaje L con todas sus potencias posibles, incluso L0. Obviamente, todas las clausuras contienen la palabra vaca. Son evidentes las siguientes identidades: L* = L+ {} L+ = LL* = L*L Puesto que el alfabeto es tambin un lenguaje sobre , puede aplicrsele esta operacin. Se ver entonces que * = W() A partir de este momento, representaremos al lenguaje universal sobre el alfabeto con el smbolo *.

1.7.7. Reflexin de lenguajes


Sea L un lenguaje cualquiera. Se llama lenguaje reflejo o inverso de L, y se representa con L-1: {x-1 | xL} Es decir, al que contiene las palabras inversas a las de L.

Captulo 1. Lenguajes, gramticas y procesadores

11

1.7.8. Otras operaciones


Pueden definirse tambin para los lenguajes las operaciones interseccin y complementacin (con respecto al lenguaje universal). Dado que stas son operaciones clsicas de teora de conjuntos, no es preciso detallarlas aqu.

1.8 Ejercicios
1. 2. 3. Sea ={!} y x=!. Definir las siguientes palabras: xx, xxx, x3, x8, x0. Cules son sus longitudes? Definir *. Sea ={0,1,2}, x=00, y=1, z=210. Definir las siguientes palabras: xy, xz, yz, xyz, x3, x2y2, (xy)2, (zxx)3. Cules son sus longitudes, cabezas y colas? Sea ={0,1,2}. Escribir seis de las cadenas ms cortas de + y de *.

1.9 Conceptos bsicos sobre gramticas


Como se ha dicho anteriormente, una gramtica describe la estructura de las frases y de las palabras de un lenguaje. Aplicada a los lenguajes naturales, esta ciencia es muy antigua: los primeros trabajos aparecieron en la India durante la primera mitad del primer milenio antes de Cristo, alcanzndose el mximo apogeo con Panini, que vivi quiz entre los siglos VII y IV antes de Cristo y desarroll la primera gramtica conocida, aplicada al lenguaje snscrito. Casi al mismo tiempo, puede que independientemente, el sofista griego Protgoras (h. 485-ca. 411 a. de J.C.) fund una escuela gramatical, que alcanz su mximo esplendor en el siglo II antes de Cristo. Se llama gramtica formal a la cudrupla G = (T, N, S, P) donde T es el alfabeto de smbolos terminales, y N es el alfabeto de smbolos no terminales. Se verifica que T N = y = T N. N N es el axioma, smbolo inicial, o smbolo distinguido. Finalmente, P es un conjunto finito de reglas de produccin de la forma u ::= v, donde se verifica que: u+ u=xAy x,y* AN v*

12

Compiladores e intrpretes: teora y prctica

Es decir, u es una palabra no vaca del lenguaje universal del alfabeto que contiene al menos un smbolo no terminal y v es una palabra, posiblemente vaca, del mismo lenguaje universal. Veamos un ejemplo de gramtica: T = {0,1,2,3,4,5,6,7,8,9} N = {N,C} S=N P={ N ::= CN N ::= C C ::= 0 C ::= 1 C ::= 2 C ::= 3 C ::= 4 C ::= 5 C ::= 6 C ::= 7 C ::= 8 C ::= 9 }

1.9.1. Notacin de Backus


Notacin abreviada: si el conjunto de producciones contiene dos reglas de la forma u ::= v u ::= w pueden representarse abreviadamente con la notacin u ::= v | w La notacin u::=v de las reglas de produccin, junto con la regla de abreviacin indicada, se denomina Forma Normal de Backus, o BNF, de las iniciales de su forma inglesa Backus Normal Form, o tambin Backus-Naur Form. La gramtica del ejemplo anterior puede representarse en BNF de la manera siguiente: T = {0,1,2,3,4,5,6,7,8,9} N = {N,C} S=N P={ N ::= CN | C C ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 }

Captulo 1. Lenguajes, gramticas y procesadores

13

1.9.2. Derivacin directa


Sea un alfabeto y x::=y una produccin sobre las palabras de ese alfabeto. Sean v y w dos palabras del mismo alfabeto (v,w*). Se dice que w es derivacin directa de v, o que v produce directamente w, o que w se reduce directamente a v, si existen dos palabras z,u*, tales que: v=zxu w=zyu Es decir, si v contiene la palabra x y, al sustituir x por y, v se transforma en w. Indicamos esta relacin con el smbolo v w. COROLARIO: Si x::=y es una produccin sobre , se sigue que xy. Ejemplos: Sea el alfabeto castellano de las letras maysculas, y ME ::= BA una produccin sobre . Es fcil ver que CAMELLOCABALLO. Sea el alfabeto ={0,1,2,N,C}, y el conjunto de producciones N ::= CN N ::= C C ::= 0 C ::= 1 C ::= 2 Pueden demostrarse fcilmente las siguientes derivaciones directas: N CN CCN CCC 2CC 21C 210

1.9.3. Derivacin
Sea un alfabeto y P un conjunto de producciones sobre las palabras de ese alfabeto. Sean v y w dos palabras del mismo alfabeto (v,w*). Se dice que w es derivacin de v, o que v produce w, o que w se reduce a v, si existe una secuencia finita de palabras u0, u1, ..., un (n>0), tales que v = u0 u1 u2 ... un-1 un = w Indicamos esta relacin con el smbolo v + w. La secuencia anterior se llama derivacin de longitud n. En el ejemplo anterior, puede verse que N + 210 mediante una secuencia de longitud 6. COROLARIO: Si v w, entonces v + w mediante una secuencia de longitud 1.

14

Compiladores e intrpretes: teora y prctica

1.9.4. Relacin de Thue


Sea un alfabeto y P un conjunto de producciones sobre las palabras de ese alfabeto. Sean v y w dos palabras del mismo alfabeto. Se dice que existe una relacin de Thue entre v y w si se verifica que v + w o v = w. Expresaremos esta relacin con el smbolo v * w.

1.9.5. Formas sentenciales y sentencias


Sea una gramtica G = (T, N, S, P). Una palabra x * se denomina forma sentencial de G si se verifica que S * x, es decir, si existe una relacin de Thue entre el axioma de la gramtica y x. Dicho de otro modo: si x=S o x deriva de S. Ejercicio: En el ejemplo anterior, comprobar que CCN, CN2 y 123 son formas sentenciales, pero no lo es NCN. Si una forma sentencial x cumple que x T* (es decir, est formada nicamente por smbolos terminales), se dice que x es una sentencia o instruccin generada por la gramtica G. Ejercicio: Cules de las formas sentenciales anteriores son sentencias?

1.9.6. Lenguaje asociado a una gramtica


Sea una gramtica G = ( T, N, S, P). Se llama lenguaje asociado a G, o lenguaje generado por G, o lenguaje descrito por G, al conjunto L(G) = {x | S*x x T*}. Es decir, el conjunto de todas las sentencias de G (todas las cadenas de smbolos terminales que derivan del axioma de G). Ejemplo: El lenguaje asociado a la gramtica de los ejemplos anteriores es el conjunto de todos los nmeros naturales ms el cero.

1.9.7. Frases y asideros


Sea G una gramtica y v=xuy una de sus formas sentenciales. Se dice que u es una frase de la forma sentencial v respecto del smbolo no terminal U N si S * xUy U + u Es decir, si en la derivacin que transforma S en xuy se pasa por una situacin intermedia en la que x e y ya han sido generados, y slo falta transformar el smbolo no terminal U en la frase u. Si U es una forma sentencial de G, entonces todas las frases que derivan de U sern, a su vez, formas sentenciales de G.

Captulo 1. Lenguajes, gramticas y procesadores

15

Una frase de v=xuy se llama frase simple si S * xUy Uu Es decir, si la derivacin de U a u se realiza en un solo paso. Se llama asidero de una forma sentencial v a la frase simple situada ms a la izquierda en v. Ejercicio: En la gramtica que define los nmeros enteros positivos, demostrar que N no es una frase de 1N. Encontrar todas las frases de 1N. Cules son frases simples? Cul es el asidero?

1.9.8. Recursividad
Una gramtica G se llama recursiva en U, U N, si U + xUy. Si x es la palabra vaca (x=) se dice que la gramtica es recursiva a izquierdas. Si y=, se dice que G es recursiva a derechas en U. Si un lenguaje es infinito, la gramtica que lo representa tiene que ser recursiva. Una regla de produccin es recursiva si tiene la forma U ::= xUy. Se dice que es recursiva a izquierdas si x=, y recursiva a derechas si y=.

1.9.9. Ejercicios
1. Sea la gramtica G = ({a,b,c,0,1}, {I}, I, {I::=a|b|c|Ia|Ib|Ic|I0|I1}). Cul es el lenguaje descrito por esta gramtica? Encontrar, si es posible, derivaciones de a, ab0, a0c01, 0a, 11, aaa. 2. Construir una gramtica para el lenguaje {abna | n=0,1,...}. 3. Sea la gramtica G = ({+,-,*,/,(,),i}, {E,T,F}, E, P), donde P contiene las producciones: E ::= T | E+T | E-T T ::= F | T*F | T/F F ::= (E) | i Obtener derivaciones de las siguientes sentencias: i, (i), i*i, i*i+i, i*(i+i).

1.10 Tipos de gramticas


Chomsky clasific las gramticas en cuatro grandes grupos (G0, G1, G2, G3), cada uno de los cuales incluye a los siguientes, de acuerdo con el siguiente esquema: G3 G2 G1 G0

16

Compiladores e intrpretes: teora y prctica

1.10.1. Gramticas de tipo 0


Son las gramticas ms generales. Las reglas de produccin tienen la forma u::=v, donde u+, v*, u=xAy, x,y*, AN, sin ninguna restriccin adicional. Los lenguajes representados por estas gramticas se llaman lenguajes sin restricciones. Puede demostrarse que todo lenguaje representado por una gramtica de tipo 0 de Chomsky puede describirse tambin por una gramtica perteneciente a un grupo un poco ms restringido (gramticas de estructura de frases), cuyas producciones tienen la forma xAy::=xvy, donde x,y,v*, AN. Puesto que v puede ser igual a , se sigue que algunas de las reglas de estas gramticas pueden tener una parte derecha ms corta que su parte izquierda. Si tal ocurre, se dice que la regla es compresora. Una gramtica que contenga al menos una regla compresora se llama gramtica compresora. En las gramticas compresoras, las derivaciones pueden ser decrecientes, pues la longitud de las palabras puede disminuir en cada uno de los pasos de la derivacin. Veamos un ejemplo: Sea la gramtica G = ({a,b}, {A,B,C}, A, P), donde P contiene las producciones A ::= aABC | abC CB ::= BC bB ::= bb bC ::= b Esta es una gramtica de tipo 0, pero no de estructura de frases, pues la regla CB::=BC no cumple las condiciones requeridas. Sin embargo, esta regla podra sustituirse por las cuatro siguientes: CB ::= XB XB ::= XY XY ::= BY BY ::= BC Con este cambio, se pueden obtener las mismas derivaciones en ms pasos, pero ahora s se cumplen las condiciones para que la gramtica sea de estructura de frases. Por tanto, el lenguaje descrito por la primera gramtica es el mismo que el de la segunda. Obsrvese que esta gramtica de estructura de frases equivalente a la gramtica de tipo 0 tiene tres reglas de produccin ms y dos smbolos adicionales (X, Y) en el alfabeto de smbolos no terminales. Esta es la derivacin de la sentencia aaabbb (a3b3) en la gramtica de tipo 0 dada ms arriba: A aABC aaABCBC aaabCBCBC aaabBCBC aaabbCBC aaabbBC aaabbbC aaabbb Obsrvese que esta gramtica es compresora, por contener la regla bC::=b. Puede comprobarse que el lenguaje representado por esta gramtica es {anbn | n=1,2,...}.

Captulo 1. Lenguajes, gramticas y procesadores

17

1.10.2. Gramticas de tipo 1


Las reglas de produccin de estas gramticas tienen la forma xAy::=xvy, donde x,y*, v+, AN, que se interpreta as: A puede transformarse en v cuando se encuentra entre el contexto izquierdo x y el contexto derecho y. Como v no puede ser igual a , se sigue que estas gramticas no pueden contener reglas compresoras. Se admite una excepcin en la regla S::=, que s puede pertenecer al conjunto de producciones de una gramtica de tipo 1. Aparte de esta regla, que lleva a la derivacin trivial S, ninguna derivacin obtenida por aplicacin de las reglas de las gramticas de tipo 1 puede ser decreciente. Es decir: si u1 u2 ... un es una derivacin correcta, se verifica que |u1| |u2| ... |un| En consecuencia, en una gramtica tipo 1, la palabra vaca pertenece al lenguaje representado por la gramtica si y slo si la regla S::= pertenece al conjunto de producciones de la gramtica. Los lenguajes representados por las gramticas tipo 1 se llaman lenguajes dependientes del contexto (context-sensitive, en ingls). Es evidente que toda gramtica de tipo 1 es tambin una gramtica de tipo 0. En consecuencia, todo lenguaje dependiente del contexto es tambin un lenguaje sin restricciones.

1.10.3. Gramticas de tipo 2


Las reglas de produccin de estas gramticas tienen la forma A::=v, donde v*, AN. En particular, v puede ser igual a . Sin embargo, para toda gramtica de tipo 2 que represente un lenguaje L, existe otra equivalente, desprovista de reglas de la forma A::=, que representa al lenguaje L-{}. Si ahora se aade a esta segunda gramtica la regla S::=, el lenguaje representado volver a ser L. Por lo tanto, las gramticas de tipo 2 pueden definirse tambin de esta forma ms restringida: las reglas de produccin tendrn la forma A::=v, donde v+, AN. Adems, pueden contener la regla S::=. Los lenguajes descritos por gramticas de tipo 2 se llaman lenguajes independientes del contexto (context-free, en ingls), pues la conversin de A en v puede aplicarse cualquiera que sea el contexto donde se encuentre A. La sintaxis de casi todos los lenguajes humanos y todos los de programacin de computadoras puede describirse mediante gramticas de este tipo. Es evidente que toda gramtica de tipo 2 cumple tambin los requisitos para ser una gramtica de tipo 1. En consecuencia, todo lenguaje independiente del contexto pertenecer tambin a la clase de los lenguajes dependientes del contexto. Veamos un ejemplo: Sea la gramtica G = ({a,b}, {S}, S, {S::=aSb|ab}). Se trata, evidentemente, de una gramtica de tipo 2. Esta es la derivacin de la sentencia aaabbb (a3b3): S aSb aaSbb aaabbb

18

Compiladores e intrpretes: teora y prctica

Puede comprobarse que el lenguaje representado por esta gramtica es {anbn | n=1,2,...}, es decir, el mismo que se vio anteriormente con un ejemplo de gramtica de tipo 0. En general, un mismo lenguaje puede describirse mediante muchas gramticas diferentes, no siempre del mismo tipo. En cambio, una gramtica determinada describe siempre un lenguaje nico.

1.10.4. Gramticas de tipo 3


Estas gramticas se clasifican en los dos grupos siguientes: 1. Gramticas lineales por la izquierda, cuyas reglas de produccin pueden tener una de las formas siguientes: A ::= a A ::= Va S ::= 2. Gramticas lineales por la derecha, cuyas reglas de produccin pueden tomar una de las formas siguientes: A ::= a A ::= aV S ::= En ambos casos, aT, A,VN y S es el axioma de la gramtica. Los lenguajes que pueden representarse mediante gramticas de tipo 3 se llaman lenguajes regulares. Es fcil ver que toda gramtica de tipo 3 cumple tambin los requisitos para ser gramtica de tipo 2. Por lo tanto, todo lenguaje regular pertenecer tambin a la clase de los lenguajes independientes del contexto. Veamos un ejemplo: Sea la gramtica G = ({0,1}, {A,B}, A, P), donde P contiene las producciones A ::= 1B | 1 B ::= 0A Se trata de una gramtica lineal por la derecha. Es fcil ver que el lenguaje descrito por la gramtica es L2 = {1, 101, 10101, ...} = {1(01)n | n=0,1,...}

1.10.5. Gramticas equivalentes


Se dice que dos gramticas son equivalentes cuando describen el mismo lenguaje.

Captulo 1. Lenguajes, gramticas y procesadores

19

1.10.6. Ejercicios
1. Sean las gramticas siguientes: G1 = ({c}, {S,A}, S, {S::=|A, A::=AA|c}) G2 = ({c,d}, {S,A}, S, {S::=|A, A::=cAd|cd}) G3 = ({c,d}, {S,A}, S, {S::=|A, A::=Ad|cA|c|d}) G4 = ({c,d}, {S,A,B}, S, {S::=cA, A::=d|cA|Bd, B::=d|Bd}) G5 = ({c}, {S,A}, S, {S::=|A, A::=AcA|c}) G6 = ({0,c}, {S,A,B}, S, {S::=AcA, A::=0, Ac::=AAcA|ABc|AcB, B::=A|AB})

Definir el lenguaje descrito por cada una de ellas, as como las relaciones de inclusin entre los seis lenguajes, si las hay. Encontrar una gramtica de tipo 2 equivalente a G6. 2. Sean los lenguajes siguientes: L1 = {0m1n | m n 0} L2 = {0k1m0n | n=k+m} L3 = {wcw | w{0,1}*} L4 = {wcw-1 | w{0,1}*} L5 = {10n | n=0,1,2,...}

Construir una gramtica que describa cada uno de los lenguajes anteriores. 3. Se llama palndromo a toda palabra x que cumpla x=x-1. Se llama lenguaje palindrmico a todo lenguaje cuyas palabras sean todas palndromos. Sean las gramticas G1 = ({a,b}, {S}, S, {S::=aSa|aSb|bSb|bSa|aa|bb}) G2 = ({a,b}, {S}, S, {S::=aS|Sa|bS|Sb|a|b}) Alguna de ellas describe un lenguaje palindrmico? 4. Sea L un lenguaje palindrmico. Es L-1 un lenguaje palindrmico? Lo es L L-1? 5. Sea x un palndromo. Es L=x* un lenguaje palindrmico?

1.11 rboles de derivacin


Toda derivacin de una gramtica de tipo 1, 2 o 3 puede representarse mediante un rbol, que se construye de la siguiente manera: 1. La raz del rbol se denota por el axioma de la gramtica. 2. Una derivacin directa se representa por un conjunto de ramas que salen de un nodo. Al aplicar una regla, un smbolo de la parte izquierda queda sustituido por la palabra x de la

20

Compiladores e intrpretes: teora y prctica

parte derecha. Por cada uno de los smbolos de x se dibuja una rama que parte del nodo dado y termina en otro, denotado por dicho smbolo. Sean dos smbolos A y B en la palabra x. Si A est a la izquierda de B en x, entonces la rama que termina en A se dibujar a la izquierda de la rama que termina en B. Para cada rama, el nodo de partida se llama padre del nodo final. Este ltimo es el hijo del primero. Dos nodos hijos del mismo padre se llaman hermanos. Un nodo es ascendiente de otro si es su padre o es ascendiente de su padre. Un nodo es descendiente de otro si es su hijo o es descendiente de uno de sus hijos. A lo largo del proceso de construccin del rbol, los nodos finales de cada paso sucesivo, ledos de izquierda a derecha, dan la forma sentencial obtenida por la derivacin representada por el rbol. Se llama rama terminal aquella que se dirige hacia un nodo denotado por un smbolo terminal de la gramtica. Este nodo se denomina hoja o nodo terminal del rbol. El conjunto de las hojas del rbol, ledo de izquierda a derecha, da la sentencia generada por la derivacin. Ejemplo: Sea la gramtica G = ({0,1,2,3,4,5,6,7,8,9}, {N,C}, N, {N::=C|CN, C::=0|1|2|3|4|5|6|7|8|9}). Sea la derivacin: N CN CCN CCC 2CC 23C 235 La Figura 1.2 representa el rbol correspondiente a esta derivacin. A veces, un rbol puede representar varias derivaciones diferentes. Por ejemplo, el rbol de la Figura 1.2 representa tambin, entre otras, a las siguientes derivaciones: N CN CCN CCC CC5 2C5 235 N CN 2N 2CN 23N 23C 235 Sea S w1 w2 ... x una derivacin de la palabra x en la gramtica G. Se dice que sta es la derivacin ms a la izquierda de x en G, si en cada uno de los pasos o derivaciones directas se ha aplicado una produccin cuya parte izquierda modifica el smbolo no terminal situado

C 2 3 5

Figura 1.2. rbol equivalente a una derivacin.

Captulo 1. Lenguajes, gramticas y procesadores

21

ms a la izquierda en la forma sentencial anterior. Dicho de otro modo: en cada derivacin directa u v se ha generado el asidero de v. En las derivaciones anteriores, correspondientes al rbol de la Figura 1.2, la derivacin ms a la izquierda es la ltima.

1.11.1. Subrbol
Dado un rbol A correspondiente a una derivacin, se llama subrbol de A al rbol cuya raz es un nodo de A, cuyos nodos son todos los descendientes de la raz del subrbol en A, y cuyas ramas son todas las que unen dichos nodos entre s en A. Los nodos terminales de un subrbol, ledos de izquierda a derecha, forman una frase respecto a la raz del subrbol. Si todos los nodos terminales del subrbol son hijos de la raz, entonces la frase es simple.

1.11.2. Ambigedad
A veces, una sentencia puede obtenerse en una gramtica por medio de dos o ms rboles de derivacin diferentes. En este caso, se dice que la sentencia es ambigua. Una gramtica es ambigua si contiene al menos una sentencia ambigua. Aunque la gramtica sea ambigua, es posible que el lenguaje descrito por ella no lo sea. Puesto que a un mismo lenguaje pueden corresponderle numerosas gramticas, que una de stas sea ambigua no implica que lo sean las dems. Sin embargo, existen lenguajes para los que es imposible encontrar gramticas no ambiguas que los describan. En tal caso, se dice que estos lenguajes son inherentemente ambiguos. Ejemplo: Sea la gramtica G1 = ({i,+,*,(,)}, {E}, E, {E::=E+E|E*E|(E)|i}) y la sentencia i+i*i. La Figura 1.3 representa los dos rboles que generan esta sentencia en dicha gramtica.
E

Figura 1.3. Dos rboles que producen la sentencia i+i*i en G1.

22

Compiladores e intrpretes: teora y prctica

Sin embargo, la gramtica G2 = ({i,+,*,(,)}, {E,T,F}, E, {E::=T|E+T, T::=F|T*F, F::=(E)|i}) es equivalente a la anterior (genera el mismo lenguaje) pero no es ambigua. En efecto, ahora existe un solo rbol de derivacin de la sentencia i+i*i: el de la Figura 1.4.

E E T

T T F F + i * i F

Figura 1.4. rbol de la sentencia i+i*i en G2.

La ambigedad puede definirse tambin as:

Una gramtica es ambigua si existe en ella una sentencia que pueda obtenerse a partir del axioma mediante dos derivaciones ms a la izquierda distintas.

1.11.3. Ejercicios
1. En la gramtica G2 del apartado anterior, dibujar rboles sintcticos para las derivaciones siguientes: ETFi E T F (E) (T) (F) (i) E +T +F +i 2. En la misma gramtica G2, demostrar que las sentencias i+i*i y i*i*i no son ambiguas. Qu operador tiene precedencia en cada una de esas dos sentencias? 3. Demostrar que la siguiente gramtica es ambigua, construyendo dos rboles para cada una de las sentencias i+i*i y i+i+i: ({i,+,-,*,/,(,)}, {E,O}, E, {E::=i |(E)|EOE, O::=+|-|*|/}).

Captulo 1. Lenguajes, gramticas y procesadores

23

1.12 Gramticas limpias y bien formadas


Una gramtica se llama reducida si no contiene smbolos inaccesibles ni reglas superfluas. Se llama limpia si tampoco contiene reglas innecesarias. Se llama gramtica bien formada a una gramtica independiente del contexto que sea limpia y que carezca de smbolos y reglas no generativos y de reglas de redenominacin.

1.12.1. Reglas innecesarias


En una gramtica, las reglas de la forma U::=U son innecesarias y la hacen ambigua. A partir de ahora se supondr que una gramtica no tiene tales reglas o, si las tiene, sern eliminadas.

1.12.2. Smbolos inaccesibles


Supngase que una gramtica contiene una regla de la forma U::=x, donde U es un smbolo no terminal, distinto del axioma, que no aparece en la parte derecha de ninguna otra regla. Se dice que U es un smbolo inaccesible desde el axioma. Si un smbolo V es accesible desde el axioma S, debe cumplir que S * xVy, x,y* Para eliminar los smbolos inaccesibles, se hace una lista de todos los smbolos de la gramtica y se marca el axioma S. A continuacin, se marcan todos los smbolos que aparezcan en la parte derecha de cualquier regla cuya parte izquierda sea un smbolo marcado. El proceso contina hasta que no se marque ningn smbolo nuevo. Los smbolos que se queden sin marcar, son inaccesibles.

1.12.3. Reglas superfluas


El concepto de regla superflua se explicar con un ejemplo. Sea la gramtica G = ({e,f}, {S,A,B,C,D}, S, {S::=Be, A::=Ae|e, B::=Ce|Af, C::=Cf, D::=f}). La regla C::=Cf es superflua, pues a partir de C no se podr llegar nunca a una cadena que slo contenga smbolos terminales. Para no ser superfluo, un smbolo no terminal U debe cumplir: U + t, tT* El siguiente algoritmo elimina los smbolos superfluos: 1. Marcar los smbolos no terminales para los que exista una regla U::=x, donde x sea una cadena de smbolos terminales, o de no terminales marcados.

24

Compiladores e intrpretes: teora y prctica

2. Si todos los smbolos no terminales han quedado marcados, no existen smbolos superfluos en la gramtica. Fin del proceso. 3. Si la ltima vez que se pas por el paso 1 se marc algn smbolo no terminal, volver al paso 1. 4. Si se llega a este punto, todos los smbolos no terminales no marcados son superfluos.

1.12.4. Eliminacin de smbolos no generativos


Sea la gramtica independiente del contexto G =(T, N, S, P). Para cada smbolo AN se construye la gramtica G(A)=(T, N, A, P). Si L(G(A)) es vaco, se dice que A es un smbolo no generativo. Entonces se puede suprimir A en N, as como todas las reglas que contengan A en P, obteniendo otra gramtica ms sencilla, que representa el mismo lenguaje.

1.12.5. Eliminacin de reglas no generativas


Se llaman reglas no generativas las que tienen la forma A::=. Si el lenguaje representado por una gramtica no contiene la palabra vaca, es posible eliminarlas todas. En caso contrario, se pueden eliminar todas menos una: la regla S::=, donde S es el axioma de la gramtica. Para compensar su eliminacin, por cada smbolo A de N (A distinto de S) tal que A* en G, y por cada regla de la forma B::=xAy, aadiremos una regla de la forma B::=xy, excepto en el caso de que x=y=. Es fcil demostrar que las dos gramticas (la inicial y la corregida) representan el mismo lenguaje.

1.12.6. Eliminacin de reglas de redenominacin


Se llama regla de redenominacin a toda regla de la forma A::=B. Para compensar su eliminacin, basta aadir el siguiente conjunto de reglas: Para cada smbolo A de N tal que A*B en G, y para cada regla de la forma B::=x, donde x no es un smbolo no terminal, aadiremos una regla de la forma A::=x. Es fcil demostrar que las dos gramticas (la inicial y la corregida) representan el mismo lenguaje.

1.12.7. Ejemplo
Sea G=({0,1},{S,A,B,C},S,P), donde P es el siguiente conjunto de producciones: S ::= AB | 0S1 | A | C A ::= 0AB | B ::= B1 |

Captulo 1. Lenguajes, gramticas y procesadores

25

Es evidente que C es un smbolo no generativo, por lo que la regla S::=C es superflua y podemos eliminarla, quedando: S ::= AB | 0S1 | A A ::= 0AB | B ::= B1 | Eliminemos ahora las reglas de la forma X::= : S ::= AB | 0S1 | A | B | A ::= 0AB | 0B | 0A | 0 B ::= B1 | 1 Ahora eliminamos las reglas de redenominacin S::=A|B: S ::= AB | 0S1 | 0AB | 0B | 0A | 0 | B1 | 1 | A ::= 0AB | 0B | 0A | 0 B ::= B1 | 1 Hemos obtenido una gramtica bien formada.

1.12.8. Ejercicio
1. Limpiar la gramtica G = ({i,+}, {Z,E,F,G,P,Q,S,T}, Z, {Z::=E+T, E::=E|S+F|T, F::=F|FP|P, P::=G, G::=G|GG|F, T::=T*i|i, Q::=E|E+F|T|S, S::=i})

1.13 Lenguajes naturales y artificiales


La teora de gramticas transformacionales de Chomsky se aplica por igual a los lenguajes naturales (los que hablamos los seres humanos) y los lenguajes de programacin de computadoras. Con muy pocas excepciones, todos estos lenguajes tienen una sintaxis que se puede expresar con gramticas del tipo 2 de Chomsky; es decir, se trata de lenguajes independientes del contexto. Las dos excepciones conocidas son el alemn suizo y el bambara. El alemn suizo, para expresar una frase parecida a sta: Juan vio a Luis dejar que Mara ayudara a Pedro a hacer que Felipe trabajara admite una construccin con sintaxis parecida a la siguiente: Juan Luis Mara Pedro Felipe vio dejar ayudar hacer trabajar Algunos de los verbos exigen acusativo, otros dativo. Supongamos que los que exigen acusativo estuviesen todos delante, y que despus viniesen los que exigen dativo. Tendramos una construccin sintctica de la forma: An B m C nD m

26

Compiladores e intrpretes: teora y prctica

donde A = frase nominal en acusativo, B = frase nominal en dativo, C = verbo que exige acusativo, D = verbo que exige dativo. Esta construccin hace que la sintaxis del lenguaje no sea independiente del contexto (es fcil demostrarlo mediante tcnicas como el lema de bombeo [5]). El bambara es una lengua africana que, para formar el plural de una palabra o de una frase, simplemente la repite. Por lo tanto, en esta lengua es posible construir frases con sintaxis parecida a las siguientes: Para decir cazador de perros diramos cazador de perro perro. Para decir cazadores de perros diramos cazador de perro perro cazador de perro perro. Y as sucesivamente. Obsrvese que esto hace posible generar frases con una construccin sintctica muy parecida a la que hace que el alemn suizo sea dependiente del contexto: AnBmAnBm donde A sera cazador y B perro.

1.13.1. Lenguajes de programacin de computadoras


A lo largo de la historia de la Informtica, han surgido varias generaciones de lenguajes artificiales, progresivamente ms complejas: Primera generacin: lenguajes de la mquina. Los programas se escriben en cdigo binario. Por ejemplo: 000001011010000000000000 Segunda generacin: lenguajes simblicos. Cada instruccin de la mquina se representa mediante smbolos. Por ejemplo: ADD AX,P1 Tercera generacin: lenguajes de alto nivel. Una sola instruccin de este tipo representa usualmente varias instrucciones de la mquina. Por ejemplo: P1 = P2 + P3; Son lenguajes de alto nivel, FORTRAN, COBOL, LISP, BASIC, C, C++, APL, PASCAL, SMALLTALK, JAVA, ADA, PROLOG, y otros muchos.

1.13.2. Procesadores de lenguaje


Los programas escritos en lenguaje de la mquina son los nicos que se pueden ejecutar directamente en una computadora. Los restantes hay que traducirlos.

Captulo 1. Lenguajes, gramticas y procesadores

27

Los lenguajes simblicos se traducen mediante programas llamados ensambladores, que convierten cada instruccin simblica en la instruccin mquina equivalente. Estos programas suelen ser relativamente sencillos y no se van a considerar aqu. Los programas escritos en lenguajes de alto nivel se traducen mediante programas llamados, en general, traductores o procesadores de lenguaje. Existen tres tipos de estos traductores: Compilador: analiza un programa escrito en un lenguaje de alto nivel (programa fuente) y, si es correcto, genera un cdigo equivalente (programa objeto) escrito en otro lenguaje, que puede ser de primera generacin (de la mquina), de segunda generacin (simblico) o de tercera generacin. El programa objeto puede guardarse y ejecutarse tantas veces como se quiera, sin necesidad de traducirlo de nuevo. Un compilador se representa con el smbolo de la Figura 1.5, donde A es el lenguaje fuente, B es el lenguaje objeto y C es el lenguaje en que est escrito el propio compilador, que al ser un programa que debe ejecutarse en una computadora, tambin habr tenido que ser escrito en algn lenguaje, no necesariamente el mismo que el lenguaje fuente o el lenguaje objeto.

A C

Figura 1.5. Representacin simblica de un compilador.

Entre los lenguajes que usualmente se compilan podemos citar FORTRAN, COBOL, C, C++, PASCAL y ADA. Intrprete: analiza un programa escrito en un lenguaje de alto nivel y, si es correcto, lo ejecuta directamente en el lenguaje de la mquina en que se est ejecutando el intrprete. Cada vez que se desea ejecutar el programa, es preciso interpretarlo de nuevo. Un intrprete se representa con el smbolo de la Figura 1.6, donde A es el lenguaje fuente y C es el lenguaje en que est escrito el propio intrprete, que tambin debe ejecutarse y habr sido escrito en algn lenguaje, usualmente distinto del lenguaje fuente.

A C

Figura 1.6. Representacin simblica de un intrprete.

28

Compiladores e intrpretes: teora y prctica

Entre los lenguajes que usualmente se interpretan citaremos LISP, APL, SMALLTALK, JAVA y PROLOG. De algn lenguaje, como BASIC, existen a la vez compiladores e intrpretes. Compilador-intrprete: traduce el programa fuente a un formato o lenguaje intermedio, que despus se interpreta. Un compilador-intrprete se representa con los smbolos de la Figura 1.7, donde A es el lenguaje fuente, B es el lenguaje intermedio, C es el lenguaje en que est escrito el compilador y D es el lenguaje en que est escrito el intrprete, no necesariamente el mismo que A, B o C.

A C

B B D

Figura 1.7. Representacin simblica de un compilador-intrprete.

JAVA es un ejemplo tpico de lenguaje traducido mediante un compilador-intrprete, pues primero se compila a BYTECODE, y posteriormente ste se interpreta mediante una mquina virtual de JAVA, que no es otra cosa que un intrprete de BYTECODE. En este caso, A es JAVA, B es BYTECODE, C es el lenguaje en que est escrito el compilador de JAVA a BYTECODE, y D es el lenguaje en que est escrita la mquina virtual de JAVA. Los compiladores generan cdigo ms rpido que los intrpretes, pues stos tienen que analizar el cdigo cada vez que lo ejecutan. Sin embargo, los intrpretes proporcionan ciertas ventajas, que en algunos compensan dicha prdida de eficiencia, como la proteccin contra virus, la independencia de la mquina y la posibilidad de ejecutar instrucciones de alto nivel generadas durante la ejecucin del programa. Los compiladores-intrpretes tratan de obtener estas ventajas con una prdida menor de tiempo de ejecucin.

1.13.3. Partes de un procesador de lenguaje


Un compilador se compone de las siguientes partes (vase la Figura 1.8): Tabla de smbolos o identificadores. Analizador morfolgico, tambin llamado analizador lexical, preprocesador o scanner, en ingls. Realiza la primera fase de la compilacin. Convierte el programa que va a ser compilado en una serie de unidades ms complejas (unidades sintcticas) que desempean el papel de smbolos terminales para el analizador sintctico. Esto puede hacerse generando dichas

Captulo 1. Lenguajes, gramticas y procesadores

29

Analizador semntico

Analizador morfolgico Programa fuente

Optimizador de cdigo Programa objeto

Analizador sintctico

Generador de cdigo

Tabla de identificadores

Gestin de memoria

Proceso de errores

Figura 1.8. Estructura de un compilador.

unidades de una en una o lnea a lnea. Elimina espacios en blanco y comentarios, y detecta errores morfolgicos. Usualmente se implementa mediante un autmata finito determinista. Analizador sintctico, tambin llamado parser, en ingls. Es el elemento fundamental del procesador, pues lleva el control del proceso e invoca como subrutinas a los restantes elementos del compilador. Realiza el resto de la reduccin al axioma de la gramtica para comprobar que la instruccin es correcta. Usualmente se implementa mediante un autmata a pila o una construccin equivalente. Analizador semntico. Comprueba la correccin semntica de la instruccin, por ejemplo, la compatibilidad del tipo de las variables en una expresin. Generador de cdigo. Traduce el programa fuente al lenguaje objeto utilizando toda la informacin proporcionada por las restantes partes del compilador. Optimizador de cdigo. Mejora la eficiencia del programa objeto en ocupacin de memoria o en tiempo de ejecucin. Gestin de memoria, tanto en el procesador de lenguaje como en el programa objeto. Recuperacin de errores detectados.

En los compiladores de un solo paso o etapa, suele fundirse el analizador semntico con el generador de cdigo. Otros compiladores pueden ejecutarse en varios pasos. Por ejemplo, en el primero se puede generar un cdigo intermedio en el que ya se han realizado los anlisis morfolgico, sintctico y semntico. El segundo paso es otro programa que parte de ese cdigo intermedio y, a partir de l, genera el cdigo objeto. Todava es posible que un compilador se ejecute en tres pasos, dedicndose el tercero a la optimizacin del cdigo generado por la segunda fase. En un intrprete no existen las fases de generacin y optimizacin de cdigo, que se sustituyen por una fase de ejecucin de cdigo.

1.13.4. Nota sobre sintaxis y semntica


Aunque la distincin entre sintaxis y semntica, aplicada a los lenguajes humanos, es muy antigua, el estudio de los lenguajes de computadora ha hecho pensar que, en el fondo, se trata de una

30

Compiladores e intrpretes: teora y prctica

distincin artificial. Dado que un lenguaje de computadora permite escribir programas capaces de resolver (en principio) cualquier problema computable, es obvio que el lenguaje completo (sintaxis + semntica) se encuentra al nivel de una mquina de Turing o de una gramtica de tipo 0 de Chomsky. Sin embargo, el tratamiento formal de este tipo de gramticas es complicado: su diseo resulta oscuro y su anlisis muy costoso. Estas dificultades desaconsejan su uso en el diseo de compiladores e intrpretes. Para simplificar, se ha optado por separar todas aquellas componentes del lenguaje que se pueden tratar mediante gramticas independientes del contexto (o de tipo 2 de Chomsky), que vienen a coincidir con lo que, en los lenguajes naturales, se vena llamando sintaxis. Su diseo resulta ms natural al ingeniero informtico y existen muchos algoritmos eficientes para su anlisis. La mquina abstracta necesaria para tratar estos lenguajes es el autmata a pila. Por otra parte, podramos llamar semntica del lenguaje de programacin todo aquello que habra que aadir a la parte independiente del contexto del lenguaje (la sintaxis) para hacerla computacionalmente completa. Por ejemplo, con reglas independientes del contexto, no es posible expresar la condicin de que un identificador debe ser declarado antes de su uso, ni comprobar la coincidencia en nmero, tipo y orden entre los parmetros que se pasan en una llamada a una funcin y los de su declaracin. Para describir formalmente la semntica de los lenguajes de programacin, se han propuesto diferentes modelos2, la mayora de los cuales parte de una gramtica independiente del contexto que describe la sintaxis y la extiende con elementos capaces de expresar la semntica. Para la implementacin de estos modelos, los compiladores suelen utilizar un autmata a pila, un diccionario (o tabla de smbolos) y un conjunto de algoritmos. El autmata analiza los aspectos independientes del contexto (la sintaxis), mientras que las restantes componentes resuelven los aspectos dependientes (la semntica).

1.14 Resumen
Este captulo ha revisado la historia de la Informtica, sealando los paralelos sorprendentes que existen entre disciplinas tan aparentemente distintas como la Computabilidad, la Teora de autmatas y mquinas secuenciales, y la Teora de gramticas transformacionales. Se recuerdan y resumen las definiciones de alfabeto, palabra, lenguaje y gramtica; las operaciones con palabras y lenguajes; los conceptos de derivacin, forma sentencial, sentencia, frase y asidero; la idea de recursividad; los diversos tipos de gramticas; la representacin de las derivaciones por medio de rboles sintcticos; el concepto de ambigedad sintctica y la forma de obtener gramticas limpias. Finalmente, la ltima parte del captulo clasifica los lenguajes, tanto naturales como artificiales o de programacin, introduce el concepto de procesador de lenguaje y sus diversos tipos (compiladores, intrpretes y compiladores-intrpretes), y da paso al resto del libro, especificando cules son las partes en que se divide usualmente un compilador o un intrprete.
2 En este libro slo ser objeto de estudio el modelo de especificacin formal de la semntica de los lenguajes de programacin basado en las gramticas de atributos [6, 7].

Captulo 1. Lenguajes, gramticas y procesadores

31

1.15 Bibliografa
[1] Gdel, K. (1931): ber formal unentscheidbare Stze der Principia Mathematica und verwandter Systeme, I. Monatshefte fr Mathematik und Physik, 38, pp. 173-198. [2] Shannon, C. (1938): A symbolic analysis of relay and switching circuits, Transactions American Institute of Electrical Engineers, vol. 57, pp. 713-723. [3] Chomsky, N. (1956): Three models for the description of language, IRE Transactions on Information Theory, 2, pp. 113-124. [4] Chomsky, N. (1959): On certain formal properties of grammars, Information and Control, 1, pp. 91112. [5] Alfonseca, M.; Sancho, J., y Martnez Orga, M. (1997): Teora de lenguajes, gramticas y autmatas. Madrid. Promo-Soft. Publicaciones R.A.E.C. [6] Knuth, D. E. (1971): Semantics of context-free languages, Mathematical Systems Theory, 2(2), pp.127-145, junio 1968. Corregido en Mathematical Systems Theory, 5(1), pp. 95-96, marzo 1971. [7] Knuth, D. E. (1990): The genesis of attribute grammars, en Pierre Deransart & Martin Jourdan, editors, Attribute grammars and their applications (WAGA), vol. 461 de Lecture Notes in Computer Science, pp. 1-12, Springer-Verlag, New York-Heidelberg-Berln, septiembre 1990.

Captulo

Tabla de smbolos

La tabla de smbolos es la componente del compilador que se encarga de todos los aspectos dependientes del contexto relacionados con las restricciones impuestas a los nombres que puedan aparecer en los programas (nombres de variables, constantes, funciones, palabras reservadas). Estas restricciones obligan a llevar la cuenta, durante todo el proceso de la compilacin, de los nombres utilizados (junto con toda la informacin relevante que se deduzca de la definicin del lenguaje de programacin), para poder realizar las comprobaciones e imponer las restricciones necesarias. Por otro lado, una preocupacin muy importante en el diseo de algoritmos para la solucin de problemas computables es obtener el mejor rendimiento posible. Para ello, es esencial la eleccin correcta de las estructuras de datos. El tiempo que necesitan los algoritmos para procesar sus entradas suele depender del tamao de stas y difiere de unas estructuras de datos a otras. Los prrafos siguientes contienen reflexiones que justifican la eleccin de las estructuras de datos y algoritmos ms utilizados para las tablas de smbolos de los compiladores.

Complejidad temporal de los algoritmos 2.1 de bsqueda


En informtica, la complejidad de los algoritmos se puede estudiar estimando la dependencia entre el tiempo que necesitan para procesar su entrada y el tamao de sta. El mejor rendimiento se obtiene si ambos son independientes: el tiempo es constante. En orden decreciente de eficiencia, otros algoritmos presentan dependencia logartmica, polinmica (lineal, cuadrtica, etc.) o exponencial. Para clasificar un algoritmo de esta forma, se elige una de sus instrucciones y se estima el tiempo empleado en ejecutarla mediante el nmero de veces que el algoritmo tiene que ejecutar dicha instruccin, se calcula el tiempo en funcin del tamao de la entrada y se estudia su orden cuando la entrada se hace arbitrariamente grande.

34

Compiladores e intrpretes: teora y prctica

A lo largo de este captulo se usar n para representar el tamao de la entrada. Para la estimacin de los rdenes de eficiencia, los valores concretos de las constantes que aparecen en las funciones no son relevantes, por lo que la dependencia constante se representa como 1, la logartmica como log(n), la lineal como n, la cuadrtica como n2 y la exponencial como en. Resulta til considerar el peor tiempo posible (el tiempo utilizado en tratar la entrada que ms dificultades plantea) y el tiempo medio (calculado sobre todas las entradas o sobre una muestra suficientemente representativa de ellas). Buscar un dato en una estructura implica compararlo con alguno de los datos contenidos en ella. La instruccin seleccionada para medir el rendimiento de los algoritmos de bsqueda suele ser esta comparacin, que recibe el nombre de comparacin de claves. Una explicacin ms detallada de esta materia queda fuera del objetivo de este libro. El lector interesado puede consultar [1, 2].

2.1.1. Bsqueda lineal


La bsqueda lineal supone que los datos entre los que puede estar el buscado se guardan en una lista o vector, no necesariamente ordenados. Este algoritmo (vase la Figura 2.1) recorre la estructura desde la primera posicin, comparando (comparacin de clave) cada uno de los elementos que encuentra con el buscado. Termina por una de las dos situaciones siguientes: o se llega al final de la estructura o se encuentra el dato buscado. En la primera situacin, el dato no se ha encontrado y el algoritmo no termina con xito.

ind BsquedaLineal (tabla T, ind P, ind U, clave k) Para i de P a U: Si T [i] == k: devolver i; devolver error;
Figura 2.1. Pseudocdigo del algoritmo de bsqueda lineal del elemento k en la tabla no necesariamente ordenada T, entre las posiciones P y U.

La situacin ms costosa es la que obliga a recorrer la estructura de datos completa. Esto puede ocurrir si la bsqueda termina sin xito o, en caso contrario, si el elemento buscado es el ltimo de la estructura. En estos casos (tiempo peor) el orden coincide con el tamao de la entrada (n). La dependencia es lineal.

Captulo 2. Tabla de smbolos

35

2.1.2. Bsqueda binaria


La bsqueda binaria supone que los datos, entre los que puede estar el buscado, se guardan en una lista o vector ordenados. Este algoritmo (vase la Figura 2.2) aprovecha el orden propio de la estructura para descartar, en cada iteracin, la mitad de la tabla donde, con seguridad, no se encuentra el dato buscado. En la siguiente iteracin slo queda por estudiar la otra mitad, en la que s puede encontrarse. Para ello se compara el elemento buscado con el que ocupa la posicin central de la estructura (comparacin de clave). Si ste es posterior (anterior) al buscado, puede descartarse la segunda (primera) mitad de la estructura. El algoritmo termina por una de las dos razones siguientes: alguna de las comparaciones encuentra el elemento buscado o la ltima tabla analizada tiene slo un elemento, que no es el buscado. La ltima circunstancia significa que el dato no ha sido encontrado y la bsqueda termina sin xito. ind BusquedaBinaria (tabla T, ind P, ind U, clave K) mientras PU M= (P+U) /2 Si T[M] < K P=M+1; else Si T[M]>K U=M-1; else devolver M; devolver Error;
Figura 2.2. Pseudocdigo del algoritmo de bsqueda binaria del elemento k en la tabla ordenada T entre las posiciones P y U.

Como mucho, este algoritmo realiza las iteraciones necesarias para reducir la bsqueda a una tabla con un solo elemento. Se puede comprobar que el tamao de la tabla pendiente en la iteracin i-sima es igual a n/2i. Al despejar de esta ecuacin el nmero de iteraciones, se obtendr una funcin de log2(n), que es la expresin que determina, tanto el tiempo peor, como el tiempo medio del algoritmo.

2.1.3. Bsqueda con rboles binarios ordenados


Los rboles binarios ordenados se pueden definir de la siguiente manera: Si se llama T al rbol, clave(T) al elemento contenido en su raz, izquierdo(T) y derecho(T) a sus hijos izquierdo y derecho, respectivamente, y nodos(T) al conjunto de sus nodos, T es un rbol binario ordenado si y slo si cumple que: T nodos(T) clave(izquierdo(T)) clave(T) clave(derecho(T))

36

Compiladores e intrpretes: teora y prctica

El algoritmo de bsqueda (vase la Figura 2.3) consulta la raz del rbol para decidir si la bsqueda ha terminado con xito (en el caso en el que el elemento buscado coincida con la clave del rbol) o, en caso contrario, en qu subrbol debe seguir buscando: si la clave del rbol es posterior (anterior) al elemento buscado, la bsqueda contina por el rbol izquierdo(T) (derecho(T)). Si en cualquier momento se encuentra un subrbol vaco, la bsqueda termina sin xito. Se suelen utilizar distintas variantes de este algoritmo para que el valor devuelto resulte de la mxima utilidad: en ocasiones basta con el dato buscado, o con una indicacin de que se ha terminado sin xito; en otras, el retorno de la funcin apunta al subrbol donde est el elemento buscado o donde debera estar. La Figura 2.3 resalta la comparacin de claves. En el peor de los casos (que la bsqueda termine con fracaso, tras haber recorrido los subrboles ms profundos, o que el elemento buscado est precisamente en el nivel ms profundo del rbol), el nmero de comparaciones de clave coincidir con la profundidad del rbol. Es decir, los tiempos peor y medio dependen de la profundidad del rbol. Se escribir prof(T) para representar la profundidad del rbol T. ArbolBin Buscar(clave K, ArbolBin T) Si vaco(T) devolver rbol_vaco; Si k == clave(T) devolver T Si k < clave(T) devolver(Buscar(k,izquierdo(T)); Si k > clave(T) devolver(Buscar(k,derecho(T));
Figura 2.3. Pseudocdigo recursivo del algoritmo de bsqueda del elemento k en el rbol binario ordenado T.

Es interesante observar que este razonamiento no expresa una dependencia directa de n, sino de un parmetro del rbol binario que depende tanto de n como de la manera en la que se cre el rbol binario en el que se busca. La Figura 2.4 muestra dos posibles rboles binarios ordenados y correctos formados con el conjunto de datos {0,1,2,3,4}

1 0 2 3 4 0 1

2 3 4 b)

a)

Figura 2.4. Dos rboles binarios distintos para el conjunto de datos {0,1,2,3,4}: a) con profundidad 3, b) con profundidad 2.

Captulo 2. Tabla de smbolos

37

Posteriormente se profundizar ms en esta reflexin, para comprender cmo depende prof(T) de n.

2.1.4. Bsqueda con rboles AVL


Los rboles de Adelson-Velskii y Landis (AVL) son un subconjunto de los rboles binarios ordenados. Un rbol binario ordenado es un rbol AVL si y slo si las profundidades de los hijos de cualquier nodo no difieren en ms de una unidad. Por tanto, aunque pueda haber muchos rboles AVL para el mismo conjunto de datos, se tiene la garanta de que la profundidad es siempre ms o menos la del rbol binario menos profundo posible. El rbol binario menos profundo que se puede formar con n nodos es el que tiene todos sus niveles completos, es decir, cada nodo que no sea una hoja tiene exactamente 2 hijos. Es fcil comprobar que el nmero de nodos que hay en el nivel i-simo de un rbol con estas caractersticas es igual a 2i, y tambin que el total de nodos en un rbol de este tipo, de profundidad k, es igual a 2k+1-1. Si se despeja la profundidad k necesaria para que el nmero de nodos sea igual a n, quedar en funcin de log2(n). Se ha dicho que la profundidad es ms o menos la del rbol binario menos profundo posible, porque se permite una diferencia en la profundidad de como mucho una unidad, que se puede despreciar para valores grandes de n. Por tanto, los rboles AVL garantizan que la profundidad es del orden de log(n), mientras que en la seccin anterior se vio que el tiempo peor y medio de la bsqueda en un rbol binario T es del orden de prof(T).

2.1.5. Resumen de rendimientos


El estudio del mejor tiempo posible para cualquier algoritmo de bsqueda que se base en la comparacin de claves se parece al razonamiento informal de las secciones anteriores respecto a los rboles binarios. Intuitivamente, se puede imaginar que el mejor tiempo posible estar asociado a la profundidad del rbol binario menos profundo que se puede formar con n nodos: log2(n). Se puede concluir, por tanto, que ste es el rendimiento mejor posible para los algoritmos de ordenacin basados en comparaciones de clave. La Tabla 2.1 muestra un resumen de los rendimientos observados.

Tabla 2.1. Resumen de los rendimientos de los algoritmos de bsqueda con comparacin de clave. Algoritmo Bsqueda lineal Bsqueda binaria Cota inferior Orden del tiempo peor n log(n) log(n) Orden del tiempo medio log(n) log(n)

38

Compiladores e intrpretes: teora y prctica

2.2 El tipo de datos diccionario


2.2.1. Estructura de datos y operaciones
La teora de estructuras de datos define el diccionario como una coleccin ordenada de informacin organizada de forma que una parte de ella se considera su clave. La clave se utiliza para localizar la informacin en el diccionario, de la misma manera en que, en un diccionario de la lengua, las palabras se utilizan como clave para encontrar su significado. En un diccionario se dispondr, al menos, de las operaciones de bsqueda, insercin y borrado: Posicion Buscar(clave k, diccionario D): Busca el dato k en el diccionario D. La funcin devuelve la posicin ocupada por el dato (en caso de acabar con xito) o una indicacin de que la bsqueda ha terminado sin encontrarlo. Estado Insertar(clave k, diccionario D): Aade al diccionario D la informacin k. El retorno de la funcin informa sobre el xito de la insercin. Estado Borrar(clave k, diccionario D): Elimina del diccionario D la informacin k. Aunque la implementacin de estas funciones admita variaciones, se puede considerar general el pseudocdigo de las Figuras 2.5 y 2.6. Puede observarse en ellas que el trabajo ms importante de la insercin y el borrado es la bsqueda inicial de la clave tratada. Por tanto, la complejidad temporal de las tres operaciones

Estado Insertar (clave k, diccionario D) Posicion = Buscar(k,D); Si Posicion indica que no est Modificar D para que incluya k devolver insercin correcta en otro caso devolver error
Figura 2.5. Pseudocdigo del algoritmo de insercin de la clave k en el diccionario D.

Captulo 2. Tabla de smbolos

39

Estado Borrar (clave k, diccionario D) Posicion = Buscar(k,D); Si Posicion indica que no est devolver error en otro caso Modificar D para eliminar k devolver borrado correcto
Figura 2.6. Pseudocdigo del algoritmo de borrado de la clave k del diccionario D.

queda determinada por la de la bsqueda. En las prximas secciones se elegir razonadamente una implementacin adecuada, en cuanto a rendimiento temporal, de esta estructura de datos.

2.2.2. Implementacin con vectores ordenados


Cuando las claves del diccionario se almacenan en un vector ordenado, la operacin de bsqueda puede realizarse con el algoritmo de bsqueda binaria descrito en secciones anteriores. Tanto su tiempo medio como su tiempo peor suponen un rendimiento aceptable (vase la Tabla 2.1). Se estudiar a continuacin si el trabajo extra aadido a la bsqueda en el resto de las operaciones empeora su rendimiento. En el caso de la insercin, para que el vector siga ordenado despus de realizarla, es necesario, tras localizar la posicin que la nueva clave debera ocupar en el vector, desplazar los elementos siguientes para dejar una posicin libre (vase la Figura 2.7) En el peor de los casos (si la nueva clave debe ocupar la primera posicin del vector) sera necesario mover todos los elementos, con un rendimiento temporal de orden lineal (n). La colocacin de la informacin en su ubicacin final se realiza en tiempo constante.

a)

b)

c)

Figura 2.7. Insercin de la clave k en el diccionario D (vector ordenado): a) se busca la clave k en D; b) tras comprobar que no est se hace hueco para k; c) k ocupa su posicin en D.

40

Compiladores e intrpretes: teora y prctica

El rendimiento de la insercin es la suma del de la bsqueda (log(n)), ms el del desplazamiento (n) y el de la asignacin (1) y, por lo tanto, est determinado por el peor de ellos: n. El rendimiento lineal no es aceptable, por lo que no es necesario estudiar el borrado para rechazar el uso de vectores ordenados en la implementacin del diccionario.

2.2.3. Implementacin con rboles binarios ordenados


En la Seccin 2.1.3 se ha mostrado que el rendimiento temporal de la bsqueda depende de la profundidad del rbol y que existen diferentes rboles binarios ordenados para el mismo conjunto de datos con distintas profundidades. Si no se puede elegir a priori el rbol en el que se va a buscar, lo que suele ocurrir casi siempre, ya que el uso de un diccionario suele comenzar cuando est vaco, y se va rellenando a medida que se utiliza, tampoco se puede asegurar que no se termine utilizando el peor rbol binario posible que muestra la Figura 2.8, que, como se ve, no se puede distinguir de una simple lista.

0 1 2 3 4

Figura 2.8. Uno de los peores rboles binarios posibles respecto al rendimiento temporal de la bsqueda con los datos {0,1,2,3,4}.

En este caso (vase la Seccin 2.1.1) el rendimiento temporal de la bsqueda depende linealmente del tamao de la entrada (es de orden n). Este rendimiento no es aceptable, lo que basta para rechazar los rboles binarios ordenados para implementar el diccionario. A pesar de esto, se realizar el estudio de las operaciones de insercin y borrado, para facilitar la comprensin de la siguiente seccin.

Insercin
La insercin de la clave k en el rbol binario ordenado T comienza con su bsqueda. Si suponemos que el algoritmo de bsqueda devuelve un puntero al nodo padre donde debera insertarse el nuevo elemento, lo nico que quedara por hacer es crear un nodo nuevo y asignarlo como hijo izquierdo (derecho) al retorno de la bsqueda, si k es anterior (posterior) a la clave del rbol. La Figura 2.9 muestra grficamente esta situacin y la Figura 2.10 el pseudocdigo correspondiente.

Captulo 2. Tabla de smbolos

41


a)


b)

Figura 2.9. Representacin grfica de la insercin en rboles binarios ordenados: a) la flecha discontinua y el nodo claro indican la posicin donde debera insertarse la nueva clave; b) resultado de la insercin; se resaltan las modificaciones en el rbol de partida.

Puede observarse que el trabajo aadido a la bsqueda consiste en una comparacin de clave y una modificacin del valor de una variable. Este trabajo es el mismo para cualquier tamao de la entrada (n), por lo que supondr un incremento de tiempo constante que se podr despreciar para valores grandes de n, por lo que el rendimiento de la insercin es el mismo que el de la bsqueda. estado Insertar(clave k, ArbolBin T) ArbolBin arbol_auxiliar T, T; T=Buscar(k,T); T=nuevo_nodo(k); Si no es posible crear el nodo devolver error Si k < clave(T) izquierdo(T)=T; else derecho(T)=T; devolver ok
Figura 2.10. Pseudocdigo del algoritmo de insercin de la clave k en el rbol binario ordenado T.

Borrado
El borrado de la clave k del rbol binario ordenado T presenta la dificultad de asegurar que el rbol sigue ordenado tras eliminar el nodo que contiene a k. La disposicin de los nodos en estos rboles permite, sin embargo, simplificar el proceso gracias a los dos resultados siguientes: 1. (Vase la Figura 2.11) Para cualquier rbol binario ordenado T y cualquier nodo b del mismo, se pueden demostrar las siguientes afirmaciones relacionadas con el nodo b, que contiene el antecesor inmediato del nodo b en el rbol:

42

Compiladores e intrpretes: teora y prctica

8 4 a).1) 2 1 3 a).2) 12 11 13 5 6 7 9 b).1) 10 14 b).2) 16 17 18 15

Figura 2.11. Dos ejemplos de localizacin del nodo con el antecesor inmediato de otro dado en un rbol binario ordenado. a) El antecesor inmediato de 4 es 3: 1) el nodo raz del subrbol de los elementos menores que 4 contiene el 2; 2) al descender siguiendo los hijos derechos a partir del nodo que contiene al 2, se termina en el nodo que contiene al 3. b) El antecesor inmediato de 15 es 14: 1) los elementos menores que 15 estn en el subrbol de raz 10; 2) al descender por los hijos derechos se termina en el nodo que contiene el 14. Obsrvese que en este caso existe hijo izquierdo (el subrbol que comienza en 12), pero no hijo derecho.

Por definicin de rbol binario ordenado, b tendr que estar en el subrbol izquierdo(b) y debe ser el nodo que est ms a la derecha en dicho subrbol. Por lo tanto, se puede localizar b realizando los siguientes pasos: 1. 2. 3. Se llamar ib a izquierdo(b) en T. A partir de derecho(ib), y mientras exista el subrbol hijo derecho, se avanza hacia los niveles ms profundos del rbol por el subrbol hijo derecho del anterior. b es la raz del rbol encontrado mediante los pasos 1 y 2.

Puede observarse que b necesariamente debe carecer de hijo derecho, pues en otro caso no se habra terminado an el paso 2. 2. (Vase la Figura 2.12) Se puede demostrar que, para cualquier rbol binario ordenado T y cualquier nodo b del mismo, el rbol T construido mediante el siguiente proceso corresponde al resultado de eliminar el nodo b de T: Inicialmente T es una copia de T. Sea b el nodo que contiene el antecesor inmediato al nodo b en T. En T se sustituye el contenido del nodo b por el de su antecesor inmediato en el rbol (b).

Captulo 2. Tabla de smbolos

43

8 4 2 1 3 5 6 7 9 10 14 16 15 17 18 a)

12 11 13

8 4 2 1 3 5 6 7 9 10 14 16 14 17 18 b)

12 11 13

8 4 2 1 3 5 6 7 9 10 12 11 16 14 17 18 c)

13

Figura 2.12. Borrado del nodo 15: a) localizacin y sustitucin de 15 por su antecesor inmediato en T, b) sustitucin del antecesor por su hijo izquierdo, c) rbol final.

44

Compiladores e intrpretes: teora y prctica

En T se sustituye el subrbol cuya raz es b por el subrbol izquierdo(b), si ste existe. Si no existe, se elimina el nodo b. Por lo tanto, el borrado conlleva dos bsquedas (la del elemento que se va a borrar y la de su antecesor inmediato en el rbol) y el cambio de valor de dos variables del rbol (el contenido del nodo del elemento borrado y el subrbol donde estaba el antecesor inmediato del elemento borrado). El rendimiento temporal del proceso completo sigue siendo del orden de la profundidad del rbol (prof(T)). Realmente se necesitar el doble de la profundidad del rbol (por las dos bsquedas) ms un tiempo constante, que no depende del tamao de la entrada (por las dos asignaciones).

2.2.4. Implementacin con AVL


Cuando las claves del diccionario se almacenan en un rbol AVL, el tiempo medio y el tiempo peor de la bsqueda suponen un rendimiento aceptable (vase la Tabla 2.1). Se estudiar a continuacin si el trabajo extra aadido a la bsqueda en el resto de las operaciones empeora su rendimiento.

Insercin
Para asegurarse de que los subrboles de un rbol AVL estn balanceados, es preciso realizar algunas operaciones adicionales en la insercin, que suelen llamarse rotaciones, respecto al algoritmo descrito para rboles binarios ordenados. En concreto, cada vez que se inserta una clave, se necesitar una o dos rotaciones (con una o dos modificaciones de valor) para asegurarse de que las profundidades de los subrboles de todo el rbol siguen difiriendo, a lo ms, en una unidad. Por tanto, la insercin en rboles AVL aade un trabajo que no depende del tamao de la entrada y que requerir un tiempo de orden constante (1), que puede despreciarse para entradas grandes (valores grandes de n) frente a prof(T), que para rboles AVL es del orden de log(n), por lo que el orden de la complejidad temporal de la insercin sigue siendo log(n).

Borrado
Se puede comprobar que el borrado de una clave en rboles binarios ordenados modifica, como mucho, en una unidad la profundidad de uno de los subrboles. Por lo tanto, se puede repetir la reflexin del apartado anterior para afirmar que el borrado en rboles AVL slo requiere un trabajo adicional constante (de orden 1), respecto al borrado en rboles binarios ordenados, que se puede despreciar para entradas grandes frente a prof(T), por lo que log(n) es tambin el orden de complejidad del borrado. Por lo tanto, los rboles AVL sera una buena opcin para implementar el diccionario, si la tcnica ms eficiente fuese la comparacin de claves.

Implementacin del tipo de dato diccionario 2.3 con tablas hash


En esta seccin se intentar responder a la pregunta de si existe alguna tcnica ms eficiente que la comparacin de claves. Para ello se analizarn alternativas mejores que la dependencia logartmica entre el tiempo de ejecucin de la bsqueda y el tamao de la entrada.

Captulo 2. Tabla de smbolos

45

2.3.1. Conclusiones sobre rendimiento


En las secciones anteriores se ha reflexionado sobre la implementacin de las tablas de smbolos mediante algoritmos basados en comparaciones de clave. Se ha llegado a la conclusin (vase la Tabla 2.1) de que el rendimiento temporal de esta tcnica est acotado inferiormente por la dependencia logartmica del tamao de la entrada, del orden de log(n). Aunque desde el punto de vista de la complejidad de algoritmos este rendimiento es aceptable, para el problema tratado en este libro, supone que el tiempo necesario para compilar un programa depende logartmicamente del nmero de identificadores que contenga. Resulta claro que el diseador del compilador no puede predecir el valor de este nmero. La teora de la complejidad de algoritmos suele considerar que, si se quiere mejorar el rendimiento logartmico log(n), se necesita conseguir un rendimiento constante, que no dependa del tamao de la entrada (del orden de 1). La conclusin de todas estas reflexiones es que resulta imposible obtener un tiempo de compilacin independiente del nmero de identificadores que contengan los programas, mediante estructuras de datos y algoritmos que slo utilicen comparaciones entre ellos para insertarlos, buscarlos o borrarlos de la tabla de smbolos. Para conseguir ese rendimiento, es necesario recurrir a estructuras de datos ms complejas. En los prximos apartados se analizar el uso de funciones y tablas hash o de dispersin para lograr ese rendimiento.

2.3.2. Conceptos relacionados con tablas hash


Intuitivamente, una tabla hash es un tipo de datos que consta de un vector (para almacenar los datos) y una funcin (hash o de dispersin, que es su significado en ingls), que garantiza (idealmente) que a cada dato se le asocie una posicin nica en el vector. La tabla hash se usa de la siguiente manera: Se elige una parte de los datos para considerarla clave de los mismos (por ejemplo, si se est almacenando informacin sobre personas, la clave podra ser su DNI). Se disea una funcin hash que asocie (idealmente) a cada valor de la clave una posicin nica en el vector. Cuando se desea localizar un dato en el vector (ya sea para aadirlo a la tabla, para recuperarlo o para eliminarlo) se aplica a su clave la funcin hash, que proporciona el ndice en el vector que corresponde al dato. La mejora consiste en que el tiempo necesario para buscar un elemento ya no depende del nmero de elementos contenidos en la tabla. El rendimiento temporal ser la suma del clculo de la funcin hash y del tiempo necesario para realizar la asignacin, que no depende del tamao de la tabla. Por lo tanto, es esencial que el diseo de la funcin hash tampoco dependa del tamao de la tabla. En ese caso, el rendimiento de la bsqueda de una clave en una tabla hash ser constante (del orden de 1) y no depender del tamao de la entrada. Formalmente, la funcin hash toma valores en el conjunto de claves y recorre el conjunto de ndices o posiciones en el vector,

46

Compiladores e intrpretes: teora y prctica

por lo que slo depende de la clave, lo que garantizara la independencia entre su rendimiento y el tamao de la tabla. El nombre de la estructura (hash o dispersin) hace referencia a otro aspecto importante que ser analizado con detalle en las prximas secciones: la funcin debera dispersar las claves adecuadamente por el vector; es decir, en teora, a cada clave se le debera hacer corresponder de forma biunvoca una posicin del vector. sta es una situacin ideal prcticamente inalcanzable. En realidad, es inevitable que las funciones hash asignen la misma posicin en el vector a ms de una clave. Las diferentes tcnicas para solventar esta circunstancia originan distintos tipos de tablas de dispersin con diferentes rendimientos, que sern objeto de las prximas secciones. Las tablas hash tambin se llaman tablas de entrada calculada, ya que la funcin hash se usa para calcular la posicin de cada entrada en la tabla.

2.3.3. Funciones hash


Se usar la siguiente notacin para las funciones hash: sea K el conjunto de claves, sean 11 y m respectivamente las posiciones mnima y mxima de la tabla, y sea N el conjunto de los nmeros naturales. Cualquier funcin hash h se define de la siguiente forma: h:K [1,m]

Funciones hash inyectivas


Lo ideal es que h fuese al menos inyectiva, ya que de esta forma se garantiza que a dos claves distintas les corresponden siempre posiciones distintas del vector. Formalmente k,kK; kkh(k)h(k) Lo que se pierde al no obligar a h a ser biyectiva es que no se garantiza que se ocupen todas las posiciones del vector. En la prctica, es muy difcil disear funciones hash inyectivas, por lo que hay que conformarse con funciones no inyectivas razonablemente buenas. La no inyectividad causa un problema importante: las colisiones. Se llama colisin a la situacin en la que h asigna la misma posicin en el vector a dos o ms claves distintas. Formalmente: k,kK| kkh(k)=h(k). Cmo implementar tablas hash a pesar de las colisiones? Ya que se permiten, al menos, se intentar minimizar su aparicin. Informalmente, se pretende que sea pequea la probabilidad de que se produzca una colisin y, por tanto, grande la de que no se produzca. Formalmente, en una situacin ideal, sera deseable que la probabilidad de colisin fuese igual a 1/m, y la de que no haya colisin, (m-1)/m. La Figura 2.13 muestra grficamente esta circunstancia al insertar la segunda clave k en el supuesto de haber insertado ya la clave k (con kk).
1 El valor mnimo para las posiciones en la tabla puede ser 0 o 1, como el origen de los ndices en los distintos lenguajes de programacin. En este captulo se usar indistintamente, y segn convenga, un valor u otro.

Captulo 2. Tabla de smbolos

47

k 1 h (k) m

a)

k 1 h (k) b)

k h (k) m 1

k h (k) c) m

Figura 2.13. Justificacin intuitiva de la probabilidad de colisin. a) Situacin inicial, tras insertar la clave k. b) Al insertar k no se produce colisin, h(k) h(k), el nmero de casos favorables es m-1, ya que slo la posicin h(k) es un caso desfavorable; el nmero de casos posibles es m. c) Se produce colisin; el nmero de casos favorables es 1 y el nmero de casos posibles es m.

Funciones hash pseudoaleatorias


El siguiente mecanismo para la insercin de la clave k, que recibe el nombre de aleatorio o pseudoaleatorio, permitira alcanzar este objetivo: 1. Se tira un dado con m caras. 2. Se anota el valor de la cara superior (i). 3. Se toma i como el valor hash para la clave k : h(k)=i. 3. Se accede a la posicin i de la tabla y se le asigna la informacin de la clave k. Resulta claro que este esquema no es vlido para implementar las tablas hash, ya que el mecanismo de recuperacin de la informacin asociada a la clave k tendra los siguientes pasos: 1. Se tira un dado con m caras. 2. Se anota el valor de la cara superior (j) 3. Se define j como el valor hash para la clave k: h(k)=j. 4. Se accede a la posicin j de la tabla y se recupera la informacin almacenada en ella. El mtodo anterior slo funcionar si los resultados del experimento aleatorio se repiten siempre que se aplique a la misma clave. Pero eso entra en contradiccin con la definicin del experimento aleatorio. En particular, las funciones hash pseudoaleatorias no garantizan en modo alguno que, una vez que se ha insertado la informacin de una clave, se pueda recuperar. En las prximas secciones se analizar la prdida de eficiencia asociada a las colisiones. El objetivo ser comprobar que su gestin, aunque implique la prdida del rendimiento temporal constante, no empeora la dependencia logartmica. La utilidad de las funciones hash aleatorias consiste en su uso en el estudio terico de los rendimientos. La gestin de las colisiones dificulta el anlisis con funciones hash que no sean pseudoaleatorias.

48

Compiladores e intrpretes: teora y prctica

Funciones hash uniformes


Se pedir a las funciones hash que sean relativamente uniformes, es decir, que distribuyan las claves de manera uniforme por la tabla, sin que queden muchas posiciones libres cuando comiencen a aparecer colisiones. En este contexto, resulta esencial encontrar un mecanismo que genere elementos de un subconjunto de los nmeros naturales [1,m] sin repeticiones. El lgebra y la teora de nmeros definen la operacin mdulo para el clculo del resto de la divisin entre dos nmeros enteros, los grupos cclicos, su estructura y las condiciones para su existencia. Estos resultados, que quedan fuera del mbito de este libro, pueden utilizarse para definir funciones hash relativamente uniformes. A continuacin se muestran, sin justificar, algunos ejemplos.

Funciones hash de multiplicacin


Se utiliza una funcin auxiliar uniforme, con imagen en el intervalo real [0,1]. Su producto por el tamao de la tabla (m) nos lleva a una funcin real uniforme con imagen en el intervalo [0,m]. Formalmente h(k)=m(k), donde k es un valor numrico asociado con la clave. Si la clave no es numrica, se supondr la existencia de una funcin que calcule un valor numrico a partir de la clave. Por simplificar la notacin, se omitir esta funcin. En prximas secciones se describirn con ms detalle algunas tcnicas para obtener valores numricos a partir de claves no numricas. m es la posicin mxima dentro de la tabla, que debe cumplir la siguiente condicin: p Z| p es primo m=2p R-Q es un nmero irracional. Es frecuente utilizar el valor

51 = 2
x es la funcin suelo, que calcula el entero ms prximo por debajo de su argumento. (x) es la funcin parte fraccionaria, definida as: (x) = xx La Tabla 2.2 muestra un ejemplo de otra funcin hash de multiplicacin.
Tabla 2.2. Algunos valores de la funcin hash de multiplicacin que utiliza = y m=25. Se resaltan las colisiones. k 1 2 3 4 5 6 kx 3.141592654 6.283185307 9.424777961 12.56637061 15.70796327 18.84955592 h(k) 3 7 10 14 17 21 k 7 8 9 10 1 12 kx 21.99114858 25.13274123 28.27433388 31.41592654 34.55751919 37.69911184 h(k) 24 3 6 10 13 17

Captulo 2. Tabla de smbolos

49

Funciones hash de divisin


Se utiliza la funcin mdulo h(k)=k%m, donde k es, como en el caso anterior, un valor numrico asociado a la clave. m es el valor mximo de posicin dentro de la tabla. Se le exige que sea primo. % es la funcin mdulo, definida como el resto de la divisin de x entre m. La Tabla 2.3 muestra ejemplos de esta funcin hash.

Tabla 2.3. Algunos valores de la funcin hash de divisin, para m=7. Se resaltan las colisiones. k 1 2 3 4 5 6 h (k) 1 2 3 4 5 6 k 7 8 9 10 11 12 h (k) 0 1 2 3 4 5

Otras funciones hash


A pesar de los argumentos tericos anteriores, el diseo de funciones hash tiene mucho de trabajo artesanal y es una tarea complicada. Por eso, a continuacin, se describe una funcin hash bien documentada en la literatura, que en la prctica ha mostrado ser buena. Puede encontrarse una exposicin detallada en [3]. Dicha funcin hash es un algoritmo iterativo que calcula un valor auxiliar (hi) entre 0 y la longitud m de la clave id. El valor final h(id) se obtiene a partir de alguno de los bits del valor m-simo (hm). =0 h h =k h
0 i

i 1 i m h(k)=bits(hm,30)%n
i-1+ci

donde k es una constante deducida experimentalmente. n es el tamao de la tabla, deducido con k experimentalmente. bits(x,j) es una funcin que obtiene los j bits menos significativos del entero x. ci es el cdigo ASCII del carcter i-simo de id

50

Compiladores e intrpretes: teora y prctica

2.3.4. Factor de carga


Un concepto muy importante en el estudio de la eficiencia es el factor de carga. Dada una tabla hash con espacio para m claves, en la que ya se han insertado n, se llama factor de carga y se representa mediante la letra , al cociente entre n y m. n = m

2.3.5. Solucin de las colisiones


Puesto que se van a usar funciones hash que permiten colisiones, es necesario articular mecanismos para reaccionar frente a stas. Aunque son muchas las alternativas posibles, en las siguientes secciones se explicarn algunas de ellas con detalle.

2.3.6. Hash con direccionamiento abierto


Esta tcnica recibe su nombre del hecho de que la posicin final que se asigna a una clave no est totalmente determinada por la funcin hash. Lo ms caracterstico de este mtodo es que las colisiones se solucionan dentro del mismo espacio utilizado por la tabla, es decir, no se usa ninguna estructura de datos auxiliar para ello. De aqu se deduce la necesidad de que haya siempre posiciones libres en la tabla, es decir, que el tamao reservado para ella sea siempre mayor que el nmero de claves que se va a insertar. La insercin de una clave k mediante direccionamiento abierto funciona de la siguiente manera (la Figura 2.14 muestra el pseudocdigo para la insercin, comn a todas las variantes de encadenamiento abierto): 1. Se estima el nmero de claves que se va insertar en la tabla. 2. Se dimensiona la tabla para que siempre haya posiciones libres. El tamao necesario depende de otros aspectos de la tcnica que se explicarn a continuacin. 3. Cuando se va a insertar la clave, se calcula la posicin que le asignara la funcin hash h(k). Si dicha posicin est libre, no hay colisin y la informacin se guarda en esa posicin. En otro caso hay colisin: se recorre la tabla buscando la primera posicin libre (j). La informacin se guarda en dicha posicin j-sima. La manera de encontrar la primera posicin libre se llama sondeo o rehash. Como se ver a continuacin, el sondeo no implica necesariamente que las claves que colisionan en la misma posicin de la tabla ocupen finalmente posiciones contiguas. Por esta causa, tambin se conoce al direccionamiento abierto como espaciado. El objetivo del sondeo es ocupar la mayor parte de la tabla con el mejor rendimiento posible. La recuperacin de informacin de la tabla tiene que tener en cuenta que ya no se garantiza que la informacin de la clave k est en la posicin h(k). Hay que utilizar el mismo mecanismo de sondeo empleado en la insercin para recorrer la tabla, hasta encontrar la clave buscada. La Figura 2.15 muestra el pseudocdigo de la bsqueda, comn a todas las variantes del encadenamiento abierto.

Captulo 2. Tabla de smbolos

51

indice Insertar(clave k, TablaHash T) indice posicion=funcion_hash(k,T); int i=0; /*Numero de reintentos*/ Si k == T.datos[posicion].clave devolver posicion; /*Ya estaba*/ else{ Mientras no vacia(T.datos[posicion]) y no posicion == funcion_hash(k,T) y no k == T.datos[posicion].clave {posicion = (posicion + delta(i++))mod tamao(T);} if vacia(T.datos[posicion]) {/*No estaba y se inserta*/ T.datos[posicion].clave = k; devolver posicin;} if posicion == funcion_hash(k,T) devolver -1; /* T no tiene espacio para ese valor de hash */ if k == T.datos[posicion].clave devolver posicion; /*Ya estaba*/ }
Figura 2.14. Pseudocdigo del algoritmo de insercin de la clave k en la tabla hash T, comn a todas las tcnicas con direccionamiento abierto. Se resalta el sondeo.

indice Buscar(clave k, TablaHash T) indice posicion=funcion_hash(k,T); Si k == T.datos[posicion].clave devolver posicion; else { Mientras no vacia(T.datos[posicion]) y no posicion == funcion_hash(k,T) y no k == T.datos[posicion].clave {posicion = (posicion + delta(i++))mod tamao(T);} if vacia(T.datos[posicion]) devolver -1; /*No est*/ if posicion == funcion_hash(k,T) devolver -1; /* Adems esto significa que la tabla no tiene espacio disponible para ese valor de hash */ if k == T.datos[posicion].clave devolver posicion; /*Est*/ }
Figura 2.15. Pseudocdigo del algoritmo de bsqueda de la clave k en la tabla hash T, comn a todas las tcnicas con direccionamiento abierto. Se resalta el sondeo.

52

Compiladores e intrpretes: teora y prctica

Los algoritmos de las Figuras 2.14 y 2.15 muestran el sondeo como un desplazamiento representado por la funcin delta, que se suma a la posicin devuelta por la funcin hash, en un bucle que recorre la tabla buscando la clave, cuando es necesario. En los prximos prrafos se analizarn diferentes tipos de sondeo, es decir, distintas implementaciones de la funcin delta. Obsrvese que de la Figura 2.14 pueden deducirse distintas condiciones para concluir que no hay sitio en la tabla: Cuando la tabla est totalmente llena. Ya se ha advertido de la necesidad de que la tabla sea lo suficientemente grande para que esta situacin no se produzca nunca. Cuando, durante la repeticin del sondeo, independientemente de que haya posiciones libres en la tabla, se llega a una posicin previamente visitada. En este caso, aunque la tabla tenga sitio, no se va a poder llegar a l. La segunda condicin es muy importante para el diseo del sondeo. Hasta ahora se poda pensar que la gestin correcta de todas las claves se garantizaba con una tabla suficientemente grande. Sin embargo, un sondeo deficiente, aunque se realice en una tabla muy grande, puede dar lugar a un rendimiento similar al conseguido con una tabla demasiado pequea. Un ejemplo trivial de sondeo deficiente es el que, tras una colisin, slo visita una nica posicin ms, que es siempre la primera de la tabla. Como se ver a continuacin, esta segunda condicin es la que ms determina el diseo de los sondeos y el rendimiento del direccionamiento abierto.

Sondeo lineal
El sondeo lineal busca sitio en las posiciones siguientes, en la misma secuencia en que estn en la tabla. Si se supone que posicin=h(k) y que no est libre, el sondeo lineal mirar en la secuencia de posiciones {posicin+1, posicin+2, ...} = {posicin+i}1im-posicin. Por lo tanto, todas las claves que colisionen estarn agrupadas en posiciones contiguas a la que les asigna la funcin hash. La Figura 2.16 muestra grficamente esta circunstancia. Hay diferentes mtodos para estimar el rendimiento del direccionamiento abierto con sondeo lineal. En la literatura se pueden encontrar justificaciones, tanto analticas como basadas en simulaciones [1, 2]. Todas las justificaciones coinciden en que la dependencia del rendimiento

Figura 2.16. Posible estado de una tabla hash con direccionamiento abierto y sondeo lineal. Hay tres grupos de claves que colisionan, con tres, diez y nueve claves, respectivamente. La primera posicin de cada grupo (por la izquierda) es la asignada por la funcin hash. Obsrvese que el ltimo grupo contina en las primeras posiciones de la tabla.

Captulo 2. Tabla de smbolos

53

temporal respecto al factor de carga, cuando se buscan claves que no estn en la tabla hash, se puede aproximar mediante la siguiente expresin: 1 1 1 + (1 )2 2

Tambin coinciden en que, cuando se buscan claves que s estn en la tabla, el rendimiento se puede aproximar mediante la expresin 1 1 1 + 1 2

En la prctica es poco conveniente que las claves que colisionan formen bandas contiguas.

Sondeo multiplicativo
El sondeo multiplicativo intenta superar el inconveniente que suponen las bandas de claves que colisionan en la tabla hash. Para ello se articula un mecanismo poco costoso para dispersar los reintentos por la tabla, impidiendo la formacin de bandas al espaciarlos uniformemente. Intuitivamente, se usa como incremento el valor devuelvo por la funcin hash, de forma que, en el primer reintento, se saltan h(k) posiciones; en el segundo, 2*h(k) posiciones, etc. La Figura 2.17 muestra el pseudocdigo del sondeo multiplicativo. int delta(int numero_reintento, indice posicion_inicial) { return (posicion_inicial*numero_reintento); }
Figura 2.17. Pseudocdigo del algoritmo sondeo multiplicativo. Se necesitan dos argumentos, el nmero de reintentos y la posicin inicial.

Obsrvese que la posicin 0 de la tabla no se debe utilizar, pues el sondeo multiplicativo slo visitara sta posicin en todos los reintentos. La Figura 2.18 muestra grficamente un ejemplo de la gestin de una tabla hash con este mtodo.

Figura 2.18. Posible estado de una tabla hash con direccionamiento abierto y sondeo multiplicativo. Hay tres grupos de claves que colisionan, con cuatro, tres y dos claves, respectivamente. El primer grupo corresponde a la posicin inicial 10, el segundo a la 14 y el tercero a la 11. Obsrvese que la posicin 0 no se usa y que las posiciones visitadas por cada grupo de sondeos se entremezclan.

54

Compiladores e intrpretes: teora y prctica

El sondeo multiplicativo tiene una propiedad interesante: cuando el nmero de reintentos es suficientemente grande, al sumar el desplazamiento proporcionado por el sondeo multiplicativo se obtiene una posicin fuera de la tabla. Las Figuras 2.14 y 2.15 muestran que se utiliza la operacin mod tamao(T) para seguir recorriendo la tabla circularmente en estos casos. Es fcil comprender que, si la tabla tiene un tamao primo, los sondeos la cubrirn por completo y no se formarn bandas contiguas.

Otros sondeos
En general, podra utilizarse cualquier algoritmo para el cdigo de la funcin delta. Se pueden obtener as diferentes tipos de sondeo. Una variante es el sondeo cuadrtico, que generaliza el sondeo multiplicativo de la siguiente manera: el sondeo multiplicativo realmente evala una funcin lineal, f(x)=posicin_inicial*x (donde x es el nmero de reintentos). El sondeo cuadrtico utiliza un polinomio de segundo grado g(x)=a*x2+b*x+c, en el que hay que determinar las constantes a, b y c. La Figura 2.19 muestra el pseudocdigo del sondeo cuadrtico.

int delta(int numero_reintento) { return (a*numero_reintento2+b*numero_reintento+c); }


Figura 2.19. Pseudocdigo del algoritmo del sondeo cuadrtico. Queda pendiente determinar las constantes del polinomio de segundo grado.

Otra variante, que slo tiene inters terico, consiste en generar el incremento de la funcin de manera pseudoaleatoria. La Figura 2.20 muestra el pseudocdigo del sondeo aleatorio.

int delta( ) { return ( random() ); }


Figura 2.20. Pseudocdigo del algoritmo del sondeo pseudoaleatorio.

Este mtodo slo tiene inters para el estudio analtico del rendimiento temporal. Se puede demostrar, aunque queda fuera del objetivo de este libro, que la dependencia del factor de carga del rendimiento temporal en la bsqueda de una clave que no se encuentra en la tabla hash, puede aproximarse mediante la siguiente expresin: 1 1

Captulo 2. Tabla de smbolos

55

La bsqueda de claves que s estn en la tabla se puede aproximar mediante esta otra: 1 1 log 1

Redimensionamiento de la tabla hash


A lo largo de la seccin anterior, se han mencionado diferentes circunstancias por las que las tablas hash, gestionadas con direccionamiento abierto, pueden quedarse sin sitio para insertar claves nuevas: Cuando toda la tabla est llena. Cuando, aunque exista espacio en la tabla, el mecanismo de sondeo no es capaz de encontrarlo para la clave estudiada. El conocimiento del rendimiento de una tcnica concreta permite aadir otra causa para la redimensin de la tabla: que el rendimiento caiga por debajo de un umbral. Todas las frmulas de estimacin del rendimiento dependen del factor de carga, y ste del tamao de la tabla y del nmero de datos que contenga. Es fcil tener en cuenta el nmero de datos almacenado en la tabla (que se incrementa cada vez que se inserta una nueva clave) y, por tanto, estimar el rendimiento en cada insercin. En cualquiera de los casos, el redimensionamiento de la tabla consta de los siguientes pasos: 1. Crear una nueva tabla mayor. El nuevo tamao tiene que seguir manteniendo las restricciones de tamao de los algoritmos utilizados (ser primo, potencia con exponente primo, etc.). 2. Obtener de la tabla original toda la informacin que contenga e insertarla en la tabla nueva.

2.3.7. Hash con encadenamiento


Esta tcnica se diferencia de la anterior en el uso de listas para contener las claves que colisionan en la misma posicin de la tabla hash. De esta forma, la tabla est formada por un vector de listas de claves. La funcin hash proporciona acceso a la lista en la que se encuentran todas las claves a las que les corresponde el mismo valor de funcin hash. La Figura 2.21 muestra un ejemplo de estas tablas.

D1

Dn

Figura 2.21. Representacin grfica de una tabla hash con listas de desbordamiento. Se resalta la lista de la posicin h(k)=i.

56

Compiladores e intrpretes: teora y prctica

Es frecuente que las listas se implementen utilizando memoria dinmica, es decir, solicitando espacio al sistema operativo cuando se necesite, sin ms limitacin que la propia de la computadora. Aunque se puede utilizar cualquier algoritmo de insercin y bsqueda en listas ordenadas, se supondr que las listas no estn necesariamente ordenadas, por lo que se utilizar la bsqueda lineal. Se puede demostrar, aunque queda fuera de los objetivos de este libro, que el rendimiento temporal de la bsqueda de una clave que no est en la tabla puede aproximarse precisamente mediante el factor de carga. Esta afirmacin puede comprenderse intuitivamente. La bsqueda lineal de claves que no estn en la lista tiene un rendimiento temporal del orden del tamao de la lista. Si se supone, como se est haciendo, que la funcin hash es uniforme, podemos suponer que en una tabla de m listas en las que hay n elementos en total (con n posiblemente mayor que m) cada lista tendr aproximadamente n/m elementos. ste es, precisamente, el valor de . Tambin se puede demostrar, aunque no se va a justificar ni siquiera intuitivamente, que la dependencia, en el caso de que las claves buscadas estn en la tabla, puede aproximarse mediante la siguiente expresin: 1 1 1 + 2 2m

Tablas de smbolos para lenguajes con estructuras 2.4 de bloques


2.4.1. Conceptos
Los lenguajes de programacin con estructura de bloques tienen mecanismos para definir el alcance de los nombres e identificadores (las secciones del cdigo donde estarn definidos). Es decir, en los lenguajes de programacin con estructura de bloques, en cada bloque slo estn definidos algunos identificadores. Los bloques ms frecuentes son las subrutinas (funciones o procedimientos), aunque tambin hay lenguajes de programacin que permiten definir bloques que no corresponden a subrutinas. La mayora de los lenguajes de programacin de alto nivel (Algol, PL/I, Pascal, C, C++, Prolog, LISP, Java, etc.) tienen estructura de bloques. La Figura 2.22 muestra un ejemplo de un programa escrito con un lenguaje ficticio, con estructura de bloques similares a las de C. A lo largo de esta seccin se utilizarn los siguientes conceptos: mbito. Sinnimo de bloque. Se utilizar indistintamente. mbitos asociados a una lnea de cdigo. Toda lnea de cdigo de un programa escrito con un lenguaje de estructura de bloques est incluida directamente en un bloque. A su vez, cada bloque puede estar incluido en otro, y as sucesivamente. Los mbitos asociados

Captulo 2. Tabla de smbolos

57

{ int

a,

b,

c,

d;

{ int e, f; ... L1: . . . }

{ int i, L2:

h;

{ int a; } } }

Figura 2.22. Bloques en un programa escrito con un lenguaje ficticio con estructura de bloques similar a la de C. Los bloques se inician y terminan, respectivamente, con los smbolos { y }. Slo se declaran identificadores de tipo entero y etiquetas (cuando tras el nombre del identificador se escribe el smbolo :). En el bloque ms externo, estn declaradas las variables a, b, c y d. En el segundo bloque, en orden de apertura, se declara la variable e, la variable f y la etiqueta L1. En el tercero, las variables i y h y la etiqueta L2, y en el cuarto la variable a. El comportamiento, cuando colisionan identificadores con el mismo nombre, depende del diseador del lenguaje de programacin.

a una lnea de cdigo son todos aquellos que directa o indirectamente incluyen a la lnea de cdigo. Bloque abierto. Dada una lnea de cdigo, todos los mbitos asociados a ella estn abiertos para ella. Bloque cerrado. Dada una lnea de cdigo, todos los bloques no abiertos para esa lnea se consideran cerrados para ella. Profundidad de un bloque. La profundidad de un bloque se define de la siguiente manera recursiva: El bloque ms externo tiene profundidad 0. Al abrir un bloque, su profundidad es igual a uno ms la profundidad del bloque en el que se abre. Bloque actual. En cada situacin concreta, el mbito actual es el ms profundo de los abiertos. Identificadores activos en un mbito concreto. Se entender por identificador activo el que est definido y es accesible en un bloque.

58

Compiladores e intrpretes: teora y prctica

Identificador global o local. Estos dos trminos se utilizan cuando hay al menos dos bloques, uno incluido en el otro. El trmino local se refiere a los identificadores definidos slo en el bloque ms interno y, por tanto, inaccesibles desde el bloque que lo incluye. El trmino global se aplica a los identificadores activos en el bloque externo, que desde el punto de vista del bloque interno estaban ya definidos cuando dicho bloque se abri. Tambin se usar el trmino global para situaciones similares a sta. Aunque los diferentes lenguajes de programacin pueden seguir criterios distintos, es frecuente usar las siguientes reglas: En un punto concreto de un programa slo estn activos los identificadores definidos en el mbito actual y los definidos en los mbitos abiertos en ese punto del programa. En general, las coincidencias de nombres (cuando el nombre de un identificador del bloque actual coincide con el de otro u otros de algn bloque abierto) se resuelven a favor del bloque actual; es decir, prevalece la definicin del bloque actual, que oculta las definiciones anteriores, haciendo inaccesibles los dems identificadores que tienen el mismo nombre. En las subrutinas, los nombres de sus argumentos son locales a ella, es decir, no son accesibles fuera de la misma. El nombre de la subrutina es local al bloque en que se defini y global para la subrutina. Hay muchas maneras de organizar la tabla de smbolos para gestionar programas escritos en lenguajes con estructura de bloques. A continuacin se describirn las dos posibilidades que podran considerarse extremas: Uso de una tabla de smbolos distinta para cada mbito. Uso de una tabla de smbolos para todos los mbitos. Los algoritmos de la tabla tambin dependen de otros factores de diseo del compilador: por ejemplo, si basta realizar una pasada, o si el compilador necesitar ms de un paso por el programa fuente.

2.4.2. Uso de una tabla por mbito


En este caso, para gestionar correctamente los identificadores es necesaria una coleccin de tablas hash, una para cada mbito. Lo importante es que se mantenga el orden de apertura de los mbitos abiertos.

Compiladores de un paso
Es la situacin ms sencilla. En este caso, los mbitos no se consultan una vez que se cierran, por lo que pueden descartarse sus tablas hash. En esta circunstancia, se puede utilizar una pila de mbitos abiertos. Esta estructura de datos devuelve primero los elementos insertados en ella ms recientemente, por lo que se mantiene automticamente el orden de apertura de los mbitos. La Figura 2.23 muestra un ejemplo del uso de esta tcnica con un programa.

Captulo 2. Tabla de smbolos

59

{ int a, { int ... L1: } { int L2: }

b, c, d; e, f; ...

i, h; { int a;

B2: e, f, L1 B1: a, b, c, d 1 B1: a, b, c, d, B2 2 B1: a, b, c, d, B2 3

B3: i, h, L2 B1: a, b, c, d, B2, B3 4

B4: a B3: i, h, L2, B4 B1: a, b, c, d, B2, B3 5 B3: i, h, L2, B4 B1: a, b, c, d, B2, B3 6 B1: a, b, c, d, B2, B3 7 8

Figura 2.23. Ejemplo de tabla de smbolos de un programa escrito con un lenguaje con estructura de bloques. La tabla de smbolos utiliza una tabla hash para cada mbito; el compilador slo realiza un paso. En la pila de tablas hash se seala la cima.

La insercin de una nueva clave se realiza en la tabla correspondiente al mbito actual (el que ocupa la cima de la pila). La bsqueda de una clave es la operacin que ms se complica, ya que, si el identificador no ha sido declarado en el mbito actual (no pertenece a su tabla hash), es necesario recorrer la pila completa, hasta el mbito exterior, para asegurar que dicho identificador no ha sido declarado en el programa y, por tanto, no puede ser utilizado. La gestin de los bloques se realiza as: 1. Cuando se abre un nuevo bloque:

60

Compiladores e intrpretes: teora y prctica

Se aade su nombre, si lo tiene, como identificador en el bloque actual, antes de abrir el nuevo bloque, ya que los nombres de las subrutinas tienen que ser locales al bloque donde se declaran. Se crea una nueva tabla hash para el bloque nuevo. Se inserta en la pila (push) la nueva tabla hash, que pasa a ser la del mbito actual. Se inserta en el mbito actual el nombre del nuevo bloque, ya que los nombres de las subrutinas son globales a la propia subrutina. 2. Cuando se cierra un bloque: Se saca de la pila (pop) la tabla hash del mbito actual y se elimina dicha tabla.

Compiladores de ms de un paso
El criterio general es el mismo que en el caso anterior, pero se necesita conservar las tablas hash de los bloques cerrados, por si se requiere su informacin en pasos posteriores. Un esquema fcil de describir consiste en modificar la pila del apartado anterior para convertirla en una lista, que conserve juntas, por encima de los mbitos abiertos, las tablas hash de los mbitos cerrados. De esta manera, el mbito actual estar siempre por debajo de los cerrados. Por debajo de l, se encontrar la misma pila descrita anteriormente. Es necesario aadir la informacin necesaria para marcar los bloques como abiertos o cerrados. La gestin descrita en el apartado anterior slo cambia en lo relativo a los mbitos cerrados: cuando un mbito se cierra, se marca como cerrado. Es fcil imaginar que la pila necesitar de ciertos datos adicionales (al menos, un apuntador al mbito actual) para su gestin eficiente. La Figura 2.24 muestra una tabla hash de este tipo, para el mismo programa fuente del ejemplo de la Figura 2.23.

2.4.3. Evaluacin de estas tcnicas


Entre los inconvenientes de estas tcnicas se pueden mencionar los siguientes: Se puede fragmentar en exceso el espacio destinado en el compilador a la tabla de smbolos, lo que origina cierta ineficiencia en cuanto al espacio utilizado. La bsqueda, que implica la consulta de varias tablas, puede resultar ineficiente en cuanto al tiempo utilizado.

2.4.4. Uso de una sola tabla para todos los mbitos


Este enfoque pretende minimizar el efecto de los inconvenientes detectados en el apartado anterior. Es evidente que, una vez que se conoce qu tratamiento tiene que darse a los identificadores de los programas escritos con lenguajes con estructura de bloques, es posible implementar sus

Captulo 2. Tabla de smbolos

61

{ int a, { int ... L1: } { int L2: }

b, c, d; e, f; ...

i, h; { int a;

B2: e, f, L1 B2: e, f, L1 B1: a, b, c, d 1 B1: a, b, c, d, B2 2 B2: e, f, L1 B1: a, b, c, d, B2 3 B3: i, h, L2 B1: a, b, c, d, B2, B3 4

B2: e, f, L1 B4: a B3: i, h, L2, B4 B1: a, b, c, d, B2, B3 5

B2: e, f, L1 B4: a B3: i, h, L2, B4 B1: a, b, c, d, B2, B3 6

B2: e, f, L1 B4: a B3: i, h, L2, B4 B1: a, b, c, d, B2, B3 7

B2: e, f, L1 B4: a B3: i, h, L2, B4 B1: a, b, c, d, B2, B3 8

Figura 2.24. Ejemplo de tabla de smbolos de un programa escrito con un lenguaje con estructura de bloques. La tabla de smbolos utiliza una tabla hash para cada mbito; el compilador realiza ms de un paso. En la pila de tablas hash se seala la cima. Los mbitos abiertos estn rodeados por un recuadro ms grueso que los cerrados.

tablas de smbolos utilizando una sola tabla para todos los bloques. A continuacin se mencionan los aspectos ms relevantes que hay que tener en cuenta: 1. Habr que mantener informacin, por un lado sobre los bloques, y por otro sobre los identificadores. 2. De cada bloque se tiene que guardar, al menos, la siguiente informacin: Identificacin del bloque. Apuntador al bloque en el que se declar. Apuntador al espacio donde se guardan sus identificadores.

62

Compiladores e intrpretes: teora y prctica

No se describirn ms detalles de esta tcnica, ya que su implementacin es slo un problema de programacin.

Informacin adicional sobre los identificadores 2.5 en las tablas de smbolos


De la definicin del lenguaje de programacin utilizado depende la informacin que hay que almacenar en la tabla de smbolos para el tratamiento correcto del programa: Clase del identificador, para indicar a qu tipo de objeto se refiere el identificador. Por ejemplo, podra ser una variable, funcin o procedimiento, una etiqueta, la definicin de un tipo de dato, el valor concreto de una enumeracin, etc. Tipo, para indicar el tipo de dato. Por ejemplo: entero, real, lgico, complejo, carcter, cadena de caracteres, tipo estructurado, tipo declarado por el programador, subrutina que devuelve un dato, subrutina que no devuelve dato alguno, operador, etc.

2.6 Resumen
Uno de los objetivos de este captulo es la justificacin de la eleccin de las tablas de dispersin o hash para la implementacin de la tabla de smbolos de los compiladores e intrpretes. En primer lugar se repasa, de manera informal e intuitiva, la complejidad temporal de los algoritmos de bsqueda ms utilizados (lineal y binaria sobre vectores de datos y los especficos de rboles binarios ordenados y rboles AVL). Se muestra cmo la comparacin de claves limita el rendimiento de una manera inaceptable y se justifica el uso de las tablas hash, de las que se describen con ms detalle diferentes variantes. Debido a la complejidad de la teora en la que se basan estos resultados y que el mbito de este libro no presupone al lector ningn conocimiento especfico de la materia, se ha pretendido, siempre que ha sido posible, acompaar cada resultado con una justificacin intuitiva y convincente que supla la ausencia de la demostracin formal. El captulo termina con la descripcin de dos aspectos prcticos propios del uso que los compiladores e intrpretes dan a la tabla hash: las tablas de smbolos para los lenguajes de programacin que tienen estructura de bloques y la informacin adicional que se necesita conservar en la tabla de smbolos sobre los identificadores.

2.7 Ejercicios y otro material prctico


El lector encontrar en http://www.librosite.net/pulido abundante material prctico sobre el contenido de este captulo con ejercicios resueltos y versiones ejecutables de los algoritmos descritos.

Captulo 2. Tabla de smbolos

63

2.8 Bibliografa
[1] Knuth, D. E. (1997): The art of computer programming, Addison Wesley Longman. [2] Cormen, T. H.; Leiserson, C. E.; Rivest, R. L., y Stein, C. (2001): Introduction to algorithms, The MIT Press, McGraw-Hill Book Company. [3] McKenzie, B. J.; Harries R. y Bell, T. C. (1990): Selecting a hashing algorithm, Software - Practice and Experience, 20(2), 209-224.

Captulo

Anlisis morfolgico

3.1 Introduccin
El analizador morfolgico, tambin conocido como analizador lxico (scanner, en ingls) se encarga de dividir el programa fuente en un conjunto de unidades sintcticas (tokens, en ingls). Una unidad sintctica es una secuencia de caracteres con cohesin lgica. Ejemplos de unidades sintcticas son los identificadores, las palabras reservadas, los smbolos simples o mltiples y las constantes (numricas o literales). Para llevar a cabo esta divisin del programa en unidades sintcticas, el analizador morfolgico utiliza un subconjunto de las reglas de la gramtica del lenguaje en el que est escrito el programa que se va a compilar. Este subconjunto de reglas corresponde a un lenguaje regular, es decir, un lenguaje definido por expresiones regulares. El analizador morfolgico lleva a cabo tambin otra serie de tareas auxiliares como el tratamiento de los comentarios y la eliminacin de blancos y smbolos especiales (caracteres de tabulacin y saltos de lnea, entre otros). La Tabla 3.1 muestra las unidades sintcticas de un lenguaje ejemplo y la Figura 3.1 muestra la gramtica independiente del contexto que usar el analizador morfolgico para identificar las unidades sintcticas de dicho lenguaje. Un analizador morfolgico es un autmata finito determinista que reconoce el lenguaje generado por las expresiones regulares correspondientes a las unidades sintcticas del lenguaje fuente. En las secciones siguientes se describe cmo programar manualmente dicho autmata mediante un proceso que comprende los siguientes pasos:

66

Compiladores e intrpretes: teora y prctica

Tabla 3.1. Unidades sintcticas de un lenguaje ejemplo. Palabras reservadas begin end bool int ref function if then fi else while do input output deref true false ; , + * ( = > Smbolos simples := <= Smbolos dobles Otros nmero (uno o ms dgitos) identificador (una letra seguida de 0 o ms letras y/o dgitos)

1. Construir el Autmata Finito No Determinista (AFND) correspondiente a una expresin regular. 2. Transformar el AFND obtenido en el paso 1 en un Autmata Finito Determinista (AFD). 3. Minimizar el nmero de estados del AFD obtenido en el paso 2. 4. Implementar en forma de cdigo el AFD obtenido en el paso 3.

<US> <p_reservada> <s_simple> <s_doble> <cte_num> <id> <resto_id> <alfanumerico> <digito> <letra>

::= <p_reservada> | <s_simple> | <s_doble> | <id> | <cte_num> ::= begin | end | bool | int | ref | function | if | then | fi | else | while | do | repeat | input | output | deref | true | false ::= ; | , | + | | * | ( | ) | = | > ::= := | <= ::= <digito> | <cte_num> <digito> ::= <letra> | <letra><resto_id> ::= <alfanumerico> | <alfanumerico><resto_id> ::= <digito> | <letra> ::= 0 | 1 | ... | 9 ::= a | b | ... | z | A | B | ... | Z

Figura 3.1. Gramtica para las unidades sintcticas del lenguaje ejemplo.

Captulo 3. Anlisis morfolgico

67

Tambin es posible implementar el autmata correspondiente al analizador morfolgico utilizando una herramienta de generacin automtica como la que se describe en la ltima seccin del captulo.

3.2 Expresiones regulares


Una expresin regular es una forma abreviada de representar cadenas de caracteres que se ajustan a un determinado patrn. Al conjunto de cadenas representado por la expresin r se lo llama lenguaje generado por la expresin regular r y se escribe L(r). Una expresin regular se define sobre un alfabeto y es una cadena formada por caracteres de dicho alfabeto y por una serie de operadores tambin llamados metacaracteres. Las expresiones regulares bsicas se definen de la siguiente forma: 1. El smbolo (conjunto vaco) es una expresin regular y L() = {} 2. El smbolo (palabra vaca) es una expresin regular y L() = {} 3. Cualquier smbolo a es una expresin regular y L(a) = {a} A partir de estas expresiones regulares bsicas pueden construirse expresiones regulares ms complejas aplicando las siguientes operaciones: 1. Concatenacin (se representa con el metacarcter .) Si r y s son expresiones regulares, entonces r.s tambin es una expresin regular y L(r.s)=L(r).L(s). El operador . puede omitirse de modo que rs tambin representa la concatenacin. La concatenacin de dos lenguajes L1 y L2 se obtiene concatenando cada cadena de L1 con todas las cadenas de L2. Por ejemplo, si L1= {00 , 1} y L2 = {11 , 0 , 10}, entonces L1L2 = {0011,000,0010,111,10,110}. 2. Unin (se representa con el metacarcter |) Si r y s son expresiones regulares, entonces r | s tambin es una expresin regular y L(r | s) = L(r) L(s). Por ejemplo, el lenguaje generado por la expresin regular ab | c es L(ab | c) = {ab , c}. 3. Cierre o clausura (se representa con el metacarcter *) Si r es una expresin regular, entonces r* tambin es una expresin regular y L(r*) = L(r)*. La operacin de cierre aplicada a un lenguaje L se define as: L* = Li
i=0

68

Compiladores e intrpretes: teora y prctica

donde Li es igual a la concatenacin de L consigo mismo i veces y L0 = . Por ejemplo, el lenguaje generado por la expresin regular a*ba* es L(a*ba*) = {b , ab , ba , aba , aab , ...}, es decir, el lenguaje formado por todas las cadenas de as y bs que contienen una nica b. Cuando aparecen varias operaciones en una expresin regular, el orden de precedencia es el siguiente: cierre, concatenacin y unin. Este orden puede modificarse mediante el uso de parntesis.

Autmata Finito No Determinista (AFND) para una 3.3 expresin regular


Intuitivamente un autmata finito consta de un conjunto de estados y, partiendo de un estado inicial, realiza transiciones de un estado a otro en respuesta a los smbolos de entrada que procesa. Cuando el autmata alcanza un estado de los que se denominan finales, se dice que ha reconocido la palabra formada por concatenacin de los smbolos de entrada procesados. Un autmata finito puede ser determinista o no determinista. La expresin no determinista significa que desde un mismo estado puede haber ms de una transicin etiquetada con el mismo smbolo de entrada. Un autmata finito no determinista es una quntupla (, Q, , q0, F), donde 1. es un conjunto finito de smbolos de entrada o alfabeto. 2. Q es un conjunto finito de estados. 3. es la funcin de transicin que recibe como argumentos un estado y un smbolo de entrada o el smbolo y devuelve un subconjunto de Q. 4. q0 Q es el estado inicial. 5. F Q es el conjunto de estados finales. Un autmata finito puede representarse mediante lo que se conoce como diagrama de transicin, que es un grafo dirigido construido de la siguiente forma: Cada nodo est etiquetado con un elemento de Q. Si (p,a) = q, se dibuja un arco del nodo con etiqueta p al nodo con etiqueta q etiquetado con el smbolo a. El estado inicial aparece sealado con una flecha sin origen. Los estados finales aparecen marcados con un doble crculo.

Captulo 3. Anlisis morfolgico

69

0 p 1 r 0,1 0 1 q

Figura 3.2. Diagrama de transicin para un autmata.

La Figura 3.2 muestra el diagrama de transicin para el autmata finito determinista ({0,1},{p,q,r},,p,{q}), donde est definida de la siguiente forma: (p,0)=q (q,1)=r (p,1)=r (r,0)=r (q,0)=q (r,1)=r

Para toda expresin regular e es posible construir un autmata finito no determinista que acepte el lenguaje generado por dicha expresin regular. El algoritmo es recursivo y consta de los siguientes pasos: 1. Si e = , el autmata correspondiente es el que aparece en la Figura 3.3(a). 2. Si e = , el autmata correspondiente es el que aparece en la Figura 3.3(b). 3. Si e = a, a , el autmata correspondiente es el que aparece en la Figura 3.3(c).

p a) b) a c)

Figura 3.3. Autmatas para expresiones regulares bsicas.

4. Si e = r|s y tenemos los autmatas correspondientes a r y s, que representaremos como aparecen en la Figura 3.4, el autmata correspondiente a la expresin r|s es el que aparece en la Figura 3.5(a).

70

Compiladores e intrpretes: teora y prctica

p1

q1

p2

q2

Figura 3.4. Autmatas correspondientes a las expresiones r y s.

5. Si e = rs y tenemos los autmatas correspondientes a r y s, que representaremos como aparecen en la Figura 3.4, el autmata correspondiente a la expresin rs es el que aparece en la Figura 3.5(b).

p1 p p2

q1 q

s a)

q2

p1

q1 b)

p2

q2

Figura 3.5. Autmatas correspondientes a las expresiones r|s y rs.

6. Si e = r* y tenemos el autmata correspondiente a r, que representaremos como aparece en la Figura 3.6(a), el autmata correspondiente a la expresin r* es el que aparece en la Figura 3.6(b).

p1

r a)

q1

p1

r b)

q1

Figura 3.6. Autmatas correspondientes a las expresiones r y r*.

Captulo 3. Anlisis morfolgico

71

Como ejemplo, consideremos las reglas que definen las constantes numricas en la gramtica de la Figura 3.1, que son las siguientes: <cte_num> ::= <digito> | <cte_num> <digito> Para obtener la expresin regular correspondiente a estas reglas hay que construir primero el autmata que reconoce el lenguaje generado por la gramtica y, en un segundo paso, obtener la expresin regular equivalente al autmata. En [1] se describen en detalle los algoritmos necesarios, el primero de los cuales slo es aplicable a gramticas tipo 3. Aplicando este proceso a las reglas que definen las constantes numricas se obtiene la expresin regular digito.digito*. Esta expresin es el resultado de concatenar dos expresiones regulares: digito y digito*. Aplicando a esta expresin el paso 3 del algoritmo recursivo descrito anteriormente, se obtiene el AFND para la expresin digito [vase Figura 3.7(a)]. A partir de este AFND, y aplicando el paso 6 de dicho algoritmo, se obtiene el AFND de la Figura 3.7(b). Por ltimo, aplicando el paso 5, se obtiene el AFND para la expresin completa, que aparece en la Figura 3.7(c).
q1 digito a) q3 q4 digito b) q1 digito q2 q3 q4 digito c) q5 q6 q5 q6

q2

Figura 3.7. AFND para la expresin regular digito.digito*.

Autmata Finito Determinista (AFD) equivalente 3.4 a un AFND


Un autmata finito determinista es una quntupla (, Q, , q0, F), donde 1. es un conjunto finito de smbolos de entrada o alfabeto. 2. Q es un conjunto finito de estados.

72

Compiladores e intrpretes: teora y prctica

3. es la funcin de transicin que recibe como argumentos un estado y un smbolo de entrada y devuelve un estado. 4. q0 Q es el estado inicial. 5. F Q es el conjunto de estados finales. La funcin de transicin extendida recibe como argumentos un estado p y una cadena de caracteres wy devuelve el estado que alcanza el autmata cuando parte del estado p y procesa la cadena de caracteres w. Dado un autmata finito no determinista N = (, Q, f, q0, F), siempre es posible construir un autmata finito determinista D = (, Q, f, q0, F) equivalente (que acepte el mismo lenguaje). Para construir dicho autmata seguiremos el siguiente procedimiento: Cada estado de D corresponde a un subconjunto de los estados de N. En el autmata de la Figura 3.7(c) los subconjuntos {q1}, {q3, q4} o {q2, q4, q6} seran posibles estados del autmata finito determinista equivalente. El estado inicial q0 de D es el resultado de calcular el cierre del estado inicial q0 de N. El cierre de un estado e se representa como e y se define como el conjunto de estados alcanzables desde e mediante cero o ms transiciones . En el autmata de la Figura 3.7(c) el cierre de cada uno de los estados son las siguientes: q 1 = {q1} = {q2, q3, q4, q6} q2 = {q3, q4, q6} q3 q4 = {q4} q5 = {q5, q4, q6} q6 = {q6}

Por lo tanto, el estado inicial del AFD correspondiente al AFND de la Figura 3.7(c) ser {q1}. Desde un estado P de D habr una transicin al estado Q con el smbolo a del alfabeto. Para calcular esta transicin calculamos primero un conjunto intermedio Pa formado por los estados q de N tales que para algn p en P existe una transicin de p a q con el smbolo a. El estado Q se obtiene calculando el cierre del conjunto Pa. Veamos esto con un ejemplo. Partiendo del AFND de la Figura 3.7(c), la transicin desde el estado inicial {q1} con el smbolo digito se calculara de la siguiente forma: {q1}digito = {q2} 1 }digito = {q2, q3, q4, q6} {q Puesto que (q4,digito)=q5, la transicin desde el estado {q2,q3,q4,q6} con el smbolo digito ser: {q2, q3, q4, q6}digito = {q5} 5 }digito = {q5, q4, q6} {q Puesto que (q4,digito)=q5, la transicin desde el estado {q5,q4,q6} con el smbolo digito ser: {q5, q4, q6}digito= {q5} 5 }digito = {q5, q4, q6} {q

Captulo 3. Anlisis morfolgico

73

digito {q1 digito digito

{q2, q3, q4}

{q5, q4, q6}

Figura 3.8. Autmata finito determinista correspondiente al AFND de la Figura 3.7.

En el autmata finito determinista D un estado ser final si contiene algn estado final del AFND N. En el AFD correspondiente al autmata de la Figura 3.7(c), sern estados finales todos aquellos que contengan el estado q6.

La Figura 3.8 muestra el AFD equivalente al AFND de la Figura 3.7(c).

3.5 Autmata finito mnimo equivalente a uno dado


Recordemos que el objetivo que se persigue es obtener un autmata finito que sirva para implementar un analizador morfolgico, es decir, que acepte las cadenas correspondientes a las unidades sintcticas del lenguaje fuente que se va a compilar. Por este motivo, el analizador morfolgico ser tanto ms eficiente cuanto menor sea el nmero de estados del autmata finito correspondiente. Para cualquier autmata finito, existe un autmata finito mnimo equivalente. El primer paso para obtener este autmata mnimo es identificar pares de estados equivalentes. Decimos que los (p,w), es un estado final si y slo si estados p y q son equivalentes si para toda cadena w, (q,w) es un estado final. La relacin equivalente es una relacin de equivalencia que esta blece clases de equivalencia en el conjunto de estados de un autmata finito. Dos estados son equivalentes si no son distinguibles. Podemos calcular los pares de estados distinguibles en un AFD mediante el algoritmo por llenado de tabla. Este algoritmo realiza una bsqueda recursiva de pares distinguibles aplicando las siguientes reglas: 1. Si p es un estado final y q no lo es, el par {p,q} es distinguible. 2. Si para dos estados p y q se cumple que existe una transicin de p a r con el smbolo a y una transicin de q a s con el smbolo a, y los estados r y s son distinguibles, entonces el par {p,q} es distinguible. Como ejemplo, consideremos el autmata de la Figura 3.9, idntico al de la Figura 3.8, salvo que se han renombrado los estados para simplificar. Si aplicamos la regla 1 a dicho autmata, los estados {1, 2} y {1, 3} son distinguibles. Por lo tanto, slo es necesario averiguar si los estados {2, 3} son distinguibles. Aplicando la regla 2 a estos estados, se cumple que existe una transicin de 2 a 3 con el smbolo digito y una transicin de 3 a 3 con el smbolo digito, pero los estados 3 y 3 no son distinguibles porque son el mismo estado. Por lo tanto, los estados {2, 3} no son distinguibles, es decir, son equivalentes.

74

Compiladores e intrpretes: teora y prctica

digito 1 digito 2 digito 3

Figura 3.9. Autmata finito determinista de la Figura 3.8 con estados renombrados.

Dado un autmata finito determinista A, el algoritmo para construir un autmata mnimo equivalente B puede enunciarse de la siguiente forma: 1. Cada clase de equivalencia establecida por la relacin equivalente en el conjunto de estados de A es un estado de B. 2. El estado inicial de B es la clase de equivalencia que contiene el estado inicial de A. 3. El conjunto de estados finales de B es el conjunto de clases de equivalencia que contienen estados finales de A. 4. Sea la funcin de transicin de B. Si S y T son bloques de estados equivalentes de A y a es un smbolo de entrada (S,a) = T si se cumple que para todos los estados q de S, (q,a) pertenece al bloque T. Apliquemos este algoritmo al autmata A de la Figura 3.9. 1. Las clases de equivalencia establecidas por la relacin equivalente en el conjunto de estados de A son {1} y {2,3}. stos sern los estados del autmata mnimo B. 2. El estado inicial de B es el bloque {1}. 3. El autmata B slo tiene un estado final que es el bloque {2,3}, porque contiene los estados 2 y 3, que son estados finales en A. 4. En el autmata A hay tres transiciones, todas ellas con el smbolo digito: Del estado 1 al 2. Pasa a ser una transicin del bloque {1} al {2,3}. Del estado 2 al 3. Pasa a ser una transicin del bloque {2,3} al {2,3}. Del estado 3 al 3. Pasa a ser una transicin del bloque {2,3} al {2,3}. Las dos ltimas transiciones son redundantes, por lo que slo dejaremos una de ellas. La Figura 3.10 muestra el autmata mnimo equivalente al autmata de la Figura 3.9.

digito 1 digito 2

Figura 3.10. Autmata finito determinista mnimo equivalente al de la Figura 3.9.

Captulo 3. Anlisis morfolgico

75

3.6 Implementacin de autmatas finitos deterministas


El primer paso para implementar un autmata finito que sea capaz de reconocer las unidades sintcticas del lenguaje fuente que se va a compilar es identificar las expresiones regulares que representan dichas unidades sintcticas. Un problema que puede surgir es que determinadas expresiones regulares den lugar a que no exista una nica forma de dividir la cadena de entrada en unidades sintcticas. Por ejemplo, utilizando la expresin regular digito.digito* para representar constantes numricas, existiran varias formas de dividir la cadena de entrada 381 en unidades sintcticas: Dos unidades sintcticas: 3 y 81 Dos unidades sintcticas: 38 y 1 Una unidad sintctica: 381 Para resolver esta ambigedad, se utiliza la regla conocida como principio de la subcadena ms larga, que consiste en identificar siempre como siguiente unidad sintctica la cadena de caracteres ms larga posible. Si aplicamos este principio, la cadena 381 se identificara como una nica unidad sintctica. Para implementar este principio, puede aadirse al autmata una nueva transicin con la etiqueta otro; vase la Figura 3.11. En esta figura la etiqueta otro aparece entre corchetes para indicar que, aunque en el caso general los autmatas avanzan una posicin en la cadena de entrada cuando hacen una transicin, en este caso el autmata leer el carcter de entrada, pero sin avanzar una posicin.

digito 1 digito 2 [otro] 3

Figura 3.11. Autmata finito determinista con transicin con la etiqueta otro.

Existen diversas formas de implementar mediante cdigo un autmata finito. Una de ellas es utilizar el pseudocdigo que aparece en la Figura 3.12, en el que se utilizan las siguientes estructuras de datos: transicin: vector de dos dimensiones indexado por estados y caracteres, que representa la funcin de transicin del autmata. final: vector booleano de una dimensin indexado por estados, que representa los estados finales del autmata. error: vector de dos dimensiones indexado por estados y caracteres, que representa las casillas vacas en la tabla de transicin. avanzar: vector booleano de dos dimensiones indexado por estados y caracteres, que representa las transiciones que avanzan en la entrada.

76

Compiladores e intrpretes: teora y prctica

estado := estado inicial; ch := siguiente carcter de entrada; while not final[estado] and not error[estado,ch] do estado := transicin[estado,ch]; if avanzar[estado,ch] then ch := siguiente carcter de entrada; end if final[estado] then aceptar;
Figura 3.12. Pseudocdigo que implementa un autmata finito.

En http://www.librosite.net/pulido se incluye una versin ejecutable del pseudocdigo de la Figura 3.12.

3.7 Otras tareas del analizador morfolgico


Adems de dividir el programa fuente en unidades sintcticas, el analizador morfolgico suele llevar a cabo otras tareas auxiliares que facilitan la tarea posterior del analizador sintctico. Una de estas tareas es la de eliminar ciertos caracteres delimitadores, como espacios en blanco, tabuladores y saltos de lnea. La siguiente expresin regular representa la aparicin de uno o ms de estos caracteres delimitadores. (blanco|tab|nuevalinea)(blanco|tab|nuevalinea)* El analizador morfolgico puede tambin encargarse de eliminar los comentarios. Consideremos un formato para comentarios como el que se utiliza en el lenguaje de programacin C, es decir, cadenas de caracteres de longitud variable delimitadas por los caracteres /* y */. Aunque es difcil encontrar una expresin regular que represente este formato, s es posible construir un autmata finito como el que aparece en la Figura 3.13, que identifica este tipo de comentarios. Aunque, para el analizador morfolgico, una unidad sintctica no es ms que una secuencia de caracteres, cada unidad sintctica tiene asociada una informacin semntica que ser utiliza-

otro 1 / 2 * 3 * otro

* 4 / 5

Figura 3.13. Autmata finito para comentarios tipo C.

Captulo 3. Anlisis morfolgico

77

da por el resto de los componentes del compilador. Esta informacin semntica se almacena en forma de atributos de la unidad sintctica y se ver en detalle en el captulo sobre el anlisis semntico. En la fase de anlisis morfolgico, es posible calcular el valor de algunos de estos atributos como, por ejemplo, el valor de una constante numrica, o la cadena de caracteres concreta que forma el nombre de un identificador.

3.8 Errores morfolgicos


El analizador morfolgico puede detectar determinados tipos de error, entre los que se encuentran los siguientes: Smbolo no permitido, es decir, que no pertenece al alfabeto del lenguaje fuente. Por ejemplo, en la gramtica de la Figura 3.1 no aparece el signo <, y sera un error morfolgico que dicho smbolo apareciera en un programa fuente escrito en dicho lenguaje. Identificador mal construido o que excede de la longitud mxima permitida. En un lenguaje en el que el primer carcter de un identificador deba ser una letra, un identificador que comience con un dgito sera un ejemplo de este tipo de error. Constante numrica mal construida o que excede de la longitud mxima permitida. Por ejemplo, si el lenguaje fuente acepta nmeros en punto fijo que constan de una parte entera y una parte decimal separadas por un punto, una constante numrica en la que se omitiera la parte decimal podra ser un error morfolgico. Constante literal mal construida. Un ejemplo de este tipo de error sera el literal Pepe, al que le falta la comilla de cierre. Existen otros tipos de error que el analizador morfolgico no ser capaz de detectar. Por ejemplo, si en la entrada aparece la cadena bgein, el analizador morfolgico lo reconocer como un identificador, cuando probablemente se trate de la palabra reservada begin mal escrita. Habitualmente, cuando el analizador morfolgico detecta un error en la entrada, emite un mensaje de error para el usuario y detiene la ejecucin. Un comportamiento alternativo es intentar recuperarse del error y continuar con el procesamiento del fichero de entrada. Las estrategias de recuperacin de errores son variadas y se basan en la insercin, eliminacin o intercambio de determinados caracteres. Como ejemplo de recuperacin de errores, se podran incorporar al analizador morfolgico algunas expresiones regulares ms, que correspondan a unidades sintcticas errneas que es probable que aparezcan en la entrada. Por ejemplo, si el lenguaje fuente acepta nmeros en punto fijo, que corresponden a la expresin digito*.digito.digito*, podemos aadir la expresin regular digito*. para que recoja los nmeros en punto flotante errneos a los que les falte la parte decimal. De esta forma, cuando el analizador morfolgico detecte que la entrada corresponde a esta unidad sintctica errnea, adems de mostrar un mensaje de error dirigido al usuario, puede aadir, por ejemplo, el dgito 0 a la unidad sintctica, para transformarla en otra correcta.

78

Compiladores e intrpretes: teora y prctica

Generacin automtica de analizadores 3.9 morfolgicos: la herramienta lex


Existen herramientas que generan analizadores morfolgicos de forma automtica. Una de las ms utilizadas se llama lex. Lex recibe como entrada un fichero de texto con extensin .l, que contiene las expresiones regulares que corresponden a las unidades sintcticas del lenguaje que se va a compilar, al que llamaremos fichero de especificacin lex. Como resultado del proceso del fichero de especificacin, lex genera un fichero en cdigo C, llamado lex.yy.c. Este fichero contiene una funcin llamada yylex(), que implementa el analizador morfolgico que reconoce las unidades sintcticas especificadas en el fichero de entrada.

3.9.1. Expresiones regulares en lex


Al igual que en la notacin general para expresiones regulares descrita en la Seccin 3.2, lex utiliza los metacaracteres | y * para representar las operaciones de unin y cierre, respectivamente. Para representar la operacin de concatenacin en lex no se utiliza ningn meta-carcter especfico: basta con escribir las expresiones regulares correspondientes de forma consecutiva. Adems, en las expresiones regulares en lex pueden utilizarse otros metacaracteres que se describen a continuacin. El metacarcter . representa cualquier carcter, excepto el salto de lnea \n. Por ejemplo, la expresin regular .*0.* representa todas las cadenas que contienen al menos un 0. Los corchetes [ ] y el guin - se utilizan para representar rangos de caracteres. Por ejemplo, la expresin [a-z] representa las letras minsculas, y la expresin [0-9] representa los dgitos del 0 al 9. Los corchetes tambin pueden utilizarse para representar alternativas individuales, de modo que la expresin [xyz] representa una x, una y o una z, y es equivalente a la expresin x|y|z. El metacarcter + indica una o mas apariciones de la expresin que lo precede. Utilizando los metacaracteres vistos hasta ahora, podramos representar las constantes numricas que aparecen en la gramtica de la Figura 3.1 mediante la expresin regular [0-9]+. El metacarcter representa cualquier carcter que no est en un conjunto dado. Por ejemplo, la expresin 0 representa cualquier carcter que no sea el dgito 0. El metacarcter ^ tiene un significado similar, combinado con los corchetes. Por ejemplo, la expresin [^xyz] representa cualquier carcter que no sea x, ni y ni z, y es equivalente a la expresin (x|y|z). El metacarcter ? sirve para indicar que una parte de una expresin es opcional. Por ejemplo, la expresin (+|-)?[0-9]+ representa los nmeros enteros como una cadena compuesta por un signo opcional, seguido por al menos un dgito entre 0 y 9. Los metacaracteres pierden su significado, y pasan a ser caracteres normales, si los encerramos entre comillas. Por ejemplo, los nmeros en punto fijo, como 7.51, pueden representarse con la expresin regular (+|-)?[0-9]+ .(+|-)?[0-9]+.

Captulo 3. Anlisis morfolgico

79

3.9.2. El fichero de especificacin lex


La Figura 3.14 muestra la estructura del fichero de especificacin lex, que consta de tres secciones, separadas por lneas con el separador %%: la seccin de definiciones, la seccin de reglas y la seccin de rutinas auxiliares. seccin de definiciones %% seccin de reglas %% seccin de rutinas auxiliares
Figura 3.14. Estructura de un fichero de especificacin lex.

a) Seccin de definiciones
La seccin de definiciones contiene la siguiente informacin: Cdigo C encerrado entre los delimitadores %{ y %}, que se copia literalmente en el fichero de salida lex.yy.c antes de la definicin de la funcin yylex(). Habitualmente, esta seccin contiene declaraciones de variables y funciones que se utilizarn posteriormente en la seccin de reglas, as como directivas #include. Definiciones propias de lex, que permiten asignar nombre a una expresin regular o a una parte de ella, para utilizarlo posteriormente en lugar de la expresin. Para dar nombre a una expresin regular, se escribe el nombre en la primera columna de una lnea, seguido por uno o ms espacios en blanco y por la expresin regular que representa. Por ejemplo, podramos dar nombre a la expresin regular que representa a los dgitos del 0 al 9 de la siguiente forma: DIGITO [0-9]

Para utilizar el nombre de una expresin regular en otra expresin regular, basta con encerrarlo entre llaves. Por ejemplo, utilizando la expresin regular llamada DIGITO, podramos representar de la siguiente forma las constantes numricas que aparecen en la gramtica de la Figura 3.1: CONSTANTE {DIGITO}+ Opciones de lex similares a las opciones de la lnea de mandatos. Estas opciones se especifican escribiendo la palabra %option seguida de un espacio en blanco y del nombre de la opcin. Como ejemplo, en un fichero de especificacin de lex podra aparecer la siguiente lnea: %option noyywrap

80

Compiladores e intrpretes: teora y prctica

Veamos cul es el significado de esta lnea. Existe la posibilidad de que la funcin yylex() analice morfolgicamente varios ficheros, encadenando uno detrs de otro, con el siguiente mecanismo: cuando yylex() encuentra el fin de un fichero, llama a la funcin yywrap(). Si esta funcin devuelve 0, el anlisis contina con otro fichero; si devuelve 1, el anlisis termina. Para poder utilizar la funcin yywrap() en Linux, es necesario enlazar con la biblioteca de lex que proporciona una versin por defecto de yywrap(). En Windows, el usuario tiene que proporcionar el cdigo de la funcin, incorporndola en la ltima seccin del fichero de especificacin. La opcin noyywrap provoca que no se invoque automticamente a la funcin yywrap()cuando se encuentre un fin de fichero, y se suponga que no hay que analizar ms ficheros. Esta solucin es ms cmoda que tener que escribir la funcin o enlazar con alguna biblioteca. Definicin de condiciones de inicio. Estas definiciones se vern con ms detalle en la Seccin 3.9.4.

b) Seccin de reglas
La seccin de reglas contiene, para cada unidad sintctica, la expresin regular que la describe, seguida de uno o ms espacios en blanco y del cdigo C que debe ejecutarse cuando se localice en la entrada dicha unidad sintctica. Este cdigo C debe aparecer encerrado entre llaves. Como ejemplo, consideremos un analizador morfolgico que reconozca en la entrada las constantes numricas y las palabras reservadas begin y end. Cada vez que localice una de ellas, debe mostrar en la salida un mensaje de aviso de unidad sintctica reconocida. La Figura 3.15 muestra el fichero de especificacin lex que correspondera a dicho analizador morfolgico.

c) Seccin de rutinas auxiliares


Habitualmente, esta seccin contiene las funciones escritas por el usuario para utilizarlas en la seccin de reglas, es decir, funciones de soporte. En esta seccin tambin se incluyen las funciones de lex que el usuario puede redefinir, como, por ejemplo, la funcin yywrap(). El contenido de esta seccin se copia literalmente en el fichero lex.yy.c que genera lex. Aunque, en el caso general, la funcin yylex() es llamada por el analizador sintctico, el analizador morfolgico tambin puede funcionar como un componente autnomo, en cuyo caso ser necesario incluir en la seccin de rutinas auxiliares una funcin main que realice la llamada a la funcin yylex(). ste es el caso del fichero de especificacin lex que aparece en la Figura 3.15. La seccin de rutinas auxiliares se puede omitir, aunque s debe aparecer el separador %%.

3.9.3. Cmo funciona yylex()?


Una llamada a yylex() permite realizar el anlisis morfolgico hasta encontrar el fin de la entrada, siempre que ninguno de los fragmentos de cdigo C asociado a las expresiones regulares

Captulo 3. Anlisis morfolgico

81

%{ #include <stdio.h> /* para utilizar printf en la seccin de reglas */ %} digito [0-9] constante {digito}+ %option noyywrap %% begin end {constante}

{ printf(reconocido-begin-\n); } { printf(reconocido-end-\n); } { printf(reconocido-num-\n); }

%% int main() { return yylex(); }


Figura 3.15. Un fichero de especificacin lex.

correspondientes a las unidades sintcticas contenga una instruccin return que haga que yylex() termine. Cuando se encuentra el fin de la entrada, yylex() devuelve 0 y termina. La llamada a yylex() en la funcin main de la Figura 3.15 ilustra este caso. Otra alternativa es que el cdigo C asociado a cada expresin regular contenga una sentencia return. En este caso, cuando se identifica en la entrada una unidad sintctica que satisface dicha expresin regular, se devuelve un valor al mdulo que invoc a la funcin yylex(). La siguiente llamada a yylex() comienza a leer la entrada en el punto donde se qued la ltima vez. Como en el caso anterior, cuando se encuentra el fin de la entrada, yylex() devuelve 0 y termina. La Figura 3.16 implementa esta alternativa en un fichero de especificacin lex, para un analizador morfolgico con la misma funcionalidad que el de la Figura 3.15. Adems de la funcin main, en el fichero de especificacin de la Figura 3.16 puede apreciarse otra diferencia con respecto al que aparece en la Figura 3.15. En la seccin de definiciones, aparece la instruccin #include tokens.h. Este fichero de cabeceras aparece en la Figura 3.17 y contiene un conjunto de instrucciones #define, que asignan un valor entero a cada unidad sintctica que va a reconocer el analizador morfolgico, y que ser el valor devuelto por la funcin yylex() para cada una de ellas. Si la funcin yylex() encuentra concordancia con ms de una expresin regular, selecciona aquella que permita establecer una correspondencia de mayor nmero de caracteres con la entrada. Por ejemplo, supongamos que en un fichero de especificacin aparecen las siguientes reglas: begin end [a-z]+ { return TOK_BEGIN; } { return TOK_END; } { return TOK_ID;}

82

Compiladores e intrpretes: teora y prctica

%{ #include <stdio.h> #include tokens.h %} digito [0-9] constante {digito}+ %option noyywrap %% begin end {constante} %% int main() { int token; while (1) { token = yylex(); if (token == TOK_BEGIN) \n); if (token == TOK_END) if (token == TOK_NUM) if (token == 0) break; } { return TOK_BEGIN; } { return TOK_END; } { return TOK_NUM; }

printf(reconocido-beginprintf(reconocido-end-\n); printf(reconocido-num-\n);

Figura 3.16. Un fichero de especificacin lex con instrucciones return.

La entrada beginend concuerda con dos expresiones regulares: begin y [a-z]+, hasta que se lee la segunda e. En ese momento se descarta la expresin regular begin y se selecciona la expresin regular correspondiente a los identificadores, porque es la que establece una correspondencia de mayor longitud. Por lo tanto, la entrada beginend ser considerada como una nica unidad sintctica de tipo identificador.

#define TOK_BEGIN 1 #define TOK_END 2 #define TOK_NUM 129


Figura 3.17. El fichero tokens.h.

Captulo 3. Anlisis morfolgico

83

Si hay concordancia con varias expresiones regulares de la misma longitud, se elige aquella que aparece antes en la seccin de reglas dentro del fichero de especificacin lex. Por lo tanto, el orden en que se colocan las reglas es determinante. Por ejemplo, si en un fichero de especificacin aparecen las siguientes reglas: [a-z]+ begin end { return TOK_ID;} { return TOK_BEGIN; } { return TOK_END; }

la entrada begin ser considerada como un identificador, porque concuerda con dos expresiones regulares: begin y [a-z]+, pero la expresin regular correspondiente a los identificadores aparece antes en el fichero de especificacin. Al procesar con lex estas reglas, aparecer un mensaje en el que se indica que las reglas segunda y tercera nunca se van a utilizar. Lex declara un vector de caracteres llamado yytext, que contiene la cadena correspondiente a la ltima unidad sintctica reconocida por la funcin yylex(). La longitud de esta cadena se almacena en la variable de tipo entero yyleng. El analizador morfolgico es la parte del compilador que accede al fichero de entrada y, por lo tanto, es el que conoce la posicin (lnea y carcter) de las unidades sintcticas en dicho fichero. Esta posicin es muy importante para informar de los errores de compilacin. Para conocer la posicin de las unidades sintcticas en el fichero de entrada se pueden utilizar dos variables, una que guarde el nmero de lnea, y otra para la posicin del carcter dentro de la lnea. Ambas variables se pueden declarar en la seccin de definiciones del fichero de especificacin lex. Por ejemplo: %{ int lineno = 1; /* nmero de lnea */ int charno = 0; /* nmero de carcter */ %} La actualizacin de las variables se realiza en el cdigo de las reglas. Por ejemplo, cuando se analice la palabra reservada begin, se incrementar en 5 unidades el valor de la variable charno. Cuando se encuentre un identificador o un nmero entero, se puede utilizar el contenido de la variable yyleng para incrementar la variable charno en el nmero de caracteres correspondiente. De igual forma, cuando se encuentra un salto de lnea, la variable lineno se incrementa en 1 unidad, mientras la variable charno se inicializa a 0. La entrada y salida de lex se realiza a travs de los ficheros yyin e yyout, respectivamente. Antes de llamar a la funcin yylex(), puede asignarse a cualquiera de estos dos ficheros una variable de tipo FILE*. Si no se realiza ninguna asignacin, sus valores por defecto son la entrada estndar (stdin) y la salida estndar (stdout), respectivamente.

3.9.4. Condiciones de inicio


Lex ofrece la posibilidad de asociar condiciones de inicio a las reglas, lo que quiere decir que las acciones asociadas a esas reglas slo se ejecutarn si se cumple la condicin de inicio corres-

84

Compiladores e intrpretes: teora y prctica

pondiente. Esta caracterstica excede a la potencia de las expresiones regulares y de los autmatas finitos, pero resulta imprescindible para representar determinadas unidades sintcticas. Las condiciones de inicio se especifican en la seccin de definiciones del fichero de especificacin lex utilizando lneas con el siguiente formato: %s nombre_condicion donde nombre representa la condicin de inicio. Tambin pueden utilizarse lneas con el formato %x nombre_condicion para especificar condiciones de inicio exclusivas. Ambas opciones se diferencian porque, cuando el analizador se encuentra con una condicin de inicio exclusiva, slo son aplicables las reglas que tienen asociada esa condicin de inicio, mientras que si la condicin no es exclusiva (si se ha definido con la opcin %s), se aplican tambin las reglas que no tienen condicin de inicio. Por ejemplo: %s %x %% abc <uno>def <dos>ghi {printf(reconocido ); BEGIN(uno);} {printf(reconocido ); BEGIN(dos);} {printf(reconocido ); BEGIN(INITIAL);} uno dos

Figura 3.18. Un fichero de especificacin lex con condiciones de inicio.

En el ejemplo de la Figura 3.18, en la condicin de inicio uno pueden aplicarse las reglas correspondientes a las expresiones abc y def . En el estado dos, slo puede aplicarse la regla correspondiente a la expresin ghi. Las condiciones de inicio aparecen en la seccin de reglas del fichero de especificacin precediendo a una expresin regular y encerradas entre los smbolos < y >. Por ejemplo, si asociamos la condicin de inicio comentario a las reglas que corresponden a la identificacin de comentarios, la lnea siguiente, colocada en la seccin de reglas, indica que, una vez detectado el comienzo de un comentario, cada salto de lnea detectado en la entrada generar un incremento en el contador del nmero de lneas. <comentario>\n {lineno++;}

Se puede poner al analizador en una determinada condicin de inicio escribiendo la instruccin BEGIN(nombre_condicion) en la parte de accin de una regla. Por ejemplo, la regla

Captulo 3. Anlisis morfolgico

85

siguiente pone al analizador en la condicin de inicio comentario cuando se detectan los caracteres de comienzo de comentario en la entrada. /* {BEGIN(comentario);}

Para pasar a la condicin de inicio normal, utilizaremos la instruccin BEGIN(INITIAL) En nuestro ejemplo, la regla <comentario>*+/ {BEGIN(INITIAL);} pasa al analizador a la condicin de inicio normal cuando se detecta el fin de un comentario, es decir, uno o ms caracteres * y un carcter /. El cdigo completo que habra que incluir en el fichero de especificacin lex de un analizador morfolgico, para que identifique correctamente los comentarios de tipo C, aparece en la Figura 3.19. %x comentario %% /* <comentario>[^*\n] <comentario>*+[^*/\n]* <comentario>\n <comentario>*+/ {BEGIN(comentario);}

{lineno++;} {BEGIN(INITIAL);}

Figura 3.19. Reglas para identificacin de comentarios tipo C.

La primera regla pone al analizador en la condicin de inicio comentario cuando se detectan en la entrada los caracteres de comienzo de comentario. Las reglas segunda y tercera no realizan ninguna accin mientras se estn leyendo caracteres *, o cualquier carcter distinto de *, / o del carcter de salto de lnea. La cuarta regla incrementa el contador del nmero de lneas cada vez que se detecta en la entrada un salto de lnea. Por ltimo, la quinta regla pasa el analizador a la condicin de inicio normal cuando se detecta el fin de un comentario, es decir, uno o ms caracteres * y un carcter /. El fichero de especificacin lex completo para el lenguaje generado por la gramtica de la Figura 3.1 aparece en http://www.librosite.net/pulido

3.10 Resumen
Este captulo describe el funcionamiento de un analizador morfolgico, cuya tarea principal es dividir el programa fuente en un conjunto de unidades sintcticas. Para ello se utiliza un sub-

86

Compiladores e intrpretes: teora y prctica

conjunto de las reglas que forman la gramtica del lenguaje fuente, que debern poder representarse como expresiones regulares. A partir de estas expresiones regulares, es posible obtener un AFND que acepta el lenguaje generado por ellas y, en una segunda etapa, el AFD mnimo equivalente. Utilizando la funcin de transicin y los estados finales de este AFD, es posible implementar el autmata que actuar como analizador morfolgico. Se describen tambin otras tareas auxiliares llevadas a cabo por el analizador morfolgico, como la eliminacin de ciertos caracteres delimitadores (espacios en blanco, tabuladores y saltos de lnea), la eliminacin de comentarios y el clculo de los valores para algunos atributos semnticos de las unidades sintcticas. Se revisa tambin el tipo de errores que puede detectar el analizador morfolgico, y cmo puede comportarse ante ellos. Por ltimo, se estudia con detalle la herramienta lex, para la generacin automtica de analizadores morfolgicos, y se describe el fichero de especificacin que requiere como entrada, as como el funcionamiento del analizador morfolgico que genera como salida.

3.11 Ejercicios
3.1. Construir una gramtica que represente el lenguaje de los nmeros en punto flotante del tipo [-][cifras][.[cifras]][e[-][cifras]]. Debe haber al menos una cifra en la parte entera o en la parte decimal, as como en el exponente, si lo hay. Construir un autmata finito determinista que reconozca el lenguaje del Ejercicio 3.1. Construir una gramtica que represente el lenguaje de las cadenas de caracteres correctas en C. Construir un autmata finito determinista que reconozca el lenguaje del Ejercicio 3.3. En el lenguaje APL, una cadena de caracteres viene encerrada entre dos comillas simples. Si la cadena de caracteres contiene una comilla, sta se duplica. Construir una gramtica regular que describa el lenguaje de las cadenas de caracteres vlidas en APL. Construir un autmata finito determinista que reconozca el lenguaje del Ejercicio 3.5. Construir un autmata finito determinista que reconozca los caracteres en el lenguaje C. Ejemplos vlidos: a, \n, \033 (cualquier nmero de cifras). Ejemplos incorrectos: \. Se desea realizar un compilador para un lenguaje de programacin que manejar como tipo de dato vectores de enteros. Los vectores de enteros se representarn como una lista de nmeros enteros separados por comas. El vector ms pequeo slo tendr un nmero y, en este caso, no aparecer coma alguna. A continuacin se muestran algunos ejemplos: {23}, {1,210,5,0,09}. 3.8.1. Disear una gramtica para representar este tipo de datos. 3.8.2. Indicar qu parte de ella sera adecuado que fuera gestionada por el analizador morfolgico del compilador. Justificar razonadamente la respuesta.

3.2. 3.3. 3.4. 3.5.

3.6. 3.7. 3.8.

Captulo 3. Anlisis morfolgico

87

(c) 3.9.

Para cada una de las unidades sintcticas, especificar una expresin regular que la represente (puede usarse la notacin de lex).

En un lenguaje de programacin los nombres de las variables deben comenzar con la letra V y terminar con un dgito entre el 0 y el 9. Entre estos dos smbolos puede aparecer cualquier letra mayscula o minscula. Las constantes numricas son nmeros reales positivos que deben tener obligatoriamente las siguientes partes: parte entera (una cadena de cualquier cantidad de dgitos entre 0 y 9), separador (,), parte fraccionaria (con la misma sintaxis que la parte entera). Algunos ejemplos de expresiones correctas son las siguientes: Variable1, +Variable1 4,04, (log +V2(sen 4,54)). Especificar las expresiones regulares con notacin lex que podra utilizar un analizador morfolgico para representar las unidades sintcticas para los nombres de las variables y las constantes numricas.

3.12 Bibliografa
[1] Alfonseca, M.; Sancho, J., y Martnez Orga, M. (1997): Teora de Lenguajes, Gramticas y Autmatas, Madrid, Promo-Soft, Publicaciones R.A.E.C.

Captulo

Anlisis sintctico

Este captulo describe algunos de los diversos mtodos que suelen utilizarse para construir los analizadores sintcticos de los lenguajes independientes del contexto. Recordemos que el analizador sintctico o parser es el corazn del compilador o intrprete y gobierna todo el proceso. Su objetivo es realizar el resto del anlisis (continuando el trabajo iniciado por el analizador morfolgico) para comprobar que la sintaxis de la instruccin en cuestin es correcta. Para ello, el analizador sintctico considera como smbolos terminales las unidades sintcticas devueltas por el analizador morfolgico. Existen dos tipos principales de anlisis: Descendente o de arriba abajo (top-down, en ingls). Se parte del axioma S y se va realizando la derivacin S*x. La cadena x (que normalmente corresponde a una instruccin o un conjunto de instrucciones) se llama meta u objetivo del anlisis. La primera fase del anlisis consiste en encontrar, entre las reglas cuya parte izquierda es el axioma, la que conduce a x. De esta manera, el rbol sintctico se va construyendo de arriba abajo, tal como indica el nombre de este tipo de anlisis. En este captulo se explicar con detalle un mtodo de anlisis de arriba abajo: el que se basa en el uso de gramticas LL(1). Ascendente o de abajo arriba (bottom-up, en ingls). Se parte de la cadena objetivo x y se va reconstruyendo en sentido inverso la derivacin S*x. En este caso, la primera fase del anlisis consiste en encontrar, en la cadena x, el asidero (vase la Seccin 1.9.7), que es la parte derecha de la ltima regla que habra que aplicar para reducir S a x. De esta manera, el rbol sintctico se va construyendo de abajo arriba, tal como indica el nombre de este tipo de anlisis. En este captulo se explicarn con detalle los siguientes mtodos de anlisis ascendente: LR(0), SLR(1), LR(1), LALR(1) (estos dos ltimos son los ms generales, pues permiten analizar la sintaxis de cualquier lenguaje independiente del contexto), as como el que utiliza gramticas de precedencia simple, el menos general de todos, pues slo se aplica a len-

90

Compiladores e intrpretes: teora y prctica

guajes basados en el uso de expresiones, pero que permite obtener mejores eficiencias en esos casos. Como se dijo en la Seccin 1.4 y en la Figura 1.1, la mquina apropiada para el anlisis de los lenguajes independientes del contexto es el autmata a pila. Esto explica que, aunque en los mtodos de anlisis revisados en este captulo el autmata pueda estar ms o menos oculto, en todos ellos se observa la presencia de una pila. Por otra parte, el hecho de que un lenguaje sea independiente del contexto (que su gramtica sea del tipo 2 de Chomsky) no siempre asegura que el autmata a pila correspondiente resulte ser determinista. Los autmatas a pila deterministas slo son capaces de analizar un subconjunto de los lenguajes independientes del contexto. A lo largo de las pginas siguientes, se impondr diversas restricciones a las gramticas, en funcin del mtodo utilizado. Las restricciones exigidas por un mtodo no son las mismas que las que exige otro, por lo que los distintos mtodos se complementan, lo que permite elegir el mejor o el ms eficiente para cada caso concreto. En este captulo, es necesario introducir smbolos especiales que sealen el principio o el fin de las cadenas que se van a analizar. Cuando slo hace falta aadir un smbolo final, se utilizar el smbolo $, pues es poco probable encontrarlo entre los smbolos terminales de la gramtica. Cuando hace falta sealar los dos extremos de la cadena, se utilizarn los smbolos y para el principio y el final, respectivamente.

4.1 Conjuntos importantes en una gramtica


Sea una gramtica limpia G = (T, N, S, P). Sea = T N. Sea un smbolo de esta gramtica (terminal o no terminal). Se definen los siguientes conjuntos asociados a estas gramticas y a este smbolo: Si XN, primero(X) = {V | X + Vx, VT, x*} Si XT, primero(X) = {X} Es decir, si X es terminal, primero(X) contiene slo a X; en caso contrario, primero(X) es el conjunto de smbolos terminales que pueden aparecer al principio de alguna forma sentencial derivada a partir de X. siguiente(X) = {V | S + xXVy, XN, VT, x,y*}, donde S es el axioma de la gramtica. Es decir, si X es un smbolo no terminal de la gramtica, siguiente(X) es el conjunto de smbolos terminales que pueden aparecer inmediatamente a la derecha de X en alguna forma sentencial. Si X puede aparecer en el extremo derecho de alguna forma sentencial, entonces el smbolo de fin de cadena $ siguiente(X). Para calcular el conjunto primero(X) X N T, se aplicarn las siguientes reglas, hasta que no se puedan aadir ms smbolos terminales ni a dicho conjunto. (R1) Si X T, se hace primero(X)={X}. (R2) Si X ::= P, se aade a primero(X).

Captulo 4. Anlisis sintctico

91

(R3) Si X ::= Y1 Y2 ... Yk P, se aade primero(Yi){} a primero(X) para i=1,2,,j, donde j es el primer subndice tal que Yj no genera la cadena vaca (Yj es terminal, o siendo no terminal no ocurre que Yj ). (R4) Si X ::= Y1 Y2 ... Yk P y Yj j {1,2,...,k}, se aade a primero(X). Ejemplo Consideremos la gramtica siguiente, en la que E es el axioma: 4.1 (1) E ::= TE (2) E ::= +TE (3) E ::= (4) T ::= FT (5) T ::= *FT (6) T ::= (7) F ::= (E) (8) F ::= id En esta gramtica, el conjunto primero(E) resulta ser igual al conjunto primero(T) aplicando la regla (R3) a la regla (1). Para calcular el conjunto primero(T) se aplica la regla (R3) a la regla (4), de la que se obtiene que primero(T) es igual a primero(F). Para calcular primero(F) se aplica la regla (R3) a la regla (7), aadiendo el conjunto primero((), que es igual a {(} por la regla (R1). Al aplicar la regla (R3) a la regla (8), se aade tambin el conjunto primero(id), que es igual a {id} por la regla (R1). Por tanto: primero(E)=primero(T)=primero(F)={(, id} A continuacin se calcula el conjunto primero(E), aplicando, en primer lugar, la regla (R3) a la regla (2), que aade el conjunto primero(+), que es igual a {+} por la regla (R1). Al aplicar la regla (R2) a la regla (3), se aade tambin . Por tanto, primero(E)={+, } De una forma parecida, se calcula el conjunto primero(T). Al aplicar la regla (R3) a la regla (5) se aade el conjunto primero(*), que es igual a {*} por la regla (R1), y aplicando la regla (R2) a la regla (6), se aade . Por tanto, primero(T)={*, } En alguno de los algoritmos de anlisis sintctico descritos en este captulo ser necesario extender la definicin del conjunto primero para que se aplique a una forma sentencial, lo que se har de la siguiente manera: sea G =(T, N, S, P) una gramtica independiente del contexto. Si es una forma sentencial de la gramtica, (N T)*; es decir, si puede obtenerse por derivacin, a partir del axioma S, en cero o ms pasos, aplicando reglas de P, llamaremos primero() al conjunto de smbolos terminales que pueden aparecer en primer lugar en las cadenas derivadas a partir de . Si desde se puede derivar la cadena vaca , sta tambin pertenecer a primero().

92

Compiladores e intrpretes: teora y prctica

Sea =X1X2...Xn. Para calcular primero(), se aplicar el siguiente algoritmo hasta que no se puedan aadir ms smbolos terminales o a dicho conjunto: Aadir a primero() todos los smbolos de primero(X1), excepto . Si primero(X1) contiene , aadir a primero() todos los smbolos de primero(X2), excepto . Si primero(X1) y primero(X2) contienen , aadir a primero() todos los smbolos de primero(X3), excepto . Y as sucesivamente. Si i {1,2,..,n} primero(Xi) contiene , entonces aadir a primero(). En la gramtica del Ejemplo 4.1, para calcular el conjunto primero(TEid) se calcula primero(T), por lo que se aadir {*}. Como primero(T) contiene , es necesario aadir tambin primero(E), por lo que se aade {+}. Como primero(E) tambin contiene , es necesario aadir tambin primero(id), que es igual a {id}. Por tanto: primero(TEid) = {*,+,id} Para calcular el conjunto siguiente(X) X N, deben aplicarse las siguientes reglas, hasta que no se puedan aadir ms smbolos terminales a dicho conjunto. (R1) Para el axioma S, aadir $ a siguiente(S). (R2) Si A::=X P, aadir todos los smbolos (excepto ) de primero() a siguiente(X). (R3) Si A::=X P y primero(), aadir todos los smbolos de siguiente(A) a siguiente(X). (R4) Si A::=X P, aadir siguiente(A) a siguiente(X). Como ejemplo se considerar la gramtica del Ejemplo 4.1. Se tendr que: siguiente(E)={$,)} El smbolo $ se aade al aplicar la regla (R1), y el smbolo ) al aplicar la regla (R2) a la regla (7). Al aplicar la regla (R4) a las reglas (1) y (2), el conjunto siguiente(E) resulta ser igual al conjunto siguiente(E), ya que la afirmacin siguiente(E)= siguiente(E) deducida de la regla (2) no aade ningn smbolo nuevo. Por tanto: siguiente(E)={$,)} Para calcular siguiente(T) se aplica la regla (R2) a las reglas (1) y (2) y se aade el smbolo + por pertenecer al conjunto primero(E). Como primero(E), se aplica la regla (R3) a la regla (1) y se aaden tambin los smbolos ) y $ por pertenecer a siguiente(E). Por tanto: siguiente(T)={+,$,)}

Captulo 4. Anlisis sintctico

93

Al aplicar la regla (R4) a la regla (4), siguiente(T) resulta ser igual a siguiente(T). Por tanto: siguiente(T)={+,$,)} Por ltimo, se calcula siguiente(F). Al aplicar la regla (R2) a las reglas (4) y (5), se aade el smbolo * por pertenecer a primero(T). Como primero(T), se aplica la regla (R3) a la regla (4) y se aaden los smbolos +, $ y ) por pertenecer a siguiente(T). Al aplicar la regla (R3) a la regla (5), deberan aadirse los smbolos +, $ y ) por pertenecer a siguiente(T), pero no se hace porque ya pertenecen al conjunto. Por tanto: siguiente(F)={*,+,$,)}

4.2 Anlisis sintctico descendente


Como se menciona en la introduccin de este captulo, para realizar el anlisis sintctico descendente de una palabra x, se parte del axioma S y se va realizando la derivacin S*x. Un mtodo posible para hacerlo es el anlisis descendente con vuelta atrs, que consiste en probar sistemticamente todas las alternativas hasta llegar a la reduccin buscada o hasta que se agoten dichas alternativas. La ineficiencia de este mtodo se soluciona si, en cada momento del proceso de construccin de la derivacin, slo se puede aplicar una regla de la gramtica. Esta idea es el origen de las gramticas LL(1) y del mtodo de anlisis descendente selectivo que se aplica a este tipo de gramticas.

4.2.1. Anlisis descendente con vuelta atrs


En una gramtica dada, sea S el axioma y sean las reglas cuya parte izquierda es el axioma: S ::= X1 X2 ... Xn | Y1 Y2 ... Ym | ... Sea x la palabra que se va a analizar. El objetivo del anlisis es encontrar una derivacin tal que S * x En el mtodo de anlisis descendente con vuelta atrs se prueba primero la regla S ::= X1 X2 ... Xn Para aplicarla, es necesario descomponer x de la forma x = x1 x2 ... xn y tratar de encontrar las derivaciones X1 * x1 X2 * x2 ... Xn * xn

94

Compiladores e intrpretes: teora y prctica

Cada una de las derivaciones anteriores es una submeta. Pueden ocurrir los siguientes casos: (Caso 1) Xi=xi: submeta reconocida. Se pasa a la submeta siguiente. (Caso 2) Xixi y Xi es un smbolo terminal: submeta rechazada. Se intenta encontrar otra submeta vlida para Xi-1. Si i=1, se elige la siguiente parte derecha para el mismo smbolo no terminal a cuya parte derecha pertenece Xi. Si ya se han probado todas las partes derechas, se elige la siguiente parte derecha del smbolo no terminal del nivel superior. Si ste es el axioma, la cadena x queda rechazada. (Caso 3) Xi es un smbolo no terminal. Se buscan las reglas de las que Xi es parte izquierda: Xi ::= Xi1 Xi2 ... Xin | Yi1 Yi2 ... Yim | ... se elige la primera opcin: Xi ::= Xi1 Xi2 ... Xin se descompone xi en la forma xi = xi1 xi2 ... xin lo que da las nuevas submetas Xi1 * xi1 Xi2 * xi2 ... Xin * xin y continuamos recursivamente de la misma forma. El proceso termina cuando en el Caso 2 no hay ms reglas que probar para el axioma (en cuyo caso la cadena no es reconocida) o cuando se reconocen todas las submetas pendientes (en cuyo caso la cadena s es reconocida). Ejemplo Consideremos la gramtica siguiente: 4.2 S ::= aSb S ::= a Sea aabb la palabra a reconocer. Se prueba primero la regla S ::= aSb y se intenta encontrar las siguientes derivaciones: (S1) a * a (S2) S * ab (S3) b * b La Figura 4.1 ilustra este primer paso. Las derivaciones primera y tercera corresponden a submetas reconocidas de acuerdo con el Caso 1. La segunda derivacin corresponde al Caso 3, por lo que se elige la regla S ::= aSb, por ser la primera regla en cuya parte izquierda aparece S. Se obtienen dos nuevas submetas: a * a S * b

Captulo 4. Anlisis sintctico

95

Figura 4.1. Construccin de una derivacin para la palabra aabb: paso 1.

Como puede apreciarse en la Figura 4.2, la primera derivacin corresponde a una submeta reconocida de acuerdo con el Caso 1. La segunda derivacin corresponde al Caso 3, por lo que se elige la regla S ::= aSb, por ser la primera regla en cuya parte izquierda aparece S. Se obtiene una nueva submeta: a * b

Figura 4.2. Construccin de una derivacin para la palabra aabb: paso 2.

Esta derivacin aparece en la Figura 4.3 y corresponde a una submeta rechazada, de acuerdo con el Caso 2. Como el smbolo a es el primer smbolo en la parte derecha de la regla S ::= aSb,

96

Compiladores e intrpretes: teora y prctica

Figura 4.3. Construccin de una derivacin para la palabra aabb: paso 3.

se elige la siguiente parte derecha para el smbolo S, es decir, S ::= ab. Como ilustra la Figura 4.4, se obtiene una nueva submeta: a * b

Figura 4.4. Construccin de una derivacin para la palabra aabb: paso 4.

Captulo 4. Anlisis sintctico

97

Esta derivacin tambin corresponde a una submeta rechazada, de acuerdo con el Caso 2. Como ya no hay ms reglas para el smbolo S, se vuelve a la submeta (S2) y se elige la siguiente parte derecha para S, es decir, S ::= ab. Se obtienen dos nuevas submetas: a * a b * b

Figura 4.5. rbol de derivacin para la palabra aabb.

Ambas derivaciones corresponden a submetas reconocidas de acuerdo con el Caso 1. Como se han encontrado todas las derivaciones, se reconoce la cadena aabb. La Figura 4.5 muestra el rbol de derivacin para la palabra aabb. Como habr podido observarse, este mtodo de anlisis puede ser bastante ineficiente, porque el nmero de submetas que hay que comprobar puede ser bastante elevado. Una forma de optimizar el procedimiento consiste en ordenar las partes derechas de las reglas que comparten la misma parte izquierda, de manera que la regla buena se analice antes. Una buena estrategia para ordenar las partes derechas es poner primero las de mayor longitud. Si una parte derecha es la cadena vaca, debe ser la ltima. Si se llega a ella, la submeta tiene xito automticamente. A esta variante del mtodo se la denomina anlisis descendente con vuelta atrs rpida. Como ejemplo, consideremos la siguiente gramtica: E ::= T+E | T T ::= F*T | F F ::= i

98

Compiladores e intrpretes: teora y prctica

Figura 4.6. Construccin de una derivacin para la palabra i*i: paso 1.

Se analiza la cadena i*i. Se prueba primero E ::= T+E y se obtiene el rbol de la Figura 4.6. En cuanto se compruebe que el signo + no pertenece a la cadena objetivo, no es necesario volver a las fases precedentes del anlisis, tratando de obtener otra alternativa, sino que se puede pasar directamente a probar E::=T. El rbol de derivacin obtenido aparece en la Figura 4.7.
E

Figura 4.7. rbol de derivacin para la palabra i*i.

Captulo 4. Anlisis sintctico

99

4.2.2. Anlisis descendente selectivo


En el mtodo ptimo de anlisis descendente, en cada etapa de construccin del rbol de derivacin slo debera ser posible aplicar una regla. Este mtodo de anlisis se llama anlisis descendente selectivo, anlisis sin vuelta atrs o descenso recursivo, y las gramticas compatibles con l reciben el nombre de gramticas LL(1). Esto puede conseguirse, por ejemplo, si en la gramtica que se est utilizando para el anlisis, las partes derechas de las reglas que tienen la misma parte izquierda empiezan por un smbolo terminal distinto. Las siglas LL(1) se refieren a una familia de analizadores sintcticos descendentes en los que la entrada se lee desde la izquierda Left en ingls, las derivaciones en el rbol se hacen de izquierda Left a derecha, y en cada paso del anlisis se necesita conocer slo un 1 smbolo de la entrada. Existen varias aproximaciones a este tipo de anlisis, lo que da lugar a definiciones diferentes, no siempre equivalentes, de las gramticas LL(1). En este captulo se presentarn dos de ellas: La que se basa en el uso de la forma normal de Greibach. La que se basa en el uso de tablas de anlisis.

4.2.3. Anlisis LL(1) mediante el uso de la forma normal de Greibach


Se dice que una gramtica est en forma normal de Greibach si todas las producciones tienen la forma A ::= ax, donde A N, a T, x N*, es decir, si la parte derecha de todas las reglas empieza por un smbolo terminal, seguido opcionalmente por smbolos no terminales. De acuerdo con esta definicin, una gramtica LL(1) es una gramtica en forma normal de Greibach en la que no existen dos reglas con la misma parte izquierda, cuya parte derecha empiece por el mismo smbolo terminal. Es decir, una gramtica LL(1) cumple dos condiciones: La parte derecha de todas las reglas empieza por un smbolo terminal, seguido opcionalmente por smbolos no terminales. No existen dos reglas con la misma parte izquierda, cuya parte derecha empiece por el mismo smbolo terminal. Como ejemplo, la siguiente gramtica es LL(1), porque la parte derecha de todas las reglas empieza por un smbolo terminal, seguido opcionalmente por smbolos no terminales, y las dos reglas cuya parte izquierda es la misma (smbolo R) empiezan por un smbolo terminal distinto. F ::= iRP R ::= aAC | bZ C ::= c P ::= p

100

Compiladores e intrpretes: teora y prctica

Se llama LL(1) a este tipo de gramticas, porque en cada momento basta estudiar un carcter de la cadena objetivo para saber qu regla se debe aplicar. Eliminacin de la recursividad a izquierdas. Para convertir una gramtica en otra equivalente en forma normal de Greibach, es preciso eliminar las reglas recursivas a izquierdas, si las hay. Una regla es recursiva a izquierdas si tiene la forma A ::= Ax, donde x *. Sea la gramtica independiente del contexto G = (T, N, S, P), donde P contiene las reglas A ::= A1 | A2 | ... | An | 1 | 2 | ... | m Se construye la gramtica G = (T, N {X}, S, P), donde P se obtiene reemplazando en P las reglas anteriores por las siguientes: A ::= 1X | 2X | ... | mX X ::= 1X | 2X | ... | nX | En P ya no aparecen reglas recursivas a izquierdas y puede demostrarse que L(G) = L(G). Ejemplo Consideremos la gramtica siguiente: 4.3 E ::= E + T | T T ::= T * F | F F ::= i Para eliminar de esta gramtica las reglas recursivas a izquierdas, se empieza por aquellas en cuya parte izquierda aparece el smbolo E. En este caso 1 = +T y 1 = T. Por lo tanto, deben reemplazarse las dos primeras reglas de la gramtica por las siguientes: E ::= TX X ::= +TX | Si se aplica la misma transformacin a las reglas en cuya parte izquierda aparece el smbolo T (1 = *F y 1 = F), se obtienen las reglas: T ::= FY Y ::= *FY | Despus de eliminar la recursividad a izquierdas, se obtiene la siguiente gramtica equivalente a la original: E ::= TX X ::= +TX | T ::= FY Y ::= *FY | F ::= i Eliminacin de smbolos no terminales iniciales. El algoritmo que se va a describir a continuacin tiene por objeto eliminar las reglas que empiezan por un smbolo no terminal. Para ello, hay que establecer una relacin de orden parcial en N = {A1, A2, ..., An} de la siguiente

Captulo 4. Anlisis sintctico

101

forma: Ai precede a Aj si P contiene al menos una regla de la forma Ai::=Aj, donde *. Si existen bucles de la forma Ai::=Aj, Aj::=Ai, se elige un orden arbitrario entre los smbolos no terminales Ai y Aj. En la gramtica obtenida en el Ejemplo 4.3, puede establecerse la relacin de orden E < T < F. Despus de definir la relacin de orden, se clasifican las reglas de P en tres grupos: 1. Reglas de la forma A ::= ax, donde a T, x *. 2. Reglas de la forma Ai ::= Ajx, donde Ai < Aj y x *. 3. Reglas de la forma Ai ::= Ajx, donde Ai > Aj y x *. En la gramtica obtenida en el Ejemplo 4.3, todas las reglas son de tipo 1, excepto las reglas E ::= TX y T ::= FY, que son de tipo 2. Para obtener una gramtica en forma normal de Greibach se debe eliminar las reglas de tipo 2 y 3. Para eliminar una regla de la forma Ai ::= Ajx, basta sustituir Aj en dicha regla por todas las partes derechas de las reglas cuya parte izquierda es Aj. Se eliminan primero las reglas de tipo 3. Si existen varias reglas de este tipo, se trata primero aquella cuya parte izquierda aparece antes en la relacin de orden establecida. A continuacin, se eliminan las reglas de tipo 2. Si existen varias reglas de este tipo, se trata primero aquella cuya parte izquierda aparece ms tarde en la relacin de orden establecida. Si durante este proceso de eliminacin aparecen de nuevo reglas recursivas a izquierdas, se eliminan aplicando el procedimiento descrito anteriormente. Si aparecieran smbolos inaccesibles, tambin deberan eliminarse (vase la Seccin 1.12.2). Ejemplo En la gramtica obtenida en el Ejemplo 4.3, no hay reglas de tipo 3, por lo que slo hay que eliminar las de tipo 2: E ::= TX y T ::= FY. Como T aparece detrs de E en la relacin de orden, 4.4 se elimina primero la regla cuya parte izquierda es T, y se obtiene la siguiente gramtica: E ::= TX X ::= +TX | T ::= iY Y ::= *FY | F ::= i Despus de eliminar la regla en cuya parte izquierda aparece E, se obtiene la siguiente gramtica: E ::= iYX X ::= +TX | T ::= iY Y ::= *FY | F ::= i De acuerdo con la definicin dada anteriormente, en una gramtica en forma normal de Greibach no pueden aparecer reglas-, es decir, reglas cuya parte derecha es el smbolo . Sin

102

Compiladores e intrpretes: teora y prctica

embargo, como el objetivo es obtener una gramtica a la que se pueda aplicar el mtodo de anlisis descendente selectivo, se permite una regla- A ::= si se cumple que primero(A) siguiente(A) = . Veamos si las reglas- que aparecen en la gramtica obtenida en el Ejemplo 4.4 pueden permanecer en la gramtica. Para la regla X::= se cumple que primero(X) = {+, } y siguiente(X) = {$}. La interseccin de estos dos conjuntos es el conjunto vaco, por lo que la regla X::= puede permanecer en la gramtica. Para la regla Y::= se cumple que primero(Y) = {*, } y siguiente(Y) = {+,$}. La interseccin de estos dos conjuntos es el conjunto vaco, por lo que la regla Y::= puede permanecer en la gramtica. Si una regla- A::= no cumple la condicin indicada, a veces es posible eliminarla, aplicando el siguiente procedimiento. 1. Eliminar la regla. 2. Por cada aparicin de A en la parte derecha de una regla, aadir una nueva regla en la que se elimina dicha aparicin. Por ejemplo, si se quiere eliminar la regla A::= de una gramtica en la que aparece la regla B::= uAvAw, deben aadirse las siguientes reglas: B::= uvAw B::= uAvw B::= uvw En la gramtica obtenida en el Ejemplo 4.4, las partes derechas de todas las reglas empiezan por un smbolo terminal, seguido opcionalmente por smbolos no terminales. Puede ser que, como resultado del proceso anterior, no todos los smbolos que siguen al terminal inicial sean no terminales. En tal caso, es necesario eliminar los smbolos terminales no situados al comienzo de la parte derecha de una regla. El procedimiento que se debe aplicar en este caso es trivial. Sea, por ejemplo, la regla A ::= abC. Para eliminar el smbolo b basta reemplazar esta regla por las dos siguientes: A ::= aBC B ::= b Ejemplo Consideremos la gramtica siguiente: 4.5 A ::= Ba | a B ::= Ab | b Inicialmente no hay ninguna regla recursiva a izquierdas. Existen dos relaciones de orden posibles: A < B (por la regla A ::= Ba) y B < A (por la regla B ::= Ab). En este caso, elegiremos arbitrariamente el orden: B < A. La clasificacin de las reglas de acuerdo con este orden es la siguiente: A ::= Ba A ::= a B ::= Ab B ::= b tipo 3 tipo 1 tipo 2 tipo 1

Captulo 4. Anlisis sintctico

103

Se elimina primero la nica regla de tipo 3 que aparece en la gramtica: A ::= Ba. El resultado es el siguiente: A ::= Aba A ::= ba A ::= a B ::= Ab B ::= b recursiva a izquierdas tipo 1 tipo 1 tipo 2 tipo 3

En la gramtica resultante, el smbolo no terminal B es inaccesible, porque no aparece en la parte derecha de ninguna regla, por lo que podemos eliminar las reglas en las que aparece como parte izquierda. El resultado es el siguiente: A ::= Aba A ::= ba A ::= a recursiva a izquierdas tipo 1 tipo 1

Despus de eliminar la regla recursiva a izquierdas, el resultado es el siguiente: A ::= baX A ::= aX X ::= baX X ::= tipo 1 tipo 1 tipo 1 tipo 1

Para que esta gramtica est en forma normal de Greibach, la parte derecha de todas las reglas debe constar de un smbolo terminal seguido de smbolos no terminales. Para ello se deben transformar las reglas 1 y 3 con el siguiente resultado final: A ::= bZX A ::= aX X ::= bZX X ::= Z ::= a El siguiente paso es analizar si la regla X ::= puede permanecer en la gramtica. Para ello, deben calcularse los conjuntos primero(X) = {b,} y siguiente(X) = {$}. Como la interseccin de estos conjuntos es el conjunto vaco, la regla X ::= puede permanecer en la gramtica. Para que una gramtica en forma normal de Greibach sea una gramtica LL(1), debe cumplir que no existan dos reglas con la misma parte izquierda, cuya parte derecha empiece por el mismo smbolo terminal. La gramtica en forma normal de Greibach obtenida para el Ejemplo 4.5 cumple esta condicin, porque las dos reglas con el smbolo A en su parte izquierda empiezan por dos smbolos terminales distintos: a y b. La gramtica obtenida en el Ejemplo 4.4 tambin cumple esta condicin. Si no fuese ste el caso, el procedimiento para conseguir que dicha condicin se cumpla es bastante sencillo. Sea la siguiente gramtica:

104

Compiladores e intrpretes: teora y prctica

U ::= aV | aW V ::= bX | cY W ::= dZ | eT Para que las reglas con el smbolo U en su parte izquierda cumplan la condicin LL(1), se hace algo parecido a sacar factor comn, reemplazando las dos reglas con parte izquierda U por U ::= aK K ::= V | W Ahora las reglas de parte izquierda K no estn en forma normal de Greibach. Para que lo estn, se sustituyen los smbolos V y W por las partes derechas de sus reglas. U ::= aK K ::= bX | cY | dZ | eT El resultado es una gramtica LL(1). Hay que tener en cuenta que estas operaciones no siempre dan el resultado apetecido, pues no toda gramtica independiente del contexto puede ponerse en forma LL(1).

Una funcin para cada smbolo no terminal


En una gramtica LL(1), los smbolos no terminales se clasifican en dos grupos: los que tienen reglas- y los que no. A cada smbolo no terminal de la gramtica se le hace corresponder una funcin que realizar la parte del anlisis descendente correspondiente a dicho smbolo. La Figura 4.8 muestra la funcin escrita en C que corresponde a un smbolo no terminal sin regla-, para el que la gramtica contiene las siguientes reglas: U::=x X1 X2...Xn | y Y1 Y2...Ym |...| z Z1 Z2...Zp Esta funcin recibe dos parmetros: la cadena que se va a analizar y un contador que indica una posicin dentro de dicha cadena. Si el valor del segundo argumento es un nmero negativo, la funcin termina y devuelve dicho valor. La funcin consta de una instruccin switch, con un caso para cada una de las partes derechas de las reglas cuya parte izquierda es el smbolo no terminal para el que se implementa la funcin. En todos los casos se incrementa el valor del contador y se invocan las funciones de los smbolos no terminales que aparecen en la parte derecha correspondiente. Se aade un caso por defecto, que se ejecuta si ninguna de las partes derechas es aplicable al carcter de la cadena que se est examinando, en el que se devuelve un nmero negativo, para indicar que no se reconoce la palabra. Este nmero negativo puede ser distinto para cada funcin, lo que servir para identificar cul es la funcin que ha generado el error. Para el resto de los casos, se devuelve el valor final del contador. La Figura 4.9 muestra la funcin escrita en C que corresponde a un smbolo no terminal con regla-, para el que la gramtica contiene las siguientes reglas: U ::= x X1 X2 ... Xn | ... | z Z1 Z2 ... Zp |

Captulo 4. Anlisis sintctico

105

int U (char *cadena, int i) { if (i<0) return i; switch (cadena[i]) { case x: i++; i = X1 (cadena, i); i = X2 (cadena, i); . . . i = Xn (cadena, i); break; case y: i++; i = Y1 (cadena, i); i = Y2 (cadena, i); . . . i = Ym (cadena, i); break; case Z: i++; i = Z1 (cadena, i); i = Z2 (cadena, i); . . . i = Zp (cadena, i); break; default: return -n; } return i; }
Figura 4.8. Funcin para un smbolo no terminal sin regla-.

La nica diferencia con la funcin que aparece en la Figura 4.8 es que en la instruccin switch no se incluye el caso por defecto, porque la aplicacin de la regla- significa que la funcin correspondiente al smbolo no terminal debe devolver el mismo valor del contador que recibi, es decir, no debe avanzar en la cadena de entrada.

106

Compiladores e intrpretes: teora y prctica

int U (char *cadena, int { if (i<0) return i; switch (cadena[i]) { case x: i++; i = X1 (cadena, i = X2 (cadena, . . . i = Xn (cadena, break; . . . case Z: i++; i = Z1 (cadena, i = Z2 (cadena, . . . i = Zp (cadena, break; } return i; }

i)

i); i); i);

i); i); i);

Figura 4.9. Funcin para un smbolo no terminal con regla-.

Ejemplo Consideremos la gramtica siguiente: 4.6 E ::= T + E E ::= T E E ::= T T ::= F * T T ::= F / T T ::= F F ::= i F ::= (E) En forma normal de Greibach, la gramtica queda as: E ::= iPTME |(ECPTME | iDTME | iPTSE |(ECPTSE | iDTSE | iPT | (ECPT | iDT T ::= iPT | (ECPT | iDT | (ECDTME | iME | (ECDTSE | iSE | (ECDT | i | (ECDT | i | (ECME | (ECSE | (EC | (EC

Captulo 4. Anlisis sintctico

107

F ::= i | (EC M ::= + S ::= P ::= * D ::= / C ::= ) A partir de esta gramtica, se obtiene la siguiente gramtica LL(1): E ::= iV | (ECV V ::= *TX | /TX | +E | -E | X ::= +E | -E | T ::= iU | (ECU U ::= *T | /T | F ::= i | (EC C ::= ) Las funciones correspondientes a los siete smbolos no terminales que aparecen en esta gramtica se muestran en las Figuras 4.10 a 4.16.

int E (char *cadena, int i) { if (i<0) return i; switch (cadena[i]) { case i: i++; i = V (cadena, i); break; case (: i++; i = E (cadena, i); i = C (cadena, i); i = V (cadena, i); break; default: return -1; } return i; }
Figura 4.10. Funcin para el smbolo no terminal E.

108

Compiladores e intrpretes: teora y prctica

int V (char *cadena, int i) { if (i<0) return i; switch (cadena[i]) { case *: case /: i++; i = T (cadena, i); i = X (cadena, i); break; case +: case -: i++; i = E (cadena, i); break; } return i; }
Figura 4.11. Funcin para el smbolo no terminal V.

int X (char *cadena, int i) { if (i<0) return i; switch (cadena[i]) { case +: case -: i++; i = E (cadena, i); break; } return i; }
Figura 4.12. Funcin para el smbolo no terminal X.

Captulo 4. Anlisis sintctico

109

int T (char *cadena, int i) { if (i<0) return i; switch (cadena[i]) { case i: i++; i = U (cadena, i); break; case (: i++; i = E (cadena, i); i = C (cadena, i); i = U (cadena, i); break; default: return -2; } return i; }
Figura 4.13. Funcin para el smbolo no terminal T.

int U (char *cadena, int i) { if (i<0) return i; switch (cadena[i]) { case *: case /: i++; i = T (cadena, i); break; } return i; }
Figura 4.14. Funcin para el smbolo no terminal U.

110

Compiladores e intrpretes: teora y prctica

int F (char *cadena, int i) { if (i<0) return i; switch (cadena[i]) { case i: i++; break; case (: i++; i = E (cadena, i); i = C (cadena, i); break; default: return -3; } return i; }
Figura 4.15. Funcin para el smbolo no terminal F.

int C (char *cadena, int i) { if (i<0) return i; switch (cadena[i]) { case ): i++; break; default: return -4; } return i; }
Figura 4.16. Funcin para el smbolo no terminal C.

Anlisis de cadenas
Para analizar una cadena x con este mtodo, basta invocar la funcin correspondiente al axioma de la gramtica con los argumentos x (la cadena a analizar) y 0 (el valor inicial del contador). En el ejemplo, la llamada sera E(x,0). Si el valor devuelto por la funcin coincide con la longitud de la cadena de entrada, la cadena queda reconocida. En caso contrario, la funcin devolver un nmero negativo, que indica el error detectado. La Figura 4.17 muestra el anlisis de la palabra i+i*i. La llamada a la funcin correspondiente al axioma de la gramtica devuelve el valor 5, que coincide con la longitud de la cadena de entrada, por lo que la palabra es reconocida.

Captulo 4. Anlisis sintctico

111

E (i+i*i, 0) = = = = = = =

V E V X X X 5

(i+i*i, (i+i*i, (i+i*i, (i+i*i, (i+i*i, (i+i*i,

1) = 2) = 3) = T (i+i*i, 4)) = U (i+i*i, 5)) = 5) =

Figura 4.17. Anlisis de la cadena i+i*i.

Sin embargo, como ilustra la Figura 4.18, el anlisis de la palabra i+i* devuelve el valor 2, lo que indica que no se reconoce la palabra, y que el error lo devolvi la llamada a la funcin correspondiente al smbolo no terminal T.

E (i+i*, 0) = = = = =

V (i+i*, E (i+i*, V (i+i*, X (i+i*, -2

1) = 2) = 3) = T (i+i*, 4)) =

Figura 4.18. Anlisis de la cadena i+i*.

4.2.4. Anlisis LL(1) mediante el uso de tablas de anlisis


Otra forma de realizar el anlisis descendente selectivo utiliza una tabla de anlisis, cuyas filas son los smbolos no terminales de la gramtica y sus columnas son los smbolos terminales y el smbolo de fin de cadena $. En las celdas de la tabla aparecen reglas de la gramtica. Las celdas vacas corresponden a un error en el anlisis. Las celdas de la tabla se rellenan aplicando el siguiente procedimiento para cada produccin A ::= de la gramtica: Para cada smbolo terminal a primero(), aadir la produccin A ::= en la celda T[A,a]. Si primero(), para cada smbolo terminal b siguiente(A), aadir la produccin A ::= en la celda T[A,b]. Obsrvese que b tambin puede ser $. Como ejemplo, considrese la gramtica del Ejemplo 4.1, que es la siguiente: (1) (2) (3) (4) (5) E ::= TE E ::= +TE E ::= T ::= FT T ::= *FT

112

Compiladores e intrpretes: teora y prctica

(6) (7) (8)

T ::= F ::= (E) F ::= id

Los conjuntos primero y siguiente para los smbolos no terminales de esta gramtica son: primero(E)=primero(T)=primero(F)={(,id} primero(E)={+, } primero(T)={*, } siguiente(E)=siguiente(E)={),$} siguiente(T)=siguiente(T)={+,),$} siguiente(F)={*,+,),$} La produccin E ::= TE debe colocarse en la fila correspondiente al smbolo E, en las columnas correspondientes a los smbolos terminales del conjunto primero(TE). Si se aplica el procedimiento para calcular el conjunto primero de una forma sentencial, hay que calcular el conjunto primero(T) = {(,id}. La produccin E ::= +TE debe colocarse en la fila correspondiente al smbolo E y en las columnas correspondientes a los smbolos terminales del conjunto primero(+TE). Si se aplica el procedimiento para calcular el conjunto primero para una forma sentencial, hay que calcular el conjunto primero(+) = {+}. La produccin E ::= debe colocarse en la fila correspondiente al smbolo E y en las columnas correspondientes a los smbolos terminales del conjunto siguiente(E) = {),$}. Por este procedimiento, se van rellenando las celdas de la tabla, obtenindose la que muestra la Tabla 4.1. Una gramtica es LL(1) si en la tabla de anlisis sintctico obtenida por el procedimiento anterior aparece como mximo una regla en cada celda.
Tabla 4.1 id E E T T F F::=id T::=FT T::= F::=(E) E::=TE E::=+TE T::=FT T::= T::= + * ( E::=TE E::= E::= ) $

Ejemplo Consideremos la gramtica siguiente: 4.7 P ::= iEtPP P ::= a

Captulo 4. Anlisis sintctico

113

P::= eP P::= E ::= b La Tabla 4.2 muestra la tabla de anlisis sintctico para esta gramtica. Puede observarse que en la celda correspondiente a la interseccin de la fila P con la columna e aparecen dos reglas. El motivo es que la interseccin del conjunto primero(P) = {e, } y siguiente(P) = {$, e} no es el conjunto vaco, por lo que la regla- para el smbolo P debe colocarse en una celda que ya est ocupada por la regla P::=eP.
Tabla 4.2 a P P E E::=b P::=a P::= P::=eP b e i P::=iEtPP P::= t $

Anlisis de cadenas
La tabla de anlisis sintctico puede utilizarse para analizar cadenas mediante el siguiente algoritmo: 1. Inicializar una pila con el smbolo $ y el axioma de la gramtica y aadir el smbolo $ al final de la cadena de entrada. 2. Repetir el siguiente procedimiento: comparar el smbolo de la cima de la pila P con el siguiente smbolo de entrada e: Si P y e son iguales al smbolo $, aceptar la cadena y salir. Si son iguales y distintos del smbolo $, extraer un elemento de la pila y avanzar una posicin en la cadena de entrada. Si son distintos, y la celda de la tabla de anlisis T(P,e) est vaca, emitir mensaje de error y salir. Si son distintos, y en la celda de la tabla de anlisis T(P,e) aparece la produccin P ::= X1 X2... Xn, extraer P de la pila e insertar los smbolos X1 X2... Xn en la pila en el orden inverso a como aparecen en la parte derecha de la produccin. Como ejemplo, la Tabla 4.3 muestra el anlisis de la cadena id+id utilizando la tabla de anlisis que aparece en la Tabla 4.1. En la Tabla 4.3 aparece el contenido de la pila en cada paso del anlisis, as como el estado de la entrada y la accin a realizar. Veamos en detalle algunos de los pasos del anlisis. En el primer paso, los smbolos E (smbolo de la cima de la pila) e id (siguiente smbolo de entrada) son distintos, por lo que se aplica la regla E::=TEque aparece en la celda T(E,id). Como resultado, se extrae el smbolo E de la pila y se insertan los smbolos E y T, que son los que aparecen en la parte derecha de esta regla, en sentido inverso.

114

Compiladores e intrpretes: teora y prctica

Tabla 4.3. Anlisis de la cadena id+id. Paso 1 2 3 4 5 6 7 8 9 10 11 12 13 Pila $E $ET $ETF $ETid $ET $E $ET+ $ET $ETF $ETid $ET $E $ Entrada id+id$ id+id$ id+id$ id+id$ +id$ +id$ +id$ id$ id$ id$ $ $ $ Accin Aplicar E::=TE Aplicar T::=FT Aplicar F::=id Avanzar Aplicar T::= Aplicar E::=+TE Avanzar Aplicar T::=FT Aplicar F::=id Avanzar Aplicar T::= Aplicar E::= Cadena aceptada

En el cuarto paso del anlisis, el smbolo de la cima de la pila y el siguiente smbolo de entrada son iguales, por lo que se extrae id de la pila y se avanza una posicin en la cadena de entrada. Obsrvese que, como en el paso 5 del anlisis, cuando la regla a aplicar es una regla- se extrae un smbolo de la pila y no se inserta ninguno.

4.3 Anlisis sintctico ascendente


4.3.1. Introduccin a las tcnicas del anlisis ascendente
En contraposicin a las tcnicas del anlisis sintctico descendente, los analizadores ascendentes recorren el rbol de derivacin de una cadena de entrada correcta desde las hojas (los smbolos terminales) a la raz (el axioma), en una direccin grficamente ascendente, lo que da nombre a estas tcnicas. El anlisis consiste en un proceso iterativo, que se aplica inicialmente a la cadena completa que se va a analizar y termina, bien cuando se completa el anlisis con xito, o bien cuando ste no puede continuar, debido a algn error sintctico. En cada paso del anlisis se intenta deducir qu regla de la gramtica se tiene que utilizar en ese punto del rbol, teniendo en cuenta el estado de ste y la posicin a la que se ha llegado en la cadena de entrada. En general, al final de cada paso del anlisis se ha modificando la cadena de entrada, que queda preparada para continuar.

Captulo 4. Anlisis sintctico

115

En este tipo de anlisis se pueden realizar dos operaciones fundamentales: la reduccin y el desplazamiento.

Reduccin
Se aplica cuando se ha identificado en la cadena de entrada la parte derecha de alguna de las reglas de la gramtica. Esta operacin consiste en reemplazar, en la cadena de entrada, dicha parte derecha por el smbolo no terminal de la regla correspondiente. La Figura 4.19 muestra grficamente cmo se realiza esta operacin.

A A i n t1 tj 1 i-1 t1 tj

Figura 4.19. Representacin grfica de la reduccin de la regla Ain.

Desplazamiento
Intuitivamente, los analizadores ascendentes guardan informacin que les permite saber, en cada momento, qu partes derechas de las reglas de la gramtica son compatibles con la porcin de la cadena de entrada analizada, entre todas las reglas posibles. Como, en general, las partes derechas de las reglas tienen ms de un smbolo, en cada paso del anlisis no siempre se puede reducir una regla. Se llama desplazamiento la operacin mediante la cual se avanza un smbolo, simultneamente en la cadena de entrada y en todas las reglas que siguen siendo compatibles con la porcin de entrada analizada. La Figura 4.20 muestra grficamente un ejemplo de esta operacin. La parte inferior de la figura muestra la cadena de entrada. La superior, un subconjunto de reglas de la gramtica. El bloque izquierdo muestra la situacin anterior al desplazamiento: la cadena de entrada ha sido analizada hasta el terminal tj. El subconjunto de reglas contiene aquellas cuyas partes derechas han sido compatibles con la parte de la cadena de entrada analizada, hasta el smbolo tj inclusive. El bloque derecho muestra la situacin despus del desplazamiento. Slo se resaltan las reglas que siguen siendo compatibles con el siguiente smbolo de entrada (tj+1). El algoritmo bsico del anlisis ascendente, que se explicar con detalle en las prximas secciones, puede describirse de la siguiente manera en funcin de las operaciones de reduccin y desplazamiento: Se inicia el proceso con el primer smbolo de la cadena de entrada.

116

Compiladores e intrpretes: teora y prctica

Nh . . . t j t k N . . . Nv . . . t j t j+1 N . . . Nb . . . t j t j+1 N . . . N g . . . t jt 1 N . . . Nt . . . t jt m N . . . Ny . . . t jt j+1 N . . . Nq . . . t j t p N . . .

Nh . . . t j t k N . . . Nv . . . t j t j+1 N . . . Nb . . . t j t j+1 N . . . Ng . . . t j t 1 N . . . Nt . . . t j t m N . . . Ny . . . t j t j+1 N . . . Nq . . . t j t p N . . .

. . . t j t j+1 t j+2 t j+3 t j+4 t j+5 t j+6 t j+7 tj, . . .

. . . t j t j+1 t j+2 t j+3 t j+4 t j+5 t j+6 t j+7 tj, . . .

Figura 4.20. Representacin grfica de la operacin de desplazamiento.

Se realiza el siguiente paso del anlisis, hasta que se determine que la cadena es sintcticamente correcta (si se ha recorrido la entrada completa, reducindola al axioma) o incorrecta (si en algn instante el anlisis no puede continuar). 1. Si una regla se puede reducir (toda su parte derecha se ha desplazado) se reduce. La nueva cadena de anlisis es el resultado de reemplazar la parte de la cadena correspondiente a la parte derecha de la regla por el smbolo no terminal situado a la izquierda de la misma. El anlisis continuar a partir de dicho smbolo no terminal. 2. En otro caso, se realiza un desplazamiento sobre el smbolo correspondiente al paso de anlisis actual. Esto significa que se descartan las reglas cuyo smbolo siguiente, en la parte derecha, no sea compatible con el desplazado, mientras se avanza en las partes derechas en las que sea posible y en la cadena de entrada.

4.3.2. Algoritmo general para el anlisis ascendente


La mayora de los algoritmos de anlisis sintctico de tipo ascendente se realizan en dos fases: en la primera, se construye una tabla auxiliar para el anlisis sintctico ascendente, que en el segundo paso se utilizar en el anlisis de las cadenas de entrada. Como se ha indicado previamente, los analizadores de los lenguajes independientes del contexto pueden basarse en el autmata a pila asociado a su gramtica. Hay dos diferencias principales entre los analizadores ascendentes: la manera en que construyen la tabla de anlisis y la informacin que necesitan introducir en la pila. En este ltimo aspecto, las tcnicas ms potentes, LR(1) y LALR(1), necesitan ms informacin que las ms simples, LR(0) y SLR(1). Para simplificar, en este captulo se utilizar el mismo algoritmo para todos ellos, aunque sea a costa de introducir en la pila informacin redundante para las tcnicas LR(0) y SLR(1).

Estructura general de las tablas de anlisis ascendente


Las tablas del anlisis contienen una informacin equivalente a la funcin de transicin del autmata a pila que reconocera el lenguaje asociado a la gramtica que se est analizando. Dentro

Captulo 4. Anlisis sintctico

117

de este autmata a pila se puede distinguir la parte que tiene por objeto reconocer los asideros de la gramtica, que en realidad es un autmata finito. A lo largo de todo el captulo se utilizar el nombre autmata de anlisis para referirse a dicho autmata. El nmero de filas vara en funcin de la tcnica utilizada para construir la tabla y coincide con el nmero de estados del autmata. Cada tcnica puede construir un autmata distinto, con diferentes estados. Tendrn tantas columnas como smbolos hay en el alfabeto de la gramtica. Usualmente, la tabla se divide en dos secciones, que corresponden, respectivamente, a los smbolos terminales y no terminales. Las columnas de la tabla asociadas a los smbolos terminales forman el bloque de accin, ya que son ellas las que determinan la accin siguiente que el analizador debe realizar. Las restantes columnas de la tabla (las columnas asociadas a los smbolos no terminales) slo conservan informacin relacionada con las transiciones del autmata y forman el bloque ir_a. Sus casillas slo contienen la identificacin del estado al que hay que transitar. Para poder utilizarla en el anlisis, la tabla debe especificar, en funcin del smbolo terminal de la cadena de entrada que se recibe y del estado en que se encuentra el autmata, cul ser el estado siguiente; qu modificaciones deben realizarse en la cadena de entrada y en la pila; si se produce un desplazamiento o una reduccin, si se ha terminado con xito el anlisis o si se ha detectado algn error. Para especificar esta informacin, a lo largo de este captulo se utilizar la siguiente notacin: 1. d<e>, donde d significa desplazamiento y <e> identifica un estado del autmata de anlisis. Representa la accin de desplazar el smbolo actual y pasar al estado <e>. 2. r<p>, donde <p> identifica una regla de produccin de la gramtica. Representa la accin de reduccin de la regla <p>. 3. Aceptar, que representa la finalizacin con xito del anlisis. 4. Error, que representa la finalizacin sin xito del anlisis. La Figura 4.21 muestra grficamente la estructura de estas tablas.

T E s0 T1 Tn N1

N Nm

...
sk

...

...

...

Accin

Ir_a

Figura 4.21. Estructura de las tablas de anlisis de los analizadores sintcticos ascendentes.

118

Compiladores e intrpretes: teora y prctica

Algoritmo de anlisis ascendente


La especificacin completa del algoritmo general de anlisis ascendente describe las manipulaciones realizadas sobre la tabla, la entrada y la pila, asociadas con la accin correspondiente a cada paso del anlisis. La Figura 4.22 muestra grficamente el esquema del analizador. Tanto el significado de las columnas de accin como de las de ir_a, as como el contenido de la pila, son objeto de prximas secciones.

ENTRADA a1 a2 E s0 T1

TABLA ANLISIS T Tn N1 N Nm

PILA sm Xm s m-1 cima

...
au $

...
sk

...

...

...

X m-1

...
Accin Ir_a s0

SALIDA

Figura 4.22. Estructura de un analizador sintctico ascendente.

La Figura 4.23 muestra el algoritmo general de anlisis en pseudocdigo. Puede observarse que el algoritmo es un bucle en el que se consulta la tabla del anlisis para descubrir la accin que hay que realizar. A continuacin se estudia con detalle el tratamiento de cada tipo de operacin. Se utilizar como ejemplo la cadena i+i+i y la siguiente gramtica: { T={+,i,(,),$1}, N={E1,E,T}, E, { E ::= E$1, E ::= E+T | T, T ::= i | (E)} }

1 Ms adelante se ver que la introduccin del smbolo no terminal E, el smbolo terminal $ y la regla E::=E$ son pasos generales del algoritmo de anlisis.

Captulo 4. Anlisis sintctico

119

Estado AnalizadorAscendente(tabla_anlisis, entrada, pila, gramtica) /* La entrada contiene la cadena w$ que se quiere analizar se deja vaca la posicin 0 para las reducciones */ { puntero smbolo_actual=1; /* la posicin 0 est vaca por si fuese necesario al reducir */ estado estado_actual; push(pila,0); while( verdadero ) /* Bucle sin fin */ { estado_actual=cima(pila); if (tabla_anlisis[estado_actual, entrada[smbolo_actual]] == ds ) { push(pila, entrada[smbolo_actual]); push(pila, s); smbolo_actual++;} else if ( tabla_anlisis[estado_actual, entrada[smbolo_actual]] == rj ) { /* Podemos suponer que la regla j es A::= */ `realizar 2*longitud() pop(pila) entrada[--smbolo_actual] = A; printf(Reduccin de A::=);) else if ( tabla_anlisis[estado_actual, entrada[smbolo_actual]] == aceptar ) return CADENA_ACEPTADA; else /* casilla vaca */ return CADENA_RECHAZADA:_ERROR_SINTCTICO); }}
Figura 4.23. Pseudocdigo del algoritmo general de anlisis ascendente.

Es fcil comprobar que el lenguaje asociado est compuesto por expresiones aritmticas formadas por sumas entre parntesis opcionales, y el smbolo i como nico operando.

Inicio del anlisis


Se supone que se dispone de la tabla de anlisis que se muestra en la Figura 4.24.

120

Compiladores e intrpretes: teora y prctica

T E 0 1 2 3 4 5 6 7 8 d1 r1 r4
Accin

N ) $ E 4 d3 d3 5 d2 d2 acc d8 3 T 3

i d1

+
(*) d3

( d2

d1 d2 d6 d6

d2

d2 r1 r4 r1 r4
Ir_a

(*) Se considera que todas las casillas vacas representan la accin de error.

Figura 4.24. Una tabla de anlisis correspondiente a la gramtica {T={+,i,(,),$ }, N={E,E,T}, E, {EE$,EE+T|T,Ti|(E)}}.

Se aade a la entrada el smbolo $, que indica que se ha llegado al final de la cadena. La pila contendr inicialmente el estado inicial del autmata, que en este captulo ser el estado etiquetado con el nmero 0. La Figura 4.25 muestra el paso inicial, realizado con la gramtica del ejemplo. Es importante resaltar que el algoritmo descrito utiliza la pila para conservar toda la informacin necesaria para continuar el anlisis. Para ello, a excepcin de esta situacin inicial, en la que slo se introduce un estado, la informacin mnima que se inserte o se saque de la pila ser usualmente un par de datos: el estado del analizador y el smbolo de la gramtica considerado en ese instante. Esto permite utilizar el mismo algoritmo de anlisis en todas las tcnicas, aunque para alguna de ellas bastara con introducir el estado.

Desplazamiento d<e>
Como se ha indicado, las filas de la tabla representan los estados del autmata asociado a la gramtica y, para poder utilizarla en el anlisis, tienen que conservar simultneamente informacin sobre todas las reglas cuyas partes derechas son compatibles con la parte de la cadena de entrada que ya ha sido analizada.

Captulo 4. Anlisis sintctico

121

i 0

T E 0 1 2 3 4 5 6 7 8 d1 r1 r4
Accin

N ) $ E 4 r3 r3 5 r2 r2 acc d8 3 T 3

(0)E E$ (1)E E+T (2)T (3)T i (4)(E)

i d1

( d2

r3 d1 r2 d6 d6 d2 d2

7 r1 r4 r1 r4
Ir_a

Figura 4.25. Situacin inicial para el anlisis de la cadena i+i+i.

La indicacin de desplazamiento significa que el smbolo actualmente estudiado en la cadena de entrada es uno de los que espera alguna de las reglas con partes derechas parcialmente analizadas. Por lo tanto, se puede avanzar una posicin en la cadena de entrada, y el autmata puede pasar al estado que corresponde al desplazamiento de ese smbolo en las partes derechas de las reglas en las que dicho desplazamiento sea posible. Esta operacin implicar realizar las siguientes operaciones: Se introduce en la pila el smbolo de entrada. Para tener en cuenta el cambio de estado del autmata, el estado indicado por la operacin (<e>) tambin se introduce en la pila. Se avanza una posicin en la cadena de entrada, de manera que el smbolo actual pase a ser el siguiente al recin analizado.

122

Compiladores e intrpretes: teora y prctica

i 0

i 1

+ i

i 0 T

T E 0 1 2 3 4 5 6 7 8 d1 r1 r4 Accin d1 r2 d6 d6 d2 r1 r4 r1 r4 d8 i d1 r3 d2 r2 r2 acc

N ) $ E 4 r3 r3 5 3 T 3 E 0 1 2 3 4 5 7 6 7 8 Ir_a d1 r1 r4 d1 r2 d6 d6 i d1 r3

N ) $ E 4 r3 r3 5 r2 r2 acc d8 3 T 3

( d2

( d2

d2

d2 r1 r4 Accin r1 r4 Ir_a

Figura 4.26. Ejemplo de operacin de desplazamiento en el anlisis de la cadena i+i+i.

En el ejemplo se puede comprobar que la primera accin de anlisis en el tratamiento de la cadena i+i+i es un desplazamiento. El analizador est en el estado 0 y el prximo smbolo que hay que analizar es i. La casilla (0,i) de la tabla del anlisis contiene la indicacin d1, es decir, desplazamiento al estado 1. Por lo tanto, hay que introducir en la pila el smbolo i y el nmero 1. La Figura 4.26 muestra grficamente este paso del anlisis.

Reduccin r<p>
La indicacin de reduccin en una casilla de la tabla significa que, teniendo en cuenta el smbolo actual de la cadena de entrada, alguna de las reglas representadas en el estado actual del autmata ha desplazado su parte derecha completa, que puede ser sustituida por el smbolo no terminal de su parte izquierda. Como resultado de esta accin, el analizador debe actuar de la siguiente forma: Se saca de la pila la informacin asociada a la parte derecha de la regla <p>. Supongamos que la regla <p> es N::=. En la pila hay dos datos por cada smbolo, por lo que tendrn que realizarse tantas operaciones pop como el doble de la longitud de .

Captulo 4. Anlisis sintctico

123

T 0

i 3

+ T

i 0 T

T E 0 1 2 3 4 5 6 7 8 d1 r1 r4 Accin d1 r2 d6 d6 d2 r1 r4 r1 r4 d8 i d1 r3 d2 r2 r2 acc

N ) $ E 4 r3 r3 5 3 T 3 E 0 1 2 3 4 5 7 6 7 8 Ir_a d1 r1 r4 d1 r2 d6 d6 i d1 r3

N ) $ E 4 r3 r3 5 r2 r2 acc d8 3 T 3

( d2

( d2

d2

d2 r1 r4 Accin r1 r4 Ir_a

Figura 4.27. Ejemplo de operacin de reduccin. La casilla de la tabla (3,+) contiene la indicacin r3.

Se introduce el smbolo no terminal de la regla (N) a la izquierda de la posicin actual de la cadena de entrada, y se apunta a dicho smbolo. Es fcil comprobar en el ejemplo que la segunda accin, tras el desplazamiento anterior, es una reduccin, ya que la casilla correspondiente al smbolo actual (+) y al estado que ocupa la cima de la pila (1), indica que debe reducirse la regla 3: T ::= i.Como slo hay un smbolo en la parte derecha, hay que ejecutar dos pop sobre la pila e insertar a la izquierda de la porcin de la cadena de entrada pendiente de analizar la parte izquierda de la regla (T), que pasa a ser el smbolo actual. El estado que queda en la pila es 0. La casilla (0, T) de la tabla de anlisis indica que se tiene que ir al estado 3. La Figura 4.27 muestra grficamente este paso del anlisis. La Figura 4.28 ilustra otra reduccin cuya regla asociada tiene una parte derecha de longitud mayor que 1. Se trata de la regla 1: E ::= E+T. En este caso habr que ejecutar 6 (2*3) operaciones pop en la pila. Tras realizarlas, la cima de la pila contiene el estado 0. Se aade en la posicin correspondiente de la cadena de entrada la parte izquierda (E) y se contina el anlisis.

124

Compiladores e intrpretes: teora y prctica

E 1

+ i

i 6
T

+ +

i 4

$ E
N

E 0 1 2 3 4 5 6 7 8

i d1

( d2

E 4

T 3

r3 d1 r2 d6 d6 d1 r1 r4 Accin d2 d2

r3

r3 5 3

r2

r2 acc

d8 7 r1 r4 r1 r4 Ir_a

E 6

+ +

T 4
T

+ E

i 0

E 7
N

+ T

T 6
T

+ +

i 4

$ E
N

E 0 1 2 3 4 5 6 7 8

i d1

( d2

E 4

T 3

E 0 1

i d1

( d2

E 4

T 3

r3 d1 r2 d6 d6 d1 r1 r4 Accin d2 d2

r3

r3 5 3

r3 d1 r2 d6 d6 d1 r1 r4 Accin d2 d2

r3

r3 5 3

2 3 4 5

r2

r2 acc

r2

r2 acc

d8 7 r1 r4 r1 r4 Ir_a

d8 7 r1 r4 r1 r4 Ir_a

6 7 8

Figura 4.28. Reduccin de la regla 1 en un estado intermedio del anlisis de la cadena i+i+i.

Captulo 4. Anlisis sintctico

125

Aceptacin
Cuando en la ejecucin del algoritmo se llega a una casilla que contiene esta indicacin, el anlisis termina y se concluye que la cadena de entrada es sintcticamente correcta. La Figura 4.29 muestra el final del anlisis de la cadena i+i+i en el caso del ejemplo de los puntos anteriores. Obsrvese el papel del smbolo $, que se aadi precisamente para facilitar la identificacin de esta circunstancia.

E 4

+ E

E 0 T

N ) $ E 4 r3 r3 5 r2 r2 acc d8 3 T 3

E 0 1 2 3 4 5 6 7 8

i d1

( d2

r3 d1 r2 d6 d6 d1 r1 r4
Accin

d2

d2 r1 r4 r1 r4
Ir_a

Figura 4.29. Fin de anlisis: la cadena i+i+i es correcta.

Error
Cuando la ejecucin del algoritmo llega a una casilla con esta indicacin, el anlisis termina y se concluye que la cadena de entrada contiene errores sintcticos. Las tablas del anlisis suelen contener ms casillas con esta indicacin que con cualquiera de las anteriores. Es frecuente dejar estas casillas en blanco para facilitar el manejo de la tabla. La Figura 4.30 muestra un ejemplo de anlisis de la cadena (+i) con la gramtica

126

Compiladores e intrpretes: teora y prctica

T E 0 1 2 3 4 5 6 7 8 9 10 11 d5 d5 d6 r1 r3 r5 d7 r3 r5 Accin a) d5 r6 r6 d4 d4 d11 r1 r3 r5 r1 r3 r5 i d5 d6 r2 r4 d7 r4 d4 r6 r6 r2 r4

N ( d4 acc r2 5 r4 8 2 3 )
E $ T E

T F 3 E 0 1 2 3 4 5 d5 r6 d5 d5 d6 r1 r3 r5 d7 r3 r5 Accin b) r6 d4 d4 d11 r1 r3 r5 r1 r3 r5 i d5 d6 r2 r4 d7 r4 d4 r6 r6 r2 r4

N ( d4 acc r2 5 r4 8 2 3 )
E $ T E

T 2

T 2

F 3

3 10

6 7 8 9 10 11

3 10

Ir_a

Ir_a

( 4

+ (

i 0
T

( 4
N

+ (

i 0
T

N ( d4 )
E $ T E

E 0 1 2 3 4 5 6 7 8 9 10 11

i d5

( d4

E $

T E

T 2

F 3

E 0 1 2 3

i d5

T 2

F 3

1 acc

1 acc

d6 r2 r4 d5 r6 d5 d5 d6 r1 r3 r5 d7 r3 r5 Accin c) r6 d4 d4 d11 r1 r3 r5 d7 r4 d4 r6 r2 r4

d6 r2 r4 d5 r6 d5 d5 d6 r1 r3 r5 d7 r3 r5 Accin d) r6 d4 d4 d11 r1 r3 r5 d7 r4 d4 r6 r2 r4

r2 5 r4 8 r6 9 3 10 2 3

r2 5 r4 8 r6 9 3 10 2 3

4 5 6 7 8

r1 r3 r5 Ir_a

9 10 11

r1 r3 r5 Ir_a

Figura 4.30. Ejemplo de anlisis que termina con error sintctico.

Captulo 4. Anlisis sintctico

127

T={+,*,i,(,)}, N={E,T,F}, E, { E ::= E+T | T, T ::= T*F | F, F ::= (E) | i} }

y que termina con error sintctico. Es fcil comprobar que esta gramtica genera expresiones aritmticas con los operadores binarios + y * y con el smbolo i como nico operando. Las expresiones permiten el uso opcional de parntesis. Obsrvese: (a) y (b) Muestran respectivamente la situacin previa e inicial al anlisis. (c) La casilla (0,() contiene la operacin d4, por lo que se inserta en la pila el smbolo ( y el estado 4 y se avanza una posicin en la cadena de entrada; el smbolo actual es +. (d) La casilla (4,+) est vaca, es decir, indica que se ha producido un error sintctico. El error es que se ha utilizado el smbolo + como operador mondico cuando la gramtica lo considera binario.

4.3.3. Anlisis LR(0)


Las siglas LR describen una familia de analizadores sintcticos que Examinan la entrada de izquierda a derecha (del ingls Left to right). Construyen una derivacin derecha de la cadena analizada (del ingls Rightmost derivation). Las siglas LR(k) hacen referencia a que, para realizar el anlisis, se utilizan los k smbolos siguientes de la cadena de entrada a partir del actual. Por lo tanto, LR(0) es el analizador de esa familia que realiza cada paso de anlisis teniendo en cuenta nicamente el smbolo actual.

Configuracin o elemento de anlisis LR(0)


Como se ha indicado anteriormente, los analizadores ascendentes siguen simultneamente todos los caminos posibles; es decir, cada estado del autmata de anlisis conserva todas las reglas cuyas partes derechas son compatibles con la porcin de entrada ya analizada. Cada una de esas reglas es hipottica, pues al final slo una de ellas ser la aplicada. En los apartados siguientes se estudiar con detalle la construccin del autmata de anlisis LR(0). Para ello, es necesario explicitar en un objeto concreto cada una de las hiptesis mencionadas. Informalmente, se llamar configuracin o elemento de anlisis a la representacin de cada una de las hiptesis. Una configuracin o elemento de anlisis LR(0) es una regla, junto con una indicacin respecto al punto de su parte derecha hasta el que el analizador ha identificado que dicha regla es compatible con la porcin de entrada analizada.

128

Compiladores e intrpretes: teora y prctica

Es posible utilizar diversas notaciones para las configuraciones LR(0). En este captulo mencionaremos las dos siguientes: Notacin explcita: Consiste en escribir la regla completa y marcar con un smbolo especial la posicin de la parte derecha hasta la que se ha analizado. Suele utilizarse el smbolo o el smbolo _, colocndolo entre la subcadena ya procesada de la parte derecha y la pendiente de proceso. A lo largo de este captulo se utilizar el nombre apuntador de anlisis para identificar este smbolo. A continuacin se muestran algunos ejemplos: E ::= E+T Indica que es posible que se utilice esta regla en el anlisis de la cadena de entrada, aunque, por el momento, el analizador an no ha procesado informacin suficiente para avanzar ningn smbolo en la parte derecha de la regla. Para que finalmente sea sta la regla utilizada, ser necesario reducir parte de la cadena de entrada al smbolo no terminal E, encontrar a continuacin el terminal +, y reducir posteriormente otra parte de la cadena de entrada al smbolo no terminal T. E ::= E+T Indica que es posible que en el anlisis de la cadena de entrada se vaya a utilizar esta regla, y que el analizador ya ha podido reducir parte de la cadena de entrada al smbolo no terminal E, encontrando a continuacin el smbolo terminal +. Antes de utilizar esta regla, ser necesario reducir parte de la cadena de entrada al smbolo no terminal T. E ::= E+T Indica que el analizador ya ha comprobado que toda la parte derecha de la regla es compatible con la parte de la cadena de entrada ya analizada. En este momento se podra reducir toda la parte de la cadena de entrada asociada a E+T y sustituirla por el smbolo no terminal E (la parte izquierda de la regla). Las configuraciones de este tipo se denominan configuraciones de reduccin. Todas las configuraciones que no son de reduccin son configuraciones de desplazamiento. P ::= Esta configuracin indica que es posible reducir la regla- P ::= . Siempre que se llegue a una configuracin asociada a una regla lambda, ser posible reducirla. Se trata, por tanto, de una configuracin de reduccin. Esta notacin es ms farragosa y menos adecuada para programar los algoritmos, pero resulta ms legible, por lo que ser utilizada a lo largo de este captulo. Notacin de pares numricos: Tambin se puede identificar una configuracin con un par de nmeros. El primero es el nmero de orden de la regla de que se trate en el conjunto de las reglas de produccin. El segundo indica la posicin alcanzada en la parte derecha de la regla, utilizando el 0 para la posicin anterior al primer smbolo de la izquierda, e incrementando en 1 a medida que se avanza hacia la derecha. Esta notacin es equivalente a la anterior y facilita la programacin de los algoritmos, pero resulta menos legible.

Captulo 4. Anlisis sintctico

129

A continuacin se muestran las correspondencias entre ambas notaciones en los ejemplos anteriores, suponiendo que la regla E ::= E+T es la nmero 3 y la regla P ::= es la nmero 4. E ::= E+T (3,0) E ::= E+T (3,2) E ::= E+T (3,3) P ::= (4,0) Ejemplo Puesto que el analizador sintctico LR(0) se basa en el autmata de anlisis LR(0), ser objetivo de los siguientes apartados describir detalladamente su construccin. Con este ejemplo se justi4.8 ficarn intuitivamente los pasos necesarios, que luego se formalizarn en un algoritmo. Se utilizar como ejemplo la gramtica que contiene las siguientes reglas de produccin (el axioma es E; obsrvese que la primera regla ya incorpora el smbolo de fin de cadena). (0) E ::= E$ (1) E ::= E+T (2) |T (3) T ::= i (4) |(E) Estado inicial del autmata. El estado inicial contiene las configuraciones asociadas con las hiptesis previas al anlisis; el apuntador de anlisis estar situado delante del primer smbolo de la cadena de entrada, y se trata de reducir toda la cadena al axioma. La hiptesis inicial tiene que estar relacionada con la regla del axioma. A lo largo del captulo se ver cmo se puede asegurar que el axioma slo tenga una regla. Esta hiptesis tiene que procesar la parte derecha completa de esa regla completa, por lo que el estado inicial tiene que contener la siguiente configuracin: E ::= E$ Esta configuracin representa la hiptesis de que se puede reducir toda la cadena de entrada al smbolo no terminal E, ya que a continuacin slo hay que encontrar el smbolo terminal que indica el fin de la cadena. Un smbolo no terminal nunca podr encontrarse en la cadena de entrada original, pues slo aparecer como resultado de alguna reduccin. Por ello, la hiptesis representada por una configuracin que espere encontrar a continuacin un smbolo no terminal obligar a mantener simultneamente todas las hiptesis que esperen encontrar a continuacin cualquiera de las partes derechas de las reglas de dicho smbolo no terminal, ya que la reduccin de cualquiera de ellas significara la aparicin esperada del smbolo no terminal. La Figura 4.31 muestra grficamente esta circunstancia. A lo largo de la construccin del autmata aparecern muchas configuraciones que indiquen que el analizador est situado justo delante de un smbolo no terminal. La reflexin anterior se podr aplicar a todas esas situaciones y se implementar en la operacin cierre, que se aplica a conjuntos de configuraciones y produce conjuntos de configuraciones. Por lo tanto, el estado inicial del autmata debe contener todas las hiptesis equivalentes a la

130

Compiladores e intrpretes: teora y prctica

E (c) E

E
E E

(a)

(b)

E $ (d)

T
$

Figura 4.31. Ejemplo de cierre de configuracin cuando el anlisis precede un smbolo no terminal. (a) Situacin previa al anlisis. (b) E::=E$ (c) E::=E+T (d) E::=T.

inicial: hay que aadir, por tanto, todas las que tengan el indicador del analizador delante de las partes derechas de las reglas del smbolo no terminal E. E ::= E+T E ::= T Por las mismas razones que antes, habr que realizar el cierre de estas dos configuraciones. Para la primera, no es preciso aadir al estado inicial ninguna configuracin nueva, ya que el apuntador de anlisis precede al mismo smbolo no terminal que acabamos de considerar, y las configuraciones correspondientes a su cierre ya han sido aadidas. Para la segunda, habra que aadir las siguientes configuraciones: T ::= i T ::= (E) Estas dos configuraciones tienen en comn que el analizador espera encontrar a continuacin smbolos terminales (i y (). Para ello sera necesario localizarlos en la cadena de entrada, pero eso slo ocurrir en pasos futuros del anlisis. No queda nada pendiente en la situacin inicial, por lo que, si llamamos s0 al estado inicial, se puede afirmar que:

Captulo 4. Anlisis sintctico

131

s0={E ::= E$, E ::= E+T, E ::= T, T ::= i, T ::= (E)} Completada esta reflexin, se puede describir con ms precisin cul es el contenido del estado inicial de los autmatas de anlisis LR(0). Se ha mencionado anteriormente que siempre se podr suponer que hay slo una regla cuya parte izquierda es el axioma y cuya parte derecha termina con el smbolo de final de cadena $. Si esa regla es A ::= $, el estado inicial contendr el conjunto de configuraciones resultado del cierre del conjunto de configuraciones {A ::= $}. Justificacin intuitiva de la operacin de ir de un conjunto de configuraciones a otro mediante un smbolo. El analizador tiene que consultar los smbolos terminales de la cadena de entrada. En una situacin intermedia de anlisis, tras alguna reduccin, tambin es posible encontrar como siguiente smbolo pendiente de analizar uno no terminal. Una vez estudiada la situacin inicial, se puede continuar la construccin del autmata del analizador, aadiendo las transiciones posibles desde el estado inicial. La primera conclusin es que hay transiciones posibles, tanto ante smbolos terminales, como ante no terminales. Cmo se comportara el analizador si, a partir del estado s0, encuentra en la cadena de entrada un smbolo terminal distinto de i y de ( o algn smbolo no terminal distinto de E o de T? Ya que no hay ninguna configuracin que espere ese smbolo, se concluira que la cadena de entrada no puede ser generada por la gramtica estudiada, es decir, que contiene un error sintctico. Por lo tanto, todas las transiciones desde el estado inicial con smbolos distintos de i, (, E o T conducirn a un estado de error. En teora de autmatas, es habitual omitir los estados errneos al definir las transiciones de un autmata, de forma que las transiciones que no se han especificado para algn smbolo son consideradas como errneas. La segunda conclusin es que, en el autmata del analizador, habr tantas transiciones a partir de un estado que conduzcan a estados no errneos, como smbolos sigan al apuntador de anlisis en alguna de las configuraciones del estado de partida. Esto significa que desde el estado inicial slo se podr ir a otros estados mediante los smbolos terminales i o ( o mediante los no terminales E o T. En la situacin inicial, cuando en la cadena de entrada se encuentra el smbolo E, slo las dos primeras configuraciones (E ::= E$ y E ::= E+T) representan hiptesis que siguen siendo compatibles con la entrada. En esta situacin, se puede desplazar un smbolo hacia la derecha, tanto en la cadena de entrada como en estas configuraciones. Si se utiliza el smbolo s1 para identificar el nuevo estado, tienen que pertenecer a l las configuraciones que resultan de este desplazamiento: E ::= E$ E ::= E+T

132

Compiladores e intrpretes: teora y prctica

Para completar el estado resultante de la operacin ir a, hay que aplicar la operacin cierre a las nuevas configuraciones, del mismo modo que se vio antes. En este caso, dado que el apuntador de anlisis precede slo a smbolos terminales, no se tiene que aadir ninguna otra configuracin. Por lo tanto, se puede afirmar que: s1={E ::= E$, E ::= E+T} La Figura 4.32 muestra los dos estados calculados hasta este momento, y la transicin que puede realizarse entre ellos.

s0

E::= E$, E::= E+T, E::= T, T::= i, T::= (E),

E s1 E::=E $, E::=E +T,

Figura 4.32. Estados s0 y s1 y transicin entre ellos del autmata de anlisis LR(0) del ejemplo.

Estados de aceptacin y de reduccin. Es interesante continuar la construccin del autmata con una de las transiciones posibles del estado s1: la del smbolo $. Tras aplicar el mismo razonamiento de los puntos anteriores, es fcil comprobar que el estado siguiente contiene el cierre de la configuracin E ::= E$. Esta configuracin es de reduccin: al estar el apuntador de anlisis al final de la cadena, no precede a ningn smbolo, terminal o no terminal, por lo que el cierre no aade nuevas configuraciones al conjunto. Cuando el autmata de anlisis LR(0) se encuentra en un estado que contiene una configuracin de reduccin, ha encontrado una parte de la entrada que puede reducirse (un asidero), esto es, reemplazarse por el correspondiente smbolo no terminal. Es decir, ha concluido esta fase del anlisis. Por lo tanto, se puede considerar que el autmata debe reconocer esa porcin de la entrada y el estado debe ser final. Se utilizar la representacin habitual (trazo doble) para los estados finales del autmata.

Captulo 4. Anlisis sintctico

133

s0

E::= E$, E::= E+T, E::= T, T::= i, T::= (E),

E s1 E::=E $, E::=E +T, $ sacc


E::=E$ E::=E$

Figura 4.33. Estados s0, s1 y de aceptacin (sacc) del autmata de anlisis LR(0) del ejemplo.

Obsrvese tambin que la regla de esta configuracin es especial: se trata de la regla nica asociada al axioma, cuya parte derecha termina con el smbolo especial de fin de cadena. Este estado de reduccin tambin es especial: es el estado de aceptacin de la cadena completa. Cuando el analizador llega a este estado, significa que la reduccin asociada a su configuracin ha terminado el anlisis y sustituye toda la cadena de entrada por el axioma. Se utilizar para este estado el nombre sacc. La Figura 4.33 representa grficamente esta parte del diagrama de estados. Los razonamientos anteriores pueden aplicarse tantas veces como haga falta, teniendo en cuenta que cada estado debe aparecer una sola vez en el diagrama, y que el orden en que aparezcan las configuraciones en el estado no es relevante. La Figura 4.34 presenta el diagrama completo, una vez obtenido. Obsrvese que hay cuatro estados finales ms, que no son de aceptacin: s3, s4, s7 y s8. Tambin hay varios estados a los que se llega por diferentes transiciones: s2, s4, s5 y s7. Esto significa que dichos estados aparecen ms de una vez en el proceso descrito anteriormente.

Autmata asociado a un analizador LR(0): definiciones formales


La descripcin formal del algoritmo de diseo del autmata de anlisis LR(0) precisa de la definicin previa del concepto auxiliar de gramtica aumentada y de las operaciones de cierre de un

134

Compiladores e intrpretes: teora y prctica

s7 s0 E::=E$, E::= E+T, E::= T, T::= i, T::= (E) T


E::=T E::= T

( s5

( i i s4 i
T::=i T::= i

T::=( E), E::= E+T, E::= T, T::= i, T::= (E)

E s1 E::=E $, E::=E +T $ sacc


E::=E$ E::=E$

s2

( E::=E+ T, E::=E+ T, T::= i, T::= i, T::= (E) T::= (E) T::=(E ), E::=E +T s6

+
T s3
E::=E+T E::=E+T

) s8 8

T::=(E) T::=(E)

Figura 4.34. Diagrama de estados completo del analizador LR(0) del ejemplo.

conjunto de configuraciones y paso de un conjunto de configuraciones a otro mediante un smbolo (ir a). Gramtica aumentada. Dada una gramtica independiente del contexto G=<T, N, A, P>, se define la gramtica extendida para LR(0), en la que se cumple que AN y que $T: G=<N{A}, T{$}, A, P{A ::= A$}> Es fcil comprobar que el lenguaje generado por G es el mismo que el generado por G. Recurdese que el objetivo de esta gramtica es asegurar que slo hay una regla para el axioma. Operacin de cierre. Sea I un conjunto de elementos de anlisis o configuraciones referido a la gramtica G del apartado anterior. Se define cierre(I) como el conjunto que contiene los siguientes elementos: cI ccierre(I). A::=B cierre(I) B::=P B::=cierre(I).

La Figura 4.35 muestra un posible pseudocdigo para esta operacin.

Captulo 4. Anlisis sintctico

135

ConjuntoConfiguraciones Cierre(ConjuntoConfiguraciones I, GramticaIndependienteContexto Gic) { ConjuntoConfiguraciones Cierre := I; Configuracin c; ReglaProduccin r; while( `se aaden configuraciones a Cierre en la iteracin ) { `repetir para cada elemento c en Cierre y r en Reglas(Gic) /* Se supondr que c es de la forma A::=B y r B::= */ if (B::= Cierre) Cierre := Cierre { B::= }; } return Cierre; }
Figura 4.35. Pseudocdigo para la operacin de cierre de un conjunto de configuraciones.

Operacin ir a. Sea I un conjunto de elementos de anlisis o configuraciones y X un smbolo (terminal o no) de la gramtica G del apartado anterior. Se define la operacin ir_a(I,X) as: A::= xIcierre( {A ::= X} ) No se muestra ningn pseudocdigo, ya que la operacin ir_a se reduce a una serie de aplicaciones de la operacin cierre. Grafo de estados y transiciones del autmata2. En lo siguiente, estados y transiciones sern los nombres de los conjuntos de estados (nodos) y transiciones (arcos) del autmata. A partir de la gramtica aumentada G, se puede definir formalmente el grafo de transiciones del autmata de anlisis LR(0), de la siguiente manera: 1. 2. cierre( {A ::= A$} ) estados(G). Iestados(G) (1) XNT, J=ir_a(I,X)estados(G)(I,J) transiciones(G).
2

Algunos autores llaman a los estados de este grafo conjunto de configuraciones cannicas LR(0).

136

Compiladores e intrpretes: teora y prctica

(2) I es final N (NT)* tales que N ::= I. (3) I es de aceptacin A ::= A$I. La Figura 4.36 muestra un posible pseudocdigo para el clculo de este grafo.

Grafo GrafoLR(0) (GramticaIndepenienteContexto Gic) { ConjuntoConfiguraciones estados[]; ParEnteros transiciones[]; ParEnteros aux_par; /* ParEnteros, tipo de datos con dos enteros: o y d (de origen y destino) */ entero i,j,k,it; i:=0; /* ndice de estados */ it:=0; /* ndice de transiciones */ estados[i]:=cierre({axioma(Gic)::=axioma(Gic).$)}; /*. es la concatenacin de cadenas de caracteres */

`repetir para cada ji { `repetir para cada elemento X en NT { if ( (ir_a(s[j],X) ( k [0,i] s[k]ir_a(s[j],X) ) ) { aux_par = nuevo ParEnteros; aux_par.o = i; aux_par.d = j; transiciones[ia++]=aux_par; estado[i++]=ir_a(estado[j],X); } } j++; } return s; }
Figura 4.36. Pseudocdigo para el clculo del diagrama de transiciones del autmata de anlisis LR(0).

Captulo 4. Anlisis sintctico

137

Construccin de la tabla de anlisis a partir del autmata LR(0)


Puede abordarse ahora la construccin de la tabla de anlisis a partir de este autmata. Para ello, hay que identificar las condiciones en las que se anotar cada tipo de operacin en las casillas de la tabla. Desplazamientos. Se obtienen siguiendo las transiciones del diagrama. Si el autmata transita del estado si al estado sj mediante el smbolo x, en la casilla (i,x) de la tabla se aadir dj si xT y j si xN. Reducciones. Se obtienen consultando los estados finales del diagrama, excepto el estado de aceptacin. Por definicin, cada estado final contendr una configuracin de reduccin. Si el estado final es si y su configuracin de reduccin es N::= (donde N::= es la regla k), se aadir la accin rk en todas las casillas de la fila i y las columnas correspondientes a smbolos terminales (la parte de la tabla llamada accin). Aceptacin. Se obtienen consultando los estados que transitan al estado de aceptacin (con el smbolo $). Para todas las casillas (i, $), donde i representa un estado si que tiene una transicin con el smbolo $ al estado de aceptacin, se aade la accin de aceptar. Error. Todas las dems casillas corresponden a errores sintcticos. Como se ha dicho anteriormente, estas casillas suelen dejarse vacas. La Figura 4.37 muestra la tabla de anlisis del diagrama de transiciones del Ejemplo 4.8.

T E 0 1 2 3 4 5 6 7 8 r2 r4 d4 r1 r3 d4 d2 r2 r4 r2 r4
Accin

N ) $ E 1 acc T 7

i d4

( d5

d2 d5 r1 r3 r1 r3 d5 d8 r2 r4 r1 r3

3 r1 r3 6 7

r2 r4
Ir_a

Figura 4.37. Tabla de anlisis LR(0) correspondiente al diagrama de la Figura 4.34.

138

Compiladores e intrpretes: teora y prctica

4.3.4. De LR(0) a SLR(1)


Ejemplo El anlisis LR(0) presenta limitaciones muy importantes. Para comprobarlo, se plantea la construccin de la tabla de anlisis LR(0) de la gramtica GB que contiene el siguiente conjunto de re4.9 glas de produccin, que aparecen numeradas, y en las que el axioma es el smbolo <Bloque>. (1)<Bloque> ::= begin <Decs> ; <Ejecs> end (2)<Decs> ::= dec (3) | <Decs>;dec (4)<Ejecs> ::= ejec (5) | ejec ; <Ejecs> Obsrvese que esta gramtica representa la estructura de los fragmentos de programas compuestos por bloques delimitados por los smbolos begin y end, que contienen una seccin declarativa, compuesta por una lista de smbolos dec (declaraciones), separados por ;, seguida por una serie de instrucciones ejecutables, que consta de una lista de smbolos ejec, separados tambin por ;. La gramtica extendida aade la siguiente regla de produccin: (0)<Bloque> ::= <Bloque>$ Siguiendo los algoritmos y explicaciones de las secciones anteriores, se puede comprobar que el diagrama de estados del autmata de anlisis LR(0) es el que muestra la Figura 4.38 y la tabla de anlisis LR(0) es la de la Figura 4.39. Para simplificar, se utilizan las siguiente abreviaturas:
Smbolo original <Bloque> <Bloque> <Decs> <Ejecs> begin dec end ejec Abreviatura B B D E b d f e

Este diagrama tiene una situacin peculiar no estudiada hasta este momento: el estado S7={<Ejecs> ::= ejec, <Ejecs> ::= ejec;<Ejecs>} es un estado final que contiene ms de una configuracin. A continuacin se describir con detalle qu repercusiones tiene esta situacin.

Captulo 4. Anlisis sintctico

139

s0 B::=B$, B::=bD;Ef b s2 D::=d, B::=bD;Ef, D::=D;d d f


D::=d D::=d

s1 B B::=B$ s4 B::=bD;Ef, D::=D;d D s6 B::=bD;Ef $

sacc B::= B$ s5 ; E::=e, B::=bD;Ef, E E::=e;E, D::=D;d d e


D::=D;d D::=D;d

s3

B::=bD;Ef B::=bD;Ef

E::=e , E::=e, ;
E::=e;E ;E e::=e

s9 s10 E::=e;E, E::=e, E::=e;E

s8 s7

e
E::=e;E E::=e;E

s11

Figura 4.38. Diagrama del autmata de anlisis LR(0) del ejemplo sobre los lmites de LR(0).

La casilla (7,;) presenta otra anomala: segn los algoritmos analizados, la presencia de la configuracin de reduccin <Ejecs>::=ejec obliga a aadir en las casillas de las columnas de la seccin accin de la fila 7 la indicacin r4 (4 es el identificador de la regla <Ejecs>::=ejec). La presencia adicional de una configuracin de desplazamiento con el apuntador de anlisis antes del smbolo ; posibilita que con ese smbolo se transite al estado correspondiente (s10) y, por lo tanto, obliga a aadir a la misma casilla la indicacin d10. Eso significa que, en este estado, en presencia del smbolo ;, no se sabe si se debe reducir o desplazar.

Definiciones
Se llama conflicto a la circunstancia en que una casilla de una tabla de anlisis contiene ms de una accin. Se llama conflicto reduccin / desplazamiento a la circunstancia en que una casilla contiene una configuracin de reduccin y otra de desplazamiento. Se llama conflicto reduccin / reduccin a la circunstancia en que una casilla contiene ms de una configuracin de reduccin.

140

Compiladores e intrpretes: teora y prctica

T E 0 1 2 3 4 5 6 7 8 9 10 11 r5 r4 r3 r1 r4 r3 r1 d7 r5 r5 r5 r5 r5 r4 r3 r1
d10/ r4

N ; f
E $ T B

b d2

1 acc

d3 r2 r2 r2 r2 d5 d8 d7 d9 r4 r3 r1 r2

5 r2

r4 r3 r1 11

r3 r1

Accin

Ir_a

Figura 4.39. Tabla de anlisis LR(0) correspondiente al diagrama de la Figura 4.38.

Una gramtica independiente del contexto G es una gramtica LR(0) si y slo si su tabla de anlisis LR(0) es determinista, es decir, no presenta conflictos.

4.3.5. Anlisis SLR(1)


SLR(1) es una tcnica de anlisis que simplifica la tcnica LR(1), que se ver posteriormente. Toma su nombre de las siglas en ingls de la expresin LR(1) sencillo. El estudio del ejemplo de conflicto de la seccin anterior sugiere una solucin. Es fcil comprobar que la gramtica GB puede generar la palabra begin dec ; ejec ; ejec end La Figura 4.40 muestra su rbol de derivacin. Al llegar a la segunda aparicin del smbolo ; (situada entre los dos smbolos ejec), el analizador LR(0) se encuentra en el estado s7. El smbolo siguiente que hay que procesar de la cadena de entrada es ;. A continuacin se analizar el efecto de cada una de las dos opciones sobre el rbol de derivacin.

Captulo 4. Anlisis sintctico

141

<Bloque>

<Decs> <Ejecs> <Ejecs>

begin

dec

ejec

ejec

end

Figura 4.40. rbol de derivacin de la palabra begin dec; ejec; ejec end por parte de la gramtica GB.

La Figura 4.41 muestra lo que ocurrira si se eligiese la opcin de la reduccin. Si se reduce la regla <Ejecs>::=ejec, el resto del subrbol que tiene a <Ejecs> como raz, que est resaltado en la Figura 4.41, no puede ser analizado tras la reduccin, porque <Ejecs> habra sido ya totalmente analizado. La Figura 4.42 refleja grficamente los pasos del analizador sobre el diagrama de transiciones y contiene una flecha que indica la secuencia en la que se visita cada estado. Se resaltan los estados y las transiciones de ese camino. Desde el estado inicial, tras desplazar el smbolo begin, se llega al estado s2. Al desplazar el siguiente terminal de la cadena de entrada (dec), se transita al estado s3, en el que se reduce la regla <Decs>::=dec. Cuando se reduce una regla, el algoritmo de anlisis elimina de la pila los smbolos almacenados en relacin con su parte derecha, y vuelve al estado en que se encontraba antes de comenzar a procesar dicha parte derecha. Ese estado se encuentra ahora con el smbolo no terminal que forma la parte izquierda de la regla que se est reduciendo. En este caso,

<Bloque>

Reduccin <Decs> <Ejecs> <Ejecs>

begin

dec

ejec

ejec

end

Figura 4.41. Efecto de la reduccin de la regla <Ejecs>ejec.

142

Compiladores e intrpretes: teora y prctica

s0 B::=B$, B::=bD;Ef b s2 B::=bD;Ef, D::=d, D::=D;d d f


D::=d D::=d

s1 B B::=B$ S4 B::=bD;Ef, D::=D;d D s6 B::=bD;Ef $

sacc
B::=B$ B::=B$

s5

B::=bD;Ef, E::=e, E::=e;E,

D::=D;d d e
D::=D;d D::=D;d

B::=bD;Ef B::=bD;Ef

s3

s9 s10 ; E::=e; E, E::=e;E,


E::=e, e, E::= E::=e;E e;E E::=

E::=e E::=e

s8 s7

e
E::=e;E E::=e;E

s11

Figura 4.42. Recorrido del analizador sintctico sobre la cadena begin dec; ejec; ejec end si el estado s7 fuera slo de reduccin.

antes de analizar la parte derecha dec, el analizador estaba en el estado s2 (eso es lo que representa el fragmento de la flecha que vuelve desde el estado s3 al s2). A continuacin, con el smbolo <Decs> se transita desde el estado s2 al estado s6. Los dos siguientes smbolos terminales (; y ejec) tambin dan lugar a desplazamientos a los estados s5 y s7, respectivamente. En este ltimo se reduce la regla <Ejecs>::=ejec y se vuelve al estado anterior al proceso de la parte derecha (ejec), que es, de nuevo, s5. El smbolo no terminal de la parte izquierda de la regla (<Ejecs>) hace que se transite a s6. Desde este estado slo se espera desplazar el terminal end para llegar al estado en el que se puede reducir un bloque completo. Sin embargo, el smbolo que hay que analizar en este instante es el terminal ejec. La transicin asociada a este smbolo no est definida, por lo que el anlisis terminara indicando un error sintctico. Intuitivamente, el error se ha originado porque se interpret que el smbolo ; indicaba el final de la lista de sentencias ejecutables (ejec), cuando realmente era su separador. El analizador slo tiene un comportamiento correcto posible: considerar el smbolo ; como lo que es, un separador, y desplazarlo. La Figura 4.43 muestra, en el rbol de derivacin, que este desplazamiento posibilita el xito del anlisis. Al desplazar el smbolo ;, se posibilita la reduccin posterior, primero de la segunda aparicin de ejec al smbolo no terminal <Ejecs>, y luego de ejec;<Ejecs> al smbolo no ter-

Captulo 4. Anlisis sintctico

143

<Bloque> Desplazamiento

<Decs> <Ejecs> <Ejecs>

begin

dec

ejec

ejec

end

Figura 4.43. Efecto de desplazar el smbolo ; en el anlisis de la palabra begin dec; ejec; ejec end.

minal <Ejecs>. De esta forma, el anlisis puede terminar con xito. Las Figuras 4.44 a 4.46 muestran el recorrido por el diagrama de estados correspondiente al anlisis completo.

s0 B::=B$, B::=bD;Ef b s2 B::=bD;Ef, D::=d, D::=D;d d f


D::=d D::=d

s1 B B::=B$ S4 B::=bD;Ef, D::=D;d D s6 B::=bD;Ef $

sacc
B::=B$ B::=B$

s5 ;

B::=bD;Ef, E::=e, E::=e;E, D::=D;d d

e
D::=D;d D::=D;d

s3

B::=bD;Ef B::=bD;Ef

E ::= ::= e E

s9 S10

E ::= ::=e;E e;E E

s8 s7

E::=e;E, E::=e, E::=e;E

e
E::=e;E E::=e;E

s11

Figura 4.44. Recorrido hasta el estado s7 que el anlisis sintctico debera realizar sobre el diagrama del analizador sintctico para analizar correctamente la cadena begin dec; ejec; ejec end.

144

Compiladores e intrpretes: teora y prctica

s0 B::=B$, B::=bD;Ef b s2 B::=bD;Ef, D::=d, D::=D;d d f D::=d D::=d s3 s9 s10 D s6 B

s1 B::=B$ s4 B::=bD;Ef, D::=D;d $

sacc
B::=B$ B::=B$

s5 ; E B::=bD;Ef B::=bD;Ef, E::=e, E::=e;E, D::=D;d d e


D::=D;d D::=D;d

B::=bD;Ef B::=bD;Ef

E::=e E::=e ;
E::=e E::= e;E

s8 s7

E::=e;E, E::=e, E::=e;E

e
E::=e;E E::=e;E

s11

Figura 4.45. Recorrido hasta la ltima reduccin que el anlisis sintctico debera realizar sobre el diagrama del analizador sintctico para analizar correctamente la cadena begin dec; ejec; ejec end.

Obsrvese que, en este caso, se tienen que considerar las dos configuraciones del estado s7. Primero se aplica la configuracin de desplazamiento, que aparece subrayada en la Figura 4.44. De esta manera se llega al estado s10. Con el desplazamiento correspondiente a la siguiente aparicin del terminal ejec se vuelve al estado s7, pero ahora, en presencia de end, que es el siguiente smbolo terminal analizado, slo se puede reducir y sustituir ejec por <Ejecs>. La Figura 4.45 muestra los pasos de anlisis siguientes. Tras la reduccin, que supone eliminar de la pila todo lo que corresponde a la parte derecha de la regla, el analizador se encuentra de nuevo en el estado s10 y tiene que procesar el smbolo no terminal recin incorporado a la cadena de entrada (<Ejecs>). As se llega al estado s11 en el que se reducen las dos apariciones de ejec. El analizador vuelve al estado en el que se encontraba antes de la primera aparicin de ejec, es decir, en el estado s5. Con el smbolo no terminal de la parte izquierda (<Ejecs>) se transita de s5 a s6. El siguiente smbolo para analizar es end. Su desplazamiento lleva al estado en el que se reduce el bloque de sentencias completo. La Figura 4.46 muestra los pasos finales del anlisis. Tras reducir el bloque completo, se vuelve al estado inicial. Con el smbolo no terminal de la parte izquierda de la regla reducida (<Bloque>) se llega a s1, desde donde se desplaza el smbolo final de la cadena y se llega al estado de aceptacin, lo que completa con xito el anlisis.

Captulo 4. Anlisis sintctico

145

s0 B::=B$, B::=bD;Ef b s2 B::=bD;Ef, D::=d, D::=D;d d f D::=d s3 s9 s10 D S6 B

s1 B::=B$ s4 B::=bD;Ef, D::=D;d $

sacc
B::=B$ B::=B$

s5 ; E B::=bD;Ef B::=bD;Ef, E::=e, E::=e;E, D::=D;d d e


D::=D;d D::=D;d

B::=bD;Ef B::=bD;Ef

E::=e E::=e ;
E::=e;E E::= e;E

s8 s7

E::=e;E, E::=e, E::=e;E

e
E::=e;E E::=e;E

s11

Figura 4.46. ltimos pasos del recorrido que el anlisis sintctico debera realizar sobre el diagrama del analizador sintctico para analizar correctamente la cadena begin dec; ejec; ejec end.

Lo ms relevante de este anlisis es la razn por la que el analizador no debe reducir en la casilla del conflicto: en el estado s7, el smbolo ; debe interpretarse siempre como separador de sentencias ejecutables. La reduccin slo debe aplicarse cuando se ha llegado al final de la lista de smbolos ejec, y esto ocurre slo cuando aparece el smbolo terminal end. Con el anlisis LR(0) es imposible asociar el estado s7 y el terminal end. En la prxima seccin se ver con detalle que esta solucin se basa en el hecho de que el smbolo no terminal <Ejecs> (la parte izquierda de la regla que se reduce en s7) siempre debe venir seguido por el smbolo terminal end. Este conflicto no se habra producido si en lugar de reducir la regla en todas las columnas de la parte de accin de la fila 7, slo se hubiera hecho en las columnas de los smbolos terminales que pueden seguir a <Ejecs>, que es la parte izquierda de la regla que se va a reducir.

Construccin de tablas de anlisis


En la Seccin 4.1 se presentaron dos conjuntos importantes de las gramticas independientes del contexto. Uno de ellos, el conjunto siguiente, contiene el conjunto de smbolos que pueden seguir a otro en alguna derivacin. Este conjunto puede utilizarse para generalizar las reflexiones de la seccin anterior, y es el origen de la tcnica llamada SLR(1).

146

Compiladores e intrpretes: teora y prctica

T E 0 1 2 3 4 5 6 7 8 9 10 11 r5 r4 r3 r1 r4 r3 r1 d7 r5 r5 r5 r5 r5 r4 r3 r1
d10/ r4

N ; f
E $ T B

b d2

1 acc

d3 r2 r2 r2 r2 d5 d8 d7 d9 r4 r3 r1 r2

5 r2

r4 r3 r1 11

r3 r1

Accin

Ir_a

T (0)B B$ (1)B bD;Ef (2)D d (3)D D;d (4)E e (5)E e;E E 0 1 2 3 siguiente (B)={$} siguiente (D)={;} siguiente (E)={f} 4 5 6 7 8 9 10 11 Accin d7 r5 d8 d7 d9 d10 r4 r3 r1 d3 r2 d5 d e b d2 acc 5 ; f
E $ T B

N D E

11

Ir_a

Figura 4.47. Tablas de anlisis LR(0) y SLR(1) para la gramtica del ejemplo. La parte superior muestra la tabla LR(0) y resalta las casillas que cambiarn de contenido. La parte inferior muestra la tabla SLR(1). A su izquierda estn los conjuntos auxiliares que justifican su contenido.

Captulo 4. Anlisis sintctico

147

La tabla de anlisis se construye de la misma manera que con la tcnica LR(0) (vase la Seccin 4.3.3), excepto por las reducciones: Reducciones. Igual que en el caso LR(0), se consultan los estados finales del diagrama, excepto el estado de aceptacin. Si el estado final es si y su configuracin de reduccin es N::= (donde N::= es la regla k), la accin rk se aadir slo en las casillas correspondientes a las columnas de los smbolos terminales del conjunto siguiente(N), ya que slo ellos pueden seguir a las derivaciones de N. Veamos el contenido de los conjuntos siguiente de los smbolos no terminales del Ejemplo 4.9: siguiente(<Bloque>)={$} siguiente(<Decs>)={;} siguiente(<Ejecs>)={end} Esto significa que en las filas correspondientes a los estados en los que se reduce una regla cuya parte izquierda sea <Bloque>, slo hay que colocar la accin de reduccin en la casilla correspondiente al smbolo $. En los estados en que se reduzca una regla cuya parte izquierda sea <Decs>, hay que hacerlo slo en la columna correspondiente a ;, y en los estados en que se reduzca una regla cuya parte izquierda sea <Ejecs>, hay que hacerlo slo en la columna encabezada por end. La Figura 4.47 compara las tablas de anlisis LR(0) y SLR(1) para el Ejemplo 4.9. Puede comprobarse que ha desaparecido el conflicto reduccin / desplazamiento de la tabla LR(0).

Definicin de gramtica SLR(1)


Una gramtica independiente del contexto G es una gramtica SLR(1) si y slo si su tabla de anlisis SLR(1) es determinista, es decir, no presenta conflictos.

4.3.6. Ms all de SLR(1)


Hay alguna gramtica independiente del contexto que no sea SLR(1)? Es decir, existen gramticas independientes del contexto cuyas tablas de anlisis SLR(1) siempre presentan conflictos? Ejemplo Considrese la gramtica Gaxb que contiene las siguientes reglas de produccin y cuyo axioma es 4.10 el smbolo S: (1)S ::= A (2)S ::= xb (3)A ::= aAb (4)A ::= B (5)B ::= x Es fcil comprobar que esta gramtica genera el lenguaje {xb, anxbn | n0}. A continuacin se va a construir su tabla de anlisis SLR(1). En primer lugar se aumenta la gramtica con la produccin (0)S::=S$ y se construye el diagrama de estados del autmata de anlisis LR(0) que muestra la Figura 4.48.

148

Compiladores e intrpretes: teora y prctica

s1
S::=S S::=S

s4
A::=B A::=B

s9
A::=aAb A::=aAb

S s0

s2 B
S::=A S::=A

s7

b A::=aAb

S::=S S::=A S::=xb A::=aAb A::=B B::=x

A s3

B A A::=aAb A::=aAb A::=B B::=x a x a s8


B::=x B::=x

sacc
S::=S$ S::=S$

x s5 S::=x b S::=xb
B::=x B::=x

s6 b
S::=xb S::=xb

Figura 4.48. Diagrama de estados de la gramtica Gaxb.

Es fcil calcular el valor el conjunto siguiente: siguiente(A)={$,b} siguiente(B)={$,b} siguiente(S)={$} La tabla de anlisis SLR(1) de Gaxb se muestra en la Figura 4.49. La presencia del estado s5, que contiene la reduccin de la regla (5)B::=x, y el hecho de que b siguiente(B), y que con b se pueda transitar desde s5 a s6, originan un conflicto de tipo reduccin / desplazamiento.

4.3.7. Anlisis LR(1)


Considrese Gaxb, ejemplo de una gramtica que no es SLR(1). El smbolo b pertenece a siguiente(B) a causa de las reglas A::=B y A::=aAb. Por la primera se llega a la conclusin de que los smbolos que sigan al rbol de derivacin de B tienen que contener a los que sigan al de A. Por la segunda queda claro que b es uno de esos smbolos. Por lo tanto, para que haya una b detrs del rbol de derivacin de B, tiene que ocurrir que antes del rbol hubiera una a (ya que

Captulo 4. Anlisis sintctico

149

T E 0 1 siguiente(A)={$,b} siguiente(B)={$,b} siguiente(S)={$} 2 3 4 5 6 7 8 9 r5 r3 Accin r5 r3 d3 r4


r5/d6

N x d5 acc r1 d8 r4 r5 r2 7 4 $
T S

a d3

A 2

B 4

Ir_a

Figura 4.49. Tabla de anlisis SLR(1) de la gramtica Gaxb.

AaAbaBbaxb). El rbol de derivacin debe reducir x a B, que a su vez se reducir a A. Tras hacer todo esto es cuando se puede encontrar la b. Todo lo anterior asegura que la reduccin de B::=x sera posible (en las circunstancias descritas) antes de una b. En el diagrama de estados (vase Figura 4.48) hay dos estados (s5 y s8) en los que se puede reducir la regla Bx, que corresponden a dos fragmentos distintos de anlisis desde el estado inicial, que se pueden comparar en la Figura 4.50. Para llegar a s8 desde s0, es necesario desplazar previamente el terminal a y luego el x. Para llegar a s5 basta con el smbolo x. Por lo tanto, la reduccin de B::=x antes de una b es la que se realiza en el estado s8. Cundo sera correcto reducirla en el estado s5? La Figura 4.51 muestra el rbol de la derivacin SS$A$B$x$. En este caso, la reduccin correspondera al estado s5, ya que desde el estado inicial se ha desplazado una x no precedida de una a. Por lo tanto, es cierto que los smbolos terminales que pueden seguir a B son $ y b, pero en algunos estados del diagrama (s8) la reduccin slo puede ser seguida por $ y en otros (s5) slo por b. La tabla de anlisis SLR(1), que est dirigida por el conjunto siguiente, carece de la precisin suficiente para gestionar estas gramticas. Obsrvese que una posible solucin consistira en que las configuraciones de reduccin aparecieran en los estados junto con la informacin relacionada con los smbolos en presencia de los cuales la reduccin es posible. En este caso, s5 estara ligado a Bx:$ y s8 a Bx:b.

150

Compiladores e intrpretes: teora y prctica

s1
S::=S S::=S

s4
A::=B A::=B

s9
A::=aAb A::=aAb

S s0

s2 B
S::=A S::=A

s7

b A::=aAb

S::=S S::=A S::=xb A::=aAb A::=B B::=x

A s3

B A sacc A::=aAb A::=aAb A::=B B::=x a x a s8


B::=x B::=x S::=S$ S::=S$

x s5 S::=x b S::=xb
B::=x B::=x

s6 b
S::=xb S::=xb

a) s1 S::=S s2 B
S::=A S::=A

s4
A::=B A::=B

s9
A::=aAb A::=aAb

S s0

s7

b A::=aAb

S::=S S::=A S::=xb A::=aAb A::=B B::=x

A s3

B A A::=aAb A::=aAb A::=B B::=x a x a s8


B::=x B::=x

sacc
S::=S$ S::=S$

x s5 S::=x b S::=xb
B::=x B::=x

s6 b
S::=xb S::=xb

b)

Figura 4.50. Comparacin entre los dos posibles recorridos previos a las reducciones de la regla B::=x: (a) en el estado s5, (b) en el estado s8.

Captulo 4. Anlisis sintctico

151

Figura 4.51. rbol de derivacin de la cadena x$ por la gramtica Gaxb.

Descripcin intuitiva del anlisis LR(k), k>0


La posible mejora sugerida al final de la seccin anterior es generalizable. A lo largo de las prximas secciones, se formalizar mediante el conjunto de smbolos de adelanto, para describir la tcnica de anlisis LR(k) con k>0. A partir de este punto, se llamar conjunto de smbolos de adelanto a los smbolos que, en un estado concreto del diagrama, se espera encontrar en cada una de sus configuraciones. Hasta ahora, una configuracin representaba una hiptesis en curso, en el proceso del anlisis, definida por la posicin del apuntador de anlisis en la parte derecha de una regla compatible con la porcin de cadena de entrada analizada. En este nuevo tipo de anlisis se aadir a cada configuracin los smbolos de adelanto correspondientes. Tras aadir esta informacin, la configuracin N::= {1,,m},, NN ,(NT)* {1,,m}T significa que, en este instante, una de las hiptesis posibles est relacionada con la regla N; en particular, el prefijo de la parte derecha es compatible con la porcin de la entrada analizada hasta este momento; adems, esto slo es posible si, tras terminar con esta regla, el siguiente smbolo terminal pertenece al conjunto {1,,m}. En esto se basa el aumento de precisin del anlisis LR(1) sobre SLR(1). Lo que en SLR(1) era una nica hiptesis, se multiplica ahora con tantas posibilidades como conjuntos diferentes de smbolos de adelanto. Introduccin al clculo de smbolos de adelanto. Vamos a construir el diagrama de estados del autmata de anlisis LR(1) de la gramtica Gaxb del Ejemplo 4.10. Se mantendr el mismo

152

Compiladores e intrpretes: teora y prctica

algoritmo bsico, al que se incorpora el clculo de los conjuntos de smbolos de adelanto de cada configuracin de cada estado. Empezaremos con un mecanismo para calcular el conjunto de smbolos de adelanto: De la configuracin inicial del estado inicial (A::=A$). De las configuraciones del cierre de una configuracin. De las configuraciones resultado de ir a otro conjunto de configuraciones mediante un smbolo. En el ejemplo, la configuracin inicial del estado inicial es S::=S$. Para preservar la intuicin es frecuente, en el anlisis LR(1), considerar que la primera configuracin de la nueva regla de la gramtica ampliada es S::=S, prescindiendo del smbolo final ($). Expresada de esta forma, la hiptesis inicial indica que el apuntador de anlisis se encuentra antes del primer smbolo de la cadena de entrada y que se espera poder reducirla toda ella al smbolo no terminal S. Resulta claro que el nico smbolo que se puede esperar, tras procesar completamente la regla, es el smbolo que indica el final de la misma. Por tanto, el conjunto de smbolos de adelanto de la configuracin inicial del estado inicial en el anlisis LR(1) es {$}. Para completar el estado inicial, hay que aadir las configuraciones de cierre({S::=S {$}}), lo que implica calcular los conjuntos de smbolos de adelanto para S::=A, A::=B, B::=x, A::=aAb y S::=xb. Hemos visto que, tras procesar por completo S::=S, hay que encontrar el smbolo $. Esto implica que, si S se redujo mediante la regla S::=A, tras A se puede encontrar lo mismo que se encontrara tras S, es decir, $. Este razonamiento vale para todas las configuraciones y justifica que el nuevo conjunto de smbolos de adelanto sea {$}, lo que esquematiza grficamente la Figura 4.52, que representa los cierres sucesivos mediante rboles de derivacin concatenados. La Figura 4.53 muestra grficamente cmo se completa el clculo del estado inicial, que resulta ser: s0={s ::= S {$}, S ::= A {$}, A ::= B {$}, B ::= x {$}, A ::= aAb {$}, S ::= xb {$}} El caso analizado en este estado no es el ms general que puede aparecer al realizar la operacin cierre. De hecho, puede inducir a engao que en este caso no se modifique el conjunto de smbolos de adelanto porque, como se ver en los prximos prrafos, es esta operacin la que puede modificarlos. Como sugiere la Figura 4.52, la razn por la que en este caso no se modifican los smbolos de adelanto es que el cierre se ha aplicado en todos los casos a configuraciones con la estructura: N::=A {1,... ,m},, N, AN (NT)* {1,... ,m}T Tras el smbolo no terminal que origina el cierre, no hay ningn otro smbolo terminal. Por lo tanto, lo que se encontrar tras l (en este caso A) es lo mismo que se encuentra cuando se termina de procesar cualquiera de sus reglas (A ::= ).

Captulo 4. Anlisis sintctico

153

S::=S {$}

S::=S

{$}

S::=S

{$}

A c) S::=S $ A b)

x d)

S::=S

{$}

{$}

f) $ a) S::=S {$} aAb $ e)

xb

Figura 4.52. Ejemplo de situacin de cierre que no modifica el conjunto de smbolos de adelanto: (a)S::=S {$} (b) S::=A {$} (c) A::=B {$} (d) B::=x {$} (e) A::=aAb {$} (f) S::=xb {$}

Para ilustrar esta situacin, considrese a continuacin la operacin que calcula s5=ir_a(s0,a). A::=aAb{$} es la nica configuracin de s0 relacionada con esta operacin. Hay que calcular, por tanto, el conjunto de smbolos de adelanto de A::=aAb. Parece lgico concluir que lo que se espere encontrar tras terminar con la parte derecha de la regla no cambiar porque se vayan procesando smbolos en ella. El conjunto buscado coincide con {$} y se puede extraer la siguiente conclusin: El conjunto de smbolos de adelanto de una configuracin no vara cuando se desplaza el apuntador de anlisis hacia la derecha a causa de transiciones entre estados.

154

Compiladores e intrpretes: teora y prctica

s0 S::=S{$} S::=A{$} S::=xb{$} A::=aAb{$} A::=B{$} B::=x{$}

Figura 4.53. Estado inicial del autmata de anlisis LR(1) de la gramtica Gaxb.

Para concluir los clculos para la operacin ir_a, se realiza el cierre de la configuracin resultado de desplazar a: ir_a(s0,a)=cierre( { A ::= aAb {$}} ) Hay que calcular los conjuntos de smbolos de adelanto para A::=xb y A::=B. En este caso, lo fundamental es la b que sigue a A (A ::= aAb). Aplicando el mismo razonamiento de los prrafos anteriores, la hiptesis de esa configuracin es que la prxima porcin de la cadena de entrada tiene que permitir reducir alguna regla de A, y luego desplazar la b ha de que seguirla obligatoriamente. Por eso, tras procesar completas las reglas de A, se tendr que encontrar una b (la resaltada en las lneas anteriores) y {b} es el conjunto de smbolos de adelanto buscado. El resto de las configuraciones no presentan novedades respecto a lo expuesto anteriormente. Obtendremos, por tanto, el siguiente estado: s5={A ::= aAb {$}, A ::= aAb {b}, A ::= B {b}, B ::= x {b}} La Figura 4.54 muestra el diagrama de estados correspondiente a esta situacin.

s0 S::=S{$} S::=A{$} S::=xb{$} A::=aAb{$} A::=B{$} B::=x{$} a s5 A::=aAb{$} A::=aAb{b} A::=b{b} B::=x{b}

Figura 4.54. Diagrama con los dos primeros estados del autmata del analizador LR(1) de la gramtica Gaxb.

Captulo 4. Anlisis sintctico

155

Como se ha visto, el conjunto de smbolos de adelanto puede variar cuando se cierra una configuracin con la siguiente estructura: P::=N {1,,m},,P,NN (N T)*( N T)+{1,,m}T El caso analizado contiene una cadena que se reduce a un nico smbolo no terminal. En general, puede ser cualquier cadena.

Autmata asociado a un analizador LR(1): definiciones formales


A continuacin se describirn las diferencias entre los anlisis LR(0) y LR(1). Gramtica aumentada. Como se ha descrito informalmente en las secciones anteriores, la interpretacin del smbolo de adelanto para la regla aadida a la gramtica de partida origina algunas diferencias en el anlisis LR(1). Dada cualquier gramtica independiente del contexto G=<T, N, A, P>, la gramtica extendida para LR(1) se define as, donde AN y $T: G=<T, N{A}, A, P{A ::= A}> Es fcil comprobar que el lenguaje generado por G es el mismo que el generado por G. Obsrvese que el smbolo $ no aparece de forma explcita en G. Sin embargo, la restriccin impuesta sobre l es necesaria, porque en la construccin del autmata de anlisis LR(1) el smbolo $ se utilizar como el nico smbolo de adelanto para la configuracin del estado inicial. Construccin de los conjuntos de smbolos de adelanto. El nico cambio en los conceptos descritos formalmente en la seccin Autmata asociado a un analizador LR(0): definiciones formales es la incorporacin del clculo del conjunto de smbolos de adelanto al algoritmo general: La configuracin inicial del estado inicial (A::=A) tiene como conjunto de smbolos de adelanto {$}. Dicho de otro modo: A ::= A {$} s0 Dado un estado cualquiera (si) del autmata, cuando se calcula cierre(si) P::=N {1,... ,m}si,, P,NN ,(NT)* {1,... ,m}T N::= primero_LR(1)(.{1,... ,m}) cierre(si). Donde: 1. .{1,... ,m}no define ninguna operacin, sino que es una notacin que se refiere a la concatenacin de una cadena y un conjunto de smbolos {1,... ,m}. 2. primero_LR(1) es un conjunto que se puede definir en funcin del conjunto primero de la siguiente manera: primero_LR(1)(.{1,... ,m})=i=1,...,m{primero (i)} donde i representa la concatenacin habitual de smbolos.

156

Compiladores e intrpretes: teora y prctica

s1 S::=S{$} s2

s3 A::=B{$} S::=A{$} A s5

s10

A::=aAb{$} b
1primero 2primero

S s0

A::=aAb{$} A

($)={$} (b$)={b} 3primero (b)={b} 4primero (bb)={b}

S::=S{$} S::=A{$}1 S::= xb{$}1 A::=aAb{$} A::=B{$} B::=x{$}1 a

s5 A::=aAb{b} A::= aAb{b}2 A::=B{b}2 B::=x{b}3 a B x s9

s7

A::=aAb{b} A::=aAb{b}4 A::=B{b}4 B::=x{b}3 a x s11 A::=aAb{b} B b s12 A

x s4 S::=xb{$} B::=x{$} b s13 S::=xb{$}

B::=x{b} s8 A::=B{b}

A::=aAb{b}

Figura 4.55. Diagrama de estados del autmata del analizador LR(1) de la gramtica Gaxb.

Tambin es posible calcular este conjunto de la siguiente manera: primero_LR(1)(.{1,... ,m})= primero() si primero() primero()- {1,... ,m} en otro caso

Dado un estado cualquiera (si) del autmata, y un smbolo cualquiera (XNT), cuando se calcula ir_a(si,X) P::=X siir_a(si,X) cierre({P::=X}). La Figura 4.55 muestra el diagrama de estados completo del analizador LR(1) de la gramtica Gaxb.

Observaciones sobre la naturaleza del conjunto de smbolos de adelanto


El conjunto de smbolos de adelanto no siempre contiene smbolos aislados. La Seccin 4.3.8 se dedica al anlisis LALR(1), en el que, de forma natural, se construyen conjuntos de smbolos de adelanto con ms de un smbolo. Otra circunstancia en la que puede aparecer este tipo de conjuntos es en el clculo del diagrama de estados, cuando en alguno de ellos surge la necesidad de incluir varias veces la misma configuracin con diferentes smbolos de adelanto. En

Captulo 4. Anlisis sintctico

157

este caso, el conjunto de smbolos de adelanto tiene que incluir todos los smbolos identificados. Como ejemplo, considrese la siguiente gramtica independiente del contexto para un fragmento de un lenguaje de programacin de alto nivel que permite el tipo de dato apuntador, con una notacin similar a la del lenguaje C. La gramtica describe algunos aspectos de las asignaciones a los identificadores que incluyen posibles accesos a la informacin apuntada por un puntero, mediante el uso del operador *. G*={ T={=,*,id}, S, N={S,L,R}, { SL=R | R, L*R | id, RL } }

La Figura 4.56 muestra el diagrama de estados del autmata de anlisis LR(1). Puede observarse en el estado s0 la presencia de dos configuraciones cuyos conjuntos de smbolos de adelanto contienen dos smbolos. En el caso de L::=*R{=,$} la presencia del smbolo = se justifica porque es el que sigue al no terminal L en la configuracin que se est cerrando (S::L=R{$}). Es necesario aadir tambin el smbolo $, ya que tambin hay que cerrar la configuracin R::L{$}, y en esta ocasin el smbolo no terminal L aparece al final de

s1 S::=S {$} R s2 s0 S::= S{$} S::= L=R{$} S::= R{$}

s3

S::=R {$} S::=L =R{$} R::=L {$} L s5 L::= id {$,=} id id * s4 L::=* R{$,=} R::= L{$,=} L::= *R{$,=} L::= id {$,=} * L s10 R::=L {$} L s11 L

s6 =

S::=L= R{$} R::= L{$} L::= *R{$} L::= id{$}

R * S::=L=R {$} s9

id L::=id {$} s13 id

L::=id{$,=} R::=L{$}
L::= *R{$,=} s7

L::=*R {$,=} R::=L {$,=} s8

L::=*R{$} L::=id{$}
L::=* R{$} R::= L{$}

L::=*R {$} R * s12

Figura 4.56. Ejemplo de diagrama de estados de autmata de anlisis LR(1) con conjuntos de smbolos de adelanto no unitarios.

158

Compiladores e intrpretes: teora y prctica

la parte derecha de la regla y no se modifican los smbolos de adelanto ({$}). El anlisis para la configuracin L::=id{=,$} es similar.

Construccin de tablas de anlisis LR(1)


La tabla de anlisis tiene la misma estructura que la tabla LR(0), con la excepcin de que en LR(1) la columna del smbolo de final de cadena ($) aparece por convenio, ya que no pertenece estrictamente al conjunto de terminales. Desplazamientos. Igual que en LR(0), se obtienen siguiendo las transiciones del diagrama. Si el autmata transita del estado si al estado sj mediante el smbolo x, en la casilla (i,x) de la tabla se aadir dj si x T y j si xN. Reducciones. No se aplica el mismo proceso que en las tablas LR(0) ni SLR(1). Igual que en LR(0), se consultan los estados finales del diagrama, pero en este caso se utilizan los conjuntos de smbolos de adelanto de las configuraciones de reduccin. Si el estado final es si y su configuracin de reduccin es N::={1,... m,} (donde N::= es la regla nmero k), la accin rk se aadir slo en las casillas correspondientes a las columnas de los smbolos de adelanto {1,... m,}. Aceptacin. No se aplica el mismo proceso que en las tablas LR(0), ya que el smbolo final de la cadena $ no forma parte explcitamente de la gramtica, sino que aparece slo en los smbolos de adelanto. La accin de aceptacin se escribe en la casilla (i, $), siempre que la i represente al estado si que contiene la configuracin A::=A{$}. Error. Igual que en las tablas LR(0), todas las dems casillas corresponden a errores sintcticos. Como ejemplo, la Figura 4.57 muestra la tabla de anlisis LR(1) de la gramtica Gaxb. Puede comprobarse que se ha resuelto el conflicto que haca que Gaxb no fuese SLR(1).

Definicin de gramtica LR(1)


Una gramtica independiente del contexto G es una gramtica LR(1) si y slo si su tabla de anlisis LR(1) es determinista, es decir, no presenta conflictos.

Evaluacin de la tcnica
Comparando el tamao de los diagramas de estado y de las tablas de anlisis SLR(1) y LR(1) para la gramtica Gaxb, que aparecen respectivamente en las Figuras 4.48, 4.49, 4.55 y 4.56, se comprueba que el aumento de precisin para solucionar el conflicto implica un aumento considerable en el tamao de ambos elementos. Es fcil ver, sobre todo en los diagramas de estado, que los nuevos estados s7, s8, s11 y s12 se originan como copias, respectivamente, de los antiguos estados s3, s4, s7 y s9, con smbolos de adelanto distintos. Se puede demostrar que LR(1) es el algoritmo de anlisis ms potente entre los que realizan el recorrido de la cadena de entrada de izquierda a derecha con ayuda de un smbolo de adelanto. Tambin tiene inters la extensin de este algoritmo de anlisis a un conjunto mayor de smbolos de adelanto. Como se ha dicho previamente, estas extensiones se denominan LR(k), donde

Captulo 4. Anlisis sintctico

159

T E 0 1 2 3 4 5 6 7 8 9 10 11 12 13 Accin d12 r3 r2 d7 r4 r5 r3 d7 d10 d9 d13 d9 a d5 b x d4 acc r1 r4 r5 $ S S 1

N A 2 B 3

11

Ir_a

Figura 4.57. Tabla de anlisis LR(1) para la gramtica Gaxb.

k representa la longitud de los elementos de los conjuntos de adelanto. En la prctica, las gramticas LR(1) son capaces de expresar las construcciones presentes en la mayora de los lenguajes de programacin de alto nivel. El incremento observado en el tamao de los diagramas de estado y las tablas de anlisis se acenta cuando se utiliza un valor de k mayor que 1. Por ello, en la prctica, los compiladores e intrpretes no suelen utilizar valores de k mayores que 1. En la seccin siguiente no se intentar incrementar la potencia expresiva de los analizadores ascendentes, sino slo mitigar la ineficiencia derivada del aumento del tamao de las tablas de anlisis al pasar de los analizadores LR(0) y SLR(1) a LR(1).

4.3.8. LALR(1)
Las siglas LALR hacen referencia a una familia de analizadores sintcticos ascendentes que utilizan smbolos de adelanto (de la expresin inglesa Look-Ahead-Left-to-Right). El nmero que acompaa a LALR tiene el mismo significado que en LR(k).

160

Compiladores e intrpretes: teora y prctica

Motivacin
Despus de recorrer las diferentes tcnicas del anlisis ascendente, desde LR(0) hasta LR(1), pasando por SLR(1), se llega a la conclusin de que la potencia del anlisis LR(1), y la falta de precisin del anlisis SLR(1), se deben a que, aunque los conjuntos de smbolos de adelanto y los conjuntos siguiente pueden estar relacionados (los smbolos de adelanto de una configuracin parecen, intuitivamente, estar incluidos en el conjunto siguiente del no terminal de la parte izquierda de su regla), tienen significados distintos. Que un smbolo terminal pueda seguir a la parte izquierda de una regla no significa que tenga que aparecer, cada vez que se reduzca, a continuacin de ella. De hecho, los conjuntos siguiente dependen slo de la regla, mientras que los de adelanto dependen de la configuracin y de su historia. El estudio del diagrama de la Figura 4.55 muestra la existencia de estados que slo se diferencian en los smbolos de adelanto de sus configuraciones. Ante esta situacin cabe formularse la siguiente pregunta: sera posible minimizar el nmero de estados distintos, realizando la unin de todos los smbolos de adelanto y excluyendo de los conjuntos siguientes los smbolos que realmente no pueden aparecer inmediatamente despus de reducir la regla de la configuracin correspondiente? Las prximas secciones se dedicarn a comprobar que la respuesta es afirmativa y a articular una nueva tcnica de anlisis ascendente que hace uso de ella. Ejemplo A continuacin se revisar el ejemplo de la gramtica Gaxb desde el punto de vista descrito en la 4.11 seccin anterior. La Figura 4.58 resalta cuatro parejas de estados (s3 y s8, s5 y s7, s6 y s11, s10 y s12) en el diagrama de estados del autmata de anlisis LR(1). Son cuatro parejas de estados distintos, que slo difieren en los smbolos de adelanto de sus configuraciones. Se intentar reducir el tamao del diagrama agrupando esos estados. El lector familiarizado con la teora de autmatas reconocer esta situacin como la de minimizacin de un autmata. En cualquier caso, la reduccin es un proceso iterativo, en el que dos estados se transformarn en uno solo siempre que sean equivalentes. La equivalencia de estados se basa en las siguientes condiciones: Que slo difieran en los smbolos de adelanto de las configuraciones. El estado que se obtiene al unir los de partida, contendr en cada configuracin la unin de los smbolos de adelanto. El nuevo estado debe mantener las transiciones del diagrama, es decir, tienen que llegar a l todas las transiciones que llegaran a los de partida y salir de l todas las que salieran de ellos. El proceso termina cuando no se puedan agrupar ms estados. Pareja s3 - s8: La Figura 4.59 muestra el proceso de unin de estos dos estados, que da lugar al estado nuevo s3_8. Obsrvese que la unin es posible porque La configuracin de los dos estados slo difiere en los smbolos de adelanto. Las dos transiciones que llegan desde s0 a s3 y desde s5 y s7 a s8 pueden sin problemas llegar a s3_8. No hay transiciones que salgan de s3 ni de s8.

Captulo 4. Anlisis sintctico

161

s1 S::= S{$} s2

2 s3 A::= B{$} S::=A{$} A S::=S{$} S::=A{$} S::=xb{$} A::=aAb{$} A::=B{$} B::=x{$} a x x s9 B::=x{b} B S::=xb{$} B::=x{$} b s13 S::=xb{$} s8 2 A::=B{b} s5 A::=aAb{$} A::=aAb{b} A::=B{b} B::=x{b} a 3 s6 4 A::=aAb{$} A s7 s10

1 A::=aAb{$} b

S s0

3 A::=aAb{b} A::=aAb{b} A::=B{b} B::=x{b} a x B b s12 1 A::=aAb{b} 4 A::=aAb{b} s11 A

s4

Figura 4.58. Diagrama de estados del autmata de anlisis LR(1) de la gramtica Gaxb en el que se indican los conjuntos de smbolos distintos que slo difieren en los smbolos de adelanto de sus configuraciones.

El estado resultante es: s3_8={A::=B {$,b}} Pareja s10 - s12: La Figura 4.60 muestra el proceso para esta pareja. Por razones anlogas, la unin de los dos estados es posible y su resultado es s10_12={A::=aAb {$,b}}. Pareja s6 - s11: La Figura 4.61 muestra el proceso para esta pareja. Este caso presenta una situacin nueva: tanto s6 como s11 tienen transiciones de salida mediante el smbolo b. La unin es posible, porque las dos llegan al estado nuevo s10_12, por lo que el estado resultado (s6_11) tendr una transicin con el smbolo b al estado s10_12. Es conveniente reflexionar acerca del orden en que se realizan las uniones. Si se hubiera intentado unir esta pareja antes que s10 - s12, la unin no habra sido posible. No debe preocupar esta situacin, ya que, al ser el proceso iterativo, tarde o temprano se habra unificado la pareja 10 - 12, y despus de ella tambin la 6 - 11. En cualquier caso el resultado es s6_11={A::=aAb {$,b}}

162

Compiladores e intrpretes: teora y prctica

s1

S::=S{$} s2

s3_8 A::=B{$} s::=A{$} A B s5 A::=aAb{$} A::=aAb{b} A::=B{b} B::=x{b} a B a x s9 s5

s10

A::=aAb{$} b

S s0

A::=aAb{$} A s7 A::=aAb{b} A::=aAb{b} A::=B{b} B::=x{b} a x B::=x{b} A::=aAb{b} B b s12 A::=B{b} a) A::=aAb{b} s11 A B

S::=S{$} S::=A{$} S::=xb{$} A::=aAb{$} A::=B{$} B::=x{$}

x s4 S::=xb{$} B::=x{$} b s13 S::=xb{$} s8

s1 S::=S{$} s2

s3_8 A::=B{$,b} S::=A{$} A B s5 A::=aAb{$} A::=aAb{b} A::=B{b} B::=x{b} a x s9 a s6

s10

A::=aAb{$} b

S s0

A::=aAb{$} A s7 A:;=aAb{b} A::=aAb{b} A::=B{b} B::=x{b} a x B::=x{b} s11 A::=aAb{b} b A B

S::=S{$} S::=A{$} S::=xb{$} A::=aAb{$} A::=B{$} B::=x{$}

x s4 S::=xb{$} B::=x{$} b s13 S::=xb{$}

s12 A::=aAb{b}

b)

Figura 4.59. Unin de los estados s3 y s8 en el nuevo estado s3_8. (a) Antes de la unin: se resaltan las transiciones afectadas. (b) Despus de la unin: se resalta el estado resultado.

Captulo 4. Anlisis sintctico

163

s1 S::=S{$} s2

s3_8 A::=B{b,$} S::=A{$} A B s5 s6

s10

A::=aAb{$} b

s12 A::=aAb{b} b s11

S s0

A::=aAb{$} A s7

A::=aAb{b} A A::=aAb{b} A::=aAb{b} A::=B{b} B::=x{b} a B

S::=S{$} S::=A{$} S::=xb{$} A::=aAb{$} A::=B{$} B::=x{$} a

A::=aAb{$} A::=aAb{b} A::=B {b} B::=x{b} a x s9

x B::=x{b}

x s4 S::=xb{$} B::=x{$} b a) s13 S::=xb{$}

s1 S::= S{$} s2

s3_8 A::=B{b,$} S::=A{$} A B s5 A::=aAb{$} A::=aAb{b} A::=B{b} B::=x{b} a x s9 a s6

s10_12 A::=aAb{$,b} b A::=aAb{$} A s7 b s11 A::=aAb{b} A A::=aAb{b} A::=aAb{b} A::=B{b} B::=x{b} a x B::=x{b} B

S s0

S::=S{$} S::=A{$} S::=xb{$} A::=aAb{$} A::=B{$} B::=x{$}

x s4 S::=xb{$} B::=x{$} b s13 S::=xb{$}

b)

Figura 4.60. Unin de los estados s10 y s12 en el nuevo estado s10_12.

164

Compiladores e intrpretes: teora y prctica

s1 S::=S{$} s2

s3_8 A::=B{b,$} S::=A{$} A B s5 A::=aAb{$} A::=aAb{b} A::=B{b} B::=x{b} a x s9 a s6

s10_12 A::=aAb{$,b} b A::=aAb{$} A s7 b s11 A::=aAb{b} A A::=aAb{b} A::=aAb{b} A::=B{b} B::=x{b} a x B::=x{b} B

S s0

S::=S{$} S::=A{$} S::=xb{$} A::=aAb{$} A::=B{$} B::=x{$}

x s4 S::=xb{$} B::=x{$} b a) s1 S::=S{$} s2 S::=A{$} A S::=S{$} S::=A{$} S::=xb{$} A::=aAb{$} A::=B{$} B::=x{$} a x x s4 S::=xb{$} B::=x{$} b s13 S::=xb{$} s9 s5 A::=aAb{$} A::=aAb{b} A::=B{b} B::=x{b} B s3_8 A::=B{b,$} s6_11 s13 S::=xb{$}

s10_12 A::=aAb{$,b} b

S s0

A::=aAb{$,b} A A s7 A::=aAb{b} A::=aAb{b} A::=B{b} B::=x{b} a a x B::=x{b} B

b)

Figura 4.61. Unin de los estados s6 y s11 en el nuevo estado s6_11.

Captulo 4. Anlisis sintctico

165

Pareja s5 - s7: La Figura 4.62 muestra el proceso para esta pareja.


s1

S::=S{$} s2

s3_8 A::=B{b,$} S::=A{$} A B s5 A::=aAb{$} A::=aAb{b} A::=B{b} B::=x{b} a x s9 a s6_11

s10_12 A::=aAb{$,b} b

S s0

A::=aAb{$,b} A s7 A A::=aAb{b} A::=aAb{b} A::=B{b} B::=x{b} a x B::=x{b} B

S::=S{$} S::=A{$} S::=xb{$} A::=aAb{$} A::=B{$} B::=x{$}

x s4 S::=xb{$} B::=x{$} b a) s13 S::=xb {$}

s1 S::=S{$} s2

s3_8 A::=B{b,$} S::=A{$} A B s5_7 A ::=aAb{$,b} A::=aAb{b} A::=B{b} B::=x{b} a a x s9 s6_11

s10_12 A::=aAb{$,b} b

S s0

A::=aAb{$,b} A

S::=S{$} S::=A{$} S::=xb{$} A::=aAb{$} A::=B{$} B::=x{$}

x s4 S::=xb{$} B::=x{$} b b) s13 S::=xb{$}

B::=x{b}

Figura 4.62. Unin de los estados s5 y s7 en el nuevo estado s5_7.

166

Compiladores e intrpretes: teora y prctica

Por razones anlogas (en este caso las transiciones potencialmente peligrosas llegan a s3_8 y a s6_11, que ya estn unificados) la unin es posible y el resultado es s5_7={A::=aAb {$,b}, A::= aAb {b}, A::= B {b}, B::= x {b}}

Construccin del autmata de anlisis LALR(1) a partir del autmata LR(1)


Se puede formalizar, en forma de pseudocdigo, el proceso descrito anteriormente, para calcular el diagrama de estados del analizador LALR(1) a partir del autmata del analizador LR(1). En el siguiente pseudocdigo, para representar la transicin desde el estado sp al estado sd mediante el smbolo a, se utilizar la siguiente notacin: (so,a) ::= sd Mientras haya cambios en el diagrama de estados: Para cada pareja de estados si y sj que cumplan que sus configuraciones slo difieren en los smbolos de adelanto se realizar, si se puede, la siguiente unificacin: Se crea un nuevo estado si_j cuyo contenido se calcula mediante el siguiente proceso: Para cada pareja de configuraciones ci=x {s1,...,sn}si cj=x {d1,...,dm}sj se aade a si_j la configuracin x {s1,...,sn}{d1,...,dm} cuyas transiciones se calculan de la siguiente manera: Cada transicin (so,a)si [dem. (so,a)sj)] del autmata de anlisis LR(1) origina en el autmata de anlisis LALR(1) una transicin (sp,a)si_j. Cada transicin (si,a)sd [dem. (sj,a)sd)] del autmata de anlisis LR(1) origina en el autmata de anlisis LALR(1) una transicin (si_j,a)sd. Obsrvese que esta operacin es la que podra causar que la unificacin fuera imposible, ya que el autmata tiene que seguir siendo determinista, y esto no sera posible si existiera algn smbolo para el que las transiciones desde si y desde sj no terminaran en el mismo estado. A modo de ejemplo, la parte b) de la Figura 4.62 muestra el diagrama de estados del autmata de anlisis LALR(1) para la gramtica Gaxb.

Captulo 4. Anlisis sintctico

167

Construccin de tablas de anlisis LALR(1) a partir del autmata LALR(1)


El algoritmo de creacin de la tabla de anlisis LALR(1) es el mismo que en LR(1). La Figura 4.63 muestra la tabla de anlisis LALR(1) de la gramtica Gaxb.

T E 0 1 2 3_8 4 5_7 d5_7 6_11 9


10 _12
d10_12

N x d4 acc r1 $ S S 1 A 2 B 3_8

a d5_7

r4 d13 d9

r4 r5 6_11 3_8

r5 r3 r3 r2 Accin Ir_a

13

Figura 4.63. Tabla de anlisis LALR(1) para la gramtica Gaxb.

Otros algoritmos para construir LALR(1) sin pasar por LR(1)


El algoritmo descrito en este captulo para llegar a LALR(1) mediante LR(1) no es el ms eficiente para la generacin automtica de analizadores LALR(1). Existen versiones de este algoritmo que construyen directamente el diagrama de estados del analizador LALR(1) sin necesidad de construir el analizador LR(1).

Evaluacin de la tcnica
Es fcil comprobar que, debido al mecanismo de construccin del analizador LALR(1), no se pueden aadir conflictos reduccin / desplazamiento a los que ya tuviera el analizador LR(1). Por otra parte, aunque no en todos los casos se consigue reducir el tamao de las tablas y de los diagramas, a veces se consigue la potencia de un analizador LR(1) con el tamao de un analizador LR(0). Esto hace que LALR(1) sea la tcnica de anlisis ascendente ms extendida.

168

Compiladores e intrpretes: teora y prctica

De hecho, existen herramientas informticas de libre distribucin (como yacc o bison) que construyen automticamente analizadores de este tipo. Usualmente estas herramientas no slo generan el analizador sintctico, sino que aaden ms componentes, proporcionando esqueletos de compiladores e intrpretes. En captulos sucesivos, tras describir otras componentes de los compiladores necesarias para entender estas herramientas, se proporcionar una breve descripcin de las mismas. Tambin se puede consultar en http://www.librosite.net/pulido enlaces de inters, documentacin detallada y ejemplos de uso.

4.4 Gramticas de precedencia simple


El mtodo del anlisis sintctico mediante gramticas de precedencia simple tiene utilidad, y puede ser ms eficiente que otros, en los lenguajes de expresiones, que desempean un papel importante en el acceso a bases de datos, en las frmulas de las hojas de clculo, y en otras aplicaciones. En esta seccin se utilizarn dos conjuntos especiales, llamados first(U) y last(U), que se definen de la siguiente manera: Sea una gramtica G = (T, N, S, P). Sea = T N. Sea U un smbolo de esta gramtica. Se definen los siguientes conjuntos asociados a estas gramticas y a este smbolo: first(U) = {V | U + Vx, V, x*} last(U) = {V | U + xV, V, x*} Obsrvese que, en estas funciones, UN, es decir, U tiene que ser no terminal. Estos conjuntos se calculan con facilidad aplicando conceptos de la teora algebraica de relaciones, aunque esto nos fuerza a establecer las siguientes restricciones en la gramtica: Los smbolos no terminales distintos del axioma no contienen reglas no generativas (reglas de la forma U::=). Si existieran reglas as, se puede obtener una gramtica equivalente que no las contenga, aplicando el procedimiento explicado en la Seccin 1.12.5. Si el axioma genera la palabra vaca, las reglas que definen sus derivaciones directas no deben ser recursivas. Si lo fuesen, se puede construir una gramtica equivalente que cumpla esta condicin, introduciendo un nuevo axioma que genere directamente el axioma antiguo, y aplicando el procedimiento de la Seccin 1.12.5 para trasladar la cadena vaca al nuevo axioma. Por ejemplo, sea la gramtica ({a,b,c,d}, {S,B}, S, P), donde P contiene las siguientes reglas de produccin: S ::= a S b | B B ::= c B d |

Captulo 4. Anlisis sintctico

169

Esta gramtica tiene una regla no generativa en el smbolo B, que no es el axioma. Para eliminarla, hay que aadir las reglas que se obtienen sustituyendo B por en todas las partes derechas, de donde resulta: S ::= a S b | B | B ::= c B d | cd Esta gramtica cumple la primera condicin, pero no la segunda, pues el axioma es recursivo y genera la palabra vaca. Aplicando el mtodo propuesto, se puede transformar en la siguiente gramtica equivalente: S::= S S ::= a S b | B | B ::= c B d | cd Ahora se elimina la regla no generativa, con lo que se obtiene la siguiente gramtica: S::= S | S ::= a S b | a b | B B ::= c B d | cd Esta gramtica cumple las dos restricciones anteriores y es totalmente equivalente a la gramtica de partida (genera el mismo lenguaje).

4.4.1. Notas sobre la teora de relaciones


Sea un conjunto A. Una relacin sobre los elementos de A se define como RAA, es decir, un conjunto de pares de elementos de A. Sea (a,b)R un par de elementos de A que estn en relacin R (se representa aRb). La relacin R se puede definir tambin por enumeracin de sus elementos: R = {(a,b) | aRb}. Sea una relacin R entre elementos de A. Se llama relacin transpuesta de R a la relacin R definida as: aRb bRa. Una relacin R se llama reflexiva si todos los elementos aA cumplen que aRa. Una relacin R se llama transitiva si todos los elementos a,b,cA cumplen que aRb bRc aRc. Sean dos relaciones R, P entre elementos de A. Se dice que dos elementos a,bA estn en la relacin producto de R y P, y se representa aRPb, si existe un elemento cA tal que aRc cPb. El producto de relaciones cumple la propiedad asociativa. Se llama potencia de una relacin R, y se representa Rn, al producto de R por s misma n veces. Se define R1=R y R0=I, donde I es la relacin identidad, definida as: aIb a=b. Se llama clausura transitiva de una relacin R a la siguiente relacin: R + = Ri
i=1

Obviamente, aRbaR b. Adems, cualquiera que sea R, R+ es transitiva.


+

170

Compiladores e intrpretes: teora y prctica

Se llama clausura reflexiva y transitiva de una relacin R a la siguiente relacin: R* = Ri


i=0

Obviamente, aRbaR b. Adems, cualquiera que sea R, R* es transitiva y reflexiva.


*

Teorema 4.4.1. Si A es un conjunto finito de n elementos y R es una relacin sobre los elementos de A, entonces aR+b aRkb para algn k positivo menor o igual que n. Esto significa que, si A es finito, R+ = Ri
i=1 n

Demostracin: aR+b aRpb para algn p>0. Pero aRpb s1, s2, ..., sp tal que a=s1, s1Rs2, s2Rs3, , sp-1Rsp, spRb (por definicin de Rp). Supongamos que p es mnimo y p>n. Entonces, como A slo contiene n elementos, mientras que la sucesin de si contiene p>n, debe haber elementos repetidos en dicha sucesin. Sea si=sj, j>i. Entonces, a=s1, s1Rs2, s2Rs3, , si-1Rsi, si=sj, sjRsj+1, , sp-1Rsp, spRb, y por tanto existe una sucesin ms corta, con p-(j-i) trminos intermedios, para pasar de a a b. Esto contradice la hiptesis de que p>n sea mnimo. Luego p tiene que ser menor o igual que n.

4.4.2. Relaciones y matrices booleanas


Se llama matriz booleana aquella cuyos elementos son valores lgicos, representados por los nmeros 1 (verdadero) y 0 (falso). Las operaciones lgicas clsicas (, ) pueden aplicarse a los elementos o a las matrices en la forma usual. El producto booleano de matrices se define igual que el producto matricial ordinario, sustituyendo la suma por la operacin y la multiplicacin por la operacin . El producto booleano de dos matrices B y C se representa con el smbolo B.C. Sea A un conjunto finito de n elementos, y sea R una relacin sobre los elementos de A. Se puede representar R mediante una matriz booleana B de n filas y n columnas, donde bij=1 aiRaj, y 0 en caso contrario. La aplicacin M(R)=B, que pasa de las relaciones a las matrices booleanas, es un isomorfismo, pues tiene las siguientes propiedades: M(R) = (M(R)) La matriz booleana de la relacin transpuesta de R es la matriz transpuesta de la matriz correspondiente a R. M(R P) = M(R) M(P) La matriz booleana de la unin de dos relaciones es la unin lgica de las dos matrices booleanas correspondientes.

Captulo 4. Anlisis sintctico

171

M(RP) = M(R) . M(P) La matriz booleana de la relacin producto de otras dos es el producto booleano de las dos matrices correspondientes. M(Rn) = (M(R))n La matriz booleana de la potencia ensima de una relacin es la potencia ensima de la matriz booleana de la relacin. Se llama potencia ensima de B el producto booleano de B por s misma n veces. Adems, B0 es la matriz unidad. M(R+) = (M(R))+ La matriz booleana de la clausura transitiva de una relacin es la clausura transitiva de la matriz de la relacin, definida esta ltima operacin como: B + = Bi
i=1

Como consecuencia del Teorema 4.4.1, se cumple que B + = Bi


i=1
n

4.4.3. Relaciones y conjuntos importantes de la gramtica


Recurdese la definicin del conjunto first(U) al principio de la Seccin 4.4: first(U) = {V | U + Vx, V, x*} Esta expresin define el conjunto first(U) en funcin de la relacin +, que a su vez acta sobre elementos de *, que es un conjunto infinito, por lo que no se puede aplicar el Teorema 4.4.1. Para obtener un algoritmo que permita calcular fcilmente first(U), se puede definir la siguiente relacin, que acta sobre conjuntos finitos: U F V U::=Vx P, V, x* Se calcula el cierre transitivo de F: U F + V U::=V1x1P, V1::=V2x2P, , Vn::=Vxn+1P V1,V2,...,VnN, V, x1,x2,...,xn+1* De la expresin anterior se deduce que U F + V u + Vx

172

Compiladores e intrpretes: teora y prctica

por tanto, first(U) = {V | U F + V, V} Pero la relacin F est definida sobre un conjunto finito , y se puede aplicar el Teorema 4.4.1. De la misma forma en que se ha definido la relacin F y el conjunto first(U), se puede definir la siguiente relacin y el siguiente conjunto: U L V U::= xV P, V, x* last(U) = {V | U L + V} Ejemplo Sea la gramtica ({0,1,2,3,4,5,6,7,8,9}, {N,C}, N, {N::=NC|C, 4.12 C::=0|1|2|3|4|5|6|7|8|9}). Para calcular first(C), se empieza definiendo la relacin F :

Regla N::=NC N::=C C::=0 C::=1 C::=2 C::=3 C::=4 C::=5 C::=6 C::=7 C::=8 C::=9

Relacin NFN NFC CF0 CF1 CF2 CF3 CF4 CF5 CF6 CF7 CF8 CF9

Por tanto, F = {(N,N), (N,C), (C,0), (C,1), (C,2), ..., (C,9)}

Captulo 4. Anlisis sintctico

173

La matriz booleana equivalente a F es la siguiente matriz B: N C0123456789 N: 1 1 0 0 0 0 0 0 0 0 0 0 C: 0 0 1 1 1 1 1 1 1 1 1 1 0: 0 0 0 0 0 0 0 0 0 0 0 0 1: 0 0 0 0 0 0 0 0 0 0 0 0 2: 0 0 0 0 0 0 0 0 0 0 0 0 3: 0 0 0 0 0 0 0 0 0 0 0 0 4: 0 0 0 0 0 0 0 0 0 0 0 0 5: 0 0 0 0 0 0 0 0 0 0 0 0 6: 0 0 0 0 0 0 0 0 0 0 0 0 7: 0 0 0 0 0 0 0 0 0 0 0 0 8: 0 0 0 0 0 0 0 0 0 0 0 0 9: 0 0 0 0 0 0 0 0 0 0 0 0 donde las filas y las columnas corresponden a los smbolos {N,C,0,1,2,3,4,5, 6,7,8,9}, en ese orden. De aqu se puede calcular que B2 = B3 = ... = la siguiente matriz: NC0123456789 N: 1 1 1 1 1 1 1 1 1 1 1 1 C: 0 0 0 0 0 0 0 0 0 0 0 0 0: 0 0 0 0 0 0 0 0 0 0 0 0 1: 0 0 0 0 0 0 0 0 0 0 0 0 2: 0 0 0 0 0 0 0 0 0 0 0 0 3: 0 0 0 0 0 0 0 0 0 0 0 0 4: 0 0 0 0 0 0 0 0 0 0 0 0 5: 0 0 0 0 0 0 0 0 0 0 0 0 6: 0 0 0 0 0 0 0 0 0 0 0 0 7: 0 0 0 0 0 0 0 0 0 0 0 0 8: 0 0 0 0 0 0 0 0 0 0 0 0 9: 0 0 0 0 0 0 0 0 0 0 0 0 (En cuanto dos potencias de la matriz coinciden, las restantes son todas iguales). B+ es la unin booleana de todas las matrices (las dos) anteriores: NC0123456789 N: 1 1 1 1 1 1 1 1 1 1 1 1 C: 0 0 1 1 1 1 1 1 1 1 1 1 0: 0 0 0 0 0 0 0 0 0 0 0 0 1: 0 0 0 0 0 0 0 0 0 0 0 0 2: 0 0 0 0 0 0 0 0 0 0 0 0 3: 0 0 0 0 0 0 0 0 0 0 0 0 4: 0 0 0 0 0 0 0 0 0 0 0 0 5: 0 0 0 0 0 0 0 0 0 0 0 0 6: 0 0 0 0 0 0 0 0 0 0 0 0 7: 0 0 0 0 0 0 0 0 0 0 0 0 8: 0 0 0 0 0 0 0 0 0 0 0 0 9: 0 0 0 0 0 0 0 0 0 0 0 0

174

Compiladores e intrpretes: teora y prctica

Por tanto, F + = {(N,N), (N,C), (N,0), (N,1), (N,2), ..., (N,9), (C,0), (C,1), (C,2), ..., (C,9)} Y as se tiene que: first(C) = {0, 1, 2, ..., 9} Ahora se calcula el conjunto last(N). Para ello, se define la relacin L:
Regla N::=NC N::=C C::=0 C::=1 C::=2 C::=3 C::=4 C::=5 C::=6 C::=7 C::=8 C::=9 Relacin L NLC NLC CL0 CL1 CL2 CL3 CL4 CL5 CL6 CL7 CL8 CL9

La matriz de la relacin L + se calcula igual que la de F + y sale: NC0123456789 N: 0 1 1 1 1 1 1 1 1 1 1 1 C: 0 0 1 1 1 1 1 1 1 1 1 1 0: 0 0 0 0 0 0 0 0 0 0 0 0 1: 0 0 0 0 0 0 0 0 0 0 0 0 2: 0 0 0 0 0 0 0 0 0 0 0 0 3: 0 0 0 0 0 0 0 0 0 0 0 0 4: 0 0 0 0 0 0 0 0 0 0 0 0 5: 0 0 0 0 0 0 0 0 0 0 0 0 6: 0 0 0 0 0 0 0 0 0 0 0 0 7: 0 0 0 0 0 0 0 0 0 0 0 0 8: 0 0 0 0 0 0 0 0 0 0 0 0 9: 0 0 0 0 0 0 0 0 0 0 0 0 Luego last(N) es igual a {C,0,1,2,3,4,5,6,7,8,9}.

Captulo 4. Anlisis sintctico

175

4.4.4. Relaciones de precedencia


Dada una gramtica limpia G de axioma S, se definen las siguientes relaciones de precedencia entre los smbolos del vocabulario = T N. Para todo U,V: U =. V W::=xUVy P U <. V W::=xUTy P, T F + V en P. U >. V W::=xTRy P, T L + U, R F * V en P. U <.= V R <. S R =. S U >.= V R >. S R =. S Teorema 4.4.2. U =. V UV aparece en el asidero de alguna forma sentencial. Prueba: U =. V W::=xUVy P G es limpia S * uWv existe un rbol que genera uWv. Se poda ese rbol hasta que U est en un asidero (todo smbolo ha de ser alguna vez parte de un asidero). Se aplica la regla W::=xUVy. Como U era parte del asidero, el asidero de este rbol nuevo debe ser xUVy, q.e.d. UV es parte de un asidero. Por definicin de asidero existe una regla W::=xUVy U =. V, q.e.d. Teorema 4.4.3. U >. V existe una forma sentencial xUVy donde U es el smbolo final de un asidero. Prueba: U >. V W::= xTRy P, T L + U, R F * V en P G es limpia S * uWv + uxTRyv T L + U T + tU S + uxtURyv Se dibuja este rbol y se reduce hasta que U sea parte del asidero. Por construccin, tendr que ser su ltimo smbolo. R F * V R * Vw S + uxtUVwyv Se aade al rbol anterior esta ltima derivacin. El asidero no ha cambiado, U sigue siendo el ltimo smbolo y va seguido por V, q.e.d. U es la cola de un asidero y va seguido por V. Se reduce hasta que V est en el asidero. Esto da un rbol para la forma sentencial xTVy, donde T * tU.

176

Compiladores e intrpretes: teora y prctica

Si T y V estn los dos en el asidero, existe W ::= uTVv U >. V, q.e.d. Si V es cabeza del asidero, reducimos hasta llegar a xTWw donde T est en el asidero. Ahora, W + V y W tiene que estar en el asidero, pues, si no, T sera la cola y habra sido asidero antes que V. Luego U >. V, q.e.d. Teorema 4.4.4. U <. V existe una forma sentencial xUVy donde V es el smbolo inicial de un asidero. Se demuestra de forma anloga.

4.4.5. Gramtica de precedencia simple


Definicin: G es una Gramtica de Precedencia Simple o Gramtica de Precedencia (1,1) si: 1. G cumple las condiciones especificadas al principio de la Seccin 4.4. 2. Slo existe, como mucho, una relacin de precedencia entre dos smbolos cualesquiera del vocabulario. 3. No existen en G dos producciones con la misma parte derecha. Se llama tambin Gramtica de Precedencia (1,1) porque slo se usa un smbolo a la izquierda y la derecha de un posible asidero para decidir si lo es. Si no se cumplen las condiciones anteriores, a veces se puede manipular la gramtica para intentar que se cumplan. La recursividad a izquierdas o a derechas suele dar problemas (salen dos relaciones de precedencia con los smbolos que van antes o despus del recursivo). Para evitarlo, se puede estratificar la gramtica, aadiendo smbolos nuevos, pero, si hay muchas reglas, esto es muy pesado. De todos modos, no siempre es posible obtener una gramtica de precedencia simple equivalente a una dada, pues no todos los lenguajes independientes del contexto pueden representarse mediante gramticas de precedencia simple. Supondremos que toda forma sentencial queda encuadrada entre dos smbolos especiales de principio y fin de cadena, y , que no estn en y tales que, para todo U, + <. U y U >. . Teorema 4.4.5. Una gramtica de precedencia simple no es ambigua. Adems, el asidero de una forma sentencial U1 ... Un es la subcadena Ui ... Uj, situada ms a la izquierda tal que: Ui-1 <. Ui =. Ui+1 =. Ui+2 =. ... =. Uj-1 =. Uj >. Uj+1 Prueba: Si Ui ... Uj es asidero, se cumple la relacin por los teoremas anteriores. Reduccin al absurdo. Se cumple la relacin y no es asidero. Entonces ninguna poda la har asidero, pues si en algn momento posterior resultara ser la rama completa ms a la izquierda, ya lo es ahora. Si no es asidero, cada uno de los smbolos de Ui-1 Ui ... Uj Uj+1 debe aparecer ms pronto o ms tarde como parte de un asidero. Sea Uk el primero que aparece.

Captulo 4. Anlisis sintctico

177

1. Si Uk = Ui-1, de los teoremas se sigue que Ui-1 =. Ui o Ui-1 >. Ui, lo que contradice que Ui-1 <. Ui (no puede haber dos relaciones entre dos smbolos). 2. Si Uk = Uj+1, de los teoremas se sigue que Uj =. Uj+1 o Uj <. Uj+1, lo que contradice que Uj >. Uj+1. 3. Si i-1<k<j+1, ocurre lo siguiente: a. Que Ui-1 no puede estar en el asidero (pues entonces Ui-1 =. Ui, contradiccin). b. Que Uj+1 no puede estar en el asidero (pues entonces Uj =. Uj+1, contradiccin). c. Que ningn smbolo Up, i<p<=k puede ser cabeza del asidero (pues entonces Up-1 <. Up, contradiccin). d. Que ningn smbolo Up, k<=p<j puede ser cola del asidero (pues entonces Up <. Up+1, contradiccin). e. Luego la cabeza del asidero debe ser Ui y la cola Uj, lo que contradice que Ui ... Uj no era el asidero, q.e.d. Como el asidero es nico (por construccin) y slo puede ser parte derecha de una regla (por ser la Gramtica de Precedencia Simple), slo se puede aplicar una regla para reducir. Luego la gramtica no es ambigua.

4.4.6. Construccin de las relaciones


La relacin =. se construye por simple observacin de las partes derechas de las reglas (vase la definicin de =.). La relacin <. se construye fcilmente utilizando la representacin matricial de las relaciones y teniendo en cuenta que: <. =. . F + donde . es el producto booleano de matrices. (Por definicin del producto de relaciones y la definicin de <. y =.). La relacin >. se construye de manera parecida: >. (L + ) . =. . F * donde es la transposicin de matrices. La demostracin queda como ejercicio.

4.4.7. Algoritmo de anlisis


1. Se almacenan las producciones de la gramtica en una tabla. 2. Se construye la matriz de precedencia MP de dimensiones NN (N = cardinal o nmero de elementos de ), tal que

178

Compiladores e intrpretes: teora y prctica

MP(i,j) = 0 si no existe relacin entre Ui y Uj = 1 si Ui <. Uj = 2 si Ui =. Uj = 3 si Ui >. Uj 3. Se inicializa una pila con el smbolo y se aade el smbolo al final de la cadena de entrada. 4. Se compara el smbolo situado en la cima de la pila con el siguiente smbolo de entrada. 5. Si no existe ninguna relacin, error sintctico: cadena rechazada (fin del algoritmo). 6. Si existe la relacin <. o la relacin =., se introduce el smbolo de entrada en la pila y se elimina de la entrada. Volver al paso 4. 7. Si la relacin es >., el asidero termina en la cima de la pila. 8. Se recupera el asidero de la pila, sacando smbolos hasta que el smbolo en la cima de la pila est en relacin <. con el ltimo sacado. 9. Se compara el asidero con las partes derechas de las reglas. 10. Si no coincide con ninguna, error sintctico: cadena rechazada (fin del algoritmo). 11. Si coincide con una, se coloca la parte izquierda de la regla en el extremo izquierdo de la cadena que queda por analizar. 12. Si en la pila slo queda y la cadena de entrada ha quedado reducida al axioma seguido del smbolo , la cadena ha sido reconocida (fin del algoritmo). En caso contrario, volver al paso 4. Ejemplo Sea la gramtica G = ({a,b,c}, {S}, S, {S ::= aSb | c}). Las matrices de las relacio4.13 nes son: =. : 0 0 1 0 1000 0000 0000 F+ = F L+ = L F* : 1 1 0 1 0100 0010 0001 F: 0101 0000 0000 0000 L: 0011 0000 0000 0000

Aplicando las expresiones de la Seccin 4.4.3, se obtiene: <. : 0 0 0 0 0101 0000 0000 >. : 0 0 0 0 0000 0010 0010

Captulo 4. Anlisis sintctico

179

Con lo que la matriz de precedencia queda: a b c S =. >. a =. <. <. >. b >. >. c >. >. <. <. <. <. S Se analizar ahora la cadena aacbb:

Pila

Relacin <. <. <. >. =. =. >. =. =. >. <.

Entrada acbb cbb bb bb b b S Sb Sbb aacbb

Asidero

Regla a aplicar

<.a <.a<.a <.a<.a.<.c <.a<.a <.a<.a=.S <.a<.a=.S=.b <.a <.a=.S <.a=.S=.b

S::=c

aSb

S::=aSb

aSb

S::=aSb

Luego la cadena es aceptada. Se analizar ahora la cadena aabb:

Pila

Relacin <. <. No hay

Entrada abb bb aabb

Asidero

Regla a aplicar

<.a <.a<.a

Luego la cadena es rechazada.

180

Compiladores e intrpretes: teora y prctica

Se analizar ahora la cadena acbb:

Pila

Relacin <. <. >. =. =. bb >. <. =. >.

Entrada cbb bb Sbb b b acbb

Asidero

Regla a aplicar

<.a <.a<.c <.a <.a=.S <.a=.S=.b <.S <.S=.b

S::=c

aSb

S::=aSb

Sb Sb No hay

Luego la cadena es rechazada.

4.4.8. Funciones de precedencia


La matriz de precedencias ocupa NN posiciones de memoria. A veces es posible construir dos funciones de precedencia que ocupen slo 2N. Esta operacin se llama linealizacin de la matriz. Dichas funciones, de existir, no son nicas (hay infinitas). Para el ejemplo anterior valen las dos funciones siguientes: S a b c f 0 2 2 3 3 g 2 3 2 3 0 Si es posible construir f y g, se verifica que f(U)=g(V) U =. V f(U)<g(V) U <. V f(U)>g(V) U >. V Existen matrices que no se pueden linealizar. Ejemplo: A B A = > B = =

Captulo 4. Anlisis sintctico

181

En este caso, debera cumplirse que: f(A) = g(A) f(A) > g(B) f(B) = g(A) f(B) = g(B) Es decir: f(A) > g(B) = f(B) = g(A) = f(A) f(A) > f(A) Con lo que se llega a una contradiccin. Cuando no hay inconsistencias, el siguiente algoritmo construye las funciones de precedencia: Se dibuja un grafo dirigido con 2N nodos, llamados f1, f2, , fN, g1, g2, , gN, con un arco de fi a gj si Ui >.= Uj y un arco de gj a fi si Ui <.= Uj. A cada nodo se le asigna un nmero igual al nmero total de nodos accesibles desde l (incluido l mismo). El nmero asignado a fi se toma como valor de f(Ui) y el asignado a gi es g(Ui). Demostracin: Si Ui =. Uj, hay una rama de fi a gj y viceversa, luego cualquier nodo accesible desde fi es accesible desde gj y viceversa. Luego f(Ui)=g(Uj). Si Ui >. Uj, hay una rama de fi a gj. Luego cualquier nodo accesible desde gj es accesible desde fi. Luego f(Ui)>=g(Uj). Si f(Ui)=g(Uj), existe un camino cerrado figjfkglgmfi, lo que implica que Ui >. Uj, Uk <.= Uj, Uk >.= Ul, Ui <.= Um y reordenando: Ui>.Uj>.=Uk>.=Ul>.=>.=Um>.=Ui, es decir: f(Ui)>f(Ui), con lo que se llega a una contradiccin. Luego el caso de igualdad de valores de las funciones queda excluido y se deduce que f(Ui)>g(Uj). Si Ui <. Uj, la demostracin es equivalente. La Figura 4.64 describe la aplicacin de este algoritmo al ejemplo anterior, y explica cmo se obtuvieron las funciones mencionadas al principio de esta seccin.

f(S)

f(a)

f(b)

f(c)

g(S)

g(a)

g(b)

g(c)

Figura 4.64. Grafo utilizado para la construccin de las funciones de precedencia.

182

Compiladores e intrpretes: teora y prctica

El algoritmo descrito puede mecanizarse. El grafo descrito equivale a la relacin existe un arco del nodo x al nodo y. Dicha relacin posee su matriz Booleana correspondiente, de dimensiones 2N2N, que llamaremos B, y que puede representarse as: (0) (<.=) (>.=) (0)

A partir de B, construimos B*. Entonces se verifica que f(Ui)es igual al nmero de unos en la fila i, mientras g(Ui) es igual al nmero de unos en la fila N+i. En el ejemplo anterior, B resulta ser la matriz: 00000010 00001000 00000010 00000010 01000000 01000000 10000000 01000000 A partir de B, obtenemos B*, que resulta ser la matriz: 10000010 01001000 10100010 10010010 01001000 01001100 10000010 01001001 Con lo que las dos funciones f y g resultan ser las indicadas al principio de esta seccin. Para completarlas, basta aadir los smbolos de principio y fin de cadena, a los que se asigna el valor 0. A continuacin se muestran de nuevo los resultados obtenidos. Obsrvese que, si se suma cualquier nmero entero a todos los valores de f y g, se obtienen dos nuevas funciones que tambin cumplen todas las condiciones. Por eso, si existe un par de funciones f y g, tendremos infinitas, todas equivalentes.

S a b c f 0 2 2 3 3 g 2 3 2 3 0
El uso de las funciones supone cierta prdida de informacin respecto al uso de la matriz, pues desaparecen los lugares vacos en la tabla de precedencias, que conducan directamente a la deteccin de algunos casos de error. Sin embargo, estos casos sern detectados ms tarde, de otra manera. Para comprobarlo, analicemos por medio de las funciones la cadena aabb y veremos que en este caso basta con un paso ms para detectar la condicin de error.

Captulo 4. Anlisis sintctico

183

Pila

f(x) 0 2 2 3

g(x) 3 3 2 2

Relacin <. <. =. >.

Entrada abb bb b aabb

Asidero

Regla a aplicar

<.a <.a<.a <.a<.a=.b

ab

No hay

4.5 Resumen
En este captulo se describen algunos de los mtodos que suelen utilizarse para construir los analizadores sintcticos de los lenguajes independientes del contexto. En una primera seccin del captulo se describen los conjuntos primero y siguiente asociados a una gramtica, pues son necesarios para describir los mtodos que aparecen en el resto del captulo. Estos mtodos de anlisis pueden clasificarse en dos categoras: anlisis descendente y anlisis ascendente. Dentro del anlisis descendente se describe, en primer lugar, el anlisis descendente con vuelta atrs cuya ineficiencia se soluciona con las gramticas LL(1) y el mtodo de anlisis descendente selectivo. Las operaciones fundamentales de los algoritmos de anlisis ascendente son el desplazamiento de los smbolos de entrada necesarios para reconocer los asideros y la reduccin de los mismos. Estas tcnicas se basan en la implementacin del autmata a pila para la gramtica independiente del contexto del lenguaje considerado y ste, a su vez, en el autmata finito que reconoce los asideros del anlisis. Hay diferentes maneras de construir este autmata finito. Los analizadores estudiados en orden creciente de potencia, son LR(0), SLR(1), LR(1) y LALR(1). Su principal diferencia consiste en que, para reducir una regla, comprueban condiciones ms estrictas respecto a los prximos smbolos que el anlisis encontrar en la entrada: SLR(1) tiene en cuenta el smbolo siguiente y LR(1) y LALR(1) consideran de forma explcita un smbolo de adelanto. Los algoritmos LR y LALR se podran generalizar para valores de k>1 pero la complejidad que implica gestionar ms de un smbolo de adelanto desaconseja su uso. Otro mtodo de anlisis ascendente, que no se utiliza mucho en compiladores completos, pero s en analizadores de expresiones, hojas de clculo, bases de datos, etc., utiliza gramticas de precedencia simple. El mtodo se basa en tres relaciones, llamadas de precedencia, que permiten localizar los asideros del anlisis con un algoritmo muy sencillo y eficiente. Estas relaciones pueden construirse fcilmente, por simple observacin de las reglas de produccin, o de forma automtica, mediante operaciones realizadas sobre matrices booleanas. Por ltimo, es posible mejorar el algoritmo sustituyendo las matrices por dos funciones de precedencia.

4.6 Ejercicios
1. Considrese el lenguaje de los tomos en lenguaje Prolog. Todos ellos tienen un identificador, y pueden tener cero, uno o varios argumentos. Cuando el nmero de argumentos es ma-

184

Compiladores e intrpretes: teora y prctica

yor o igual que 1, stos aparecen entre parntesis, y separados por comas; en caso contrario, no se escriben los parntesis. Finalmente, cada uno de los argumentos de un tomo puede ser otro tomo, un nmero o una variable (que comienza con letra mayscula). Supondremos, adems, que al final de cada palabra del lenguaje hay un punto. Por ejemplo, las siguientes expresiones seran vlidas: atomo. pepe(a(b,c)). paco(0,1,2,X). Supondremos que el analizador morfolgico ya ha identificado los identificadores, los nmeros y las variables, por lo que no es necesario incluir las reglas para reconocer stos en la gramtica del lenguaje. Proporcionar una gramtica LL(1) para este lenguaje. Construir la matriz de anlisis LL(1) para la gramtica proporcionada. 2. Con la matriz de anlisis LL(1) del ejercicio anterior, analizar las siguientes cadenas: atomo. pepe(a(b,c)). paco(variable(0)). 3. Sea el lenguaje {anbm+nam | m,n>=0}. Construir una gramtica LL(1) que lo reconozca. Construir el analizador sintctico correspondiente. Analizar las siguientes palabras: aabbba, aaabb. 4. Dado el lenguaje L = {w | w tiene un nmero par de ceros y unos}, construir una gramtica LL(1) que lo describa. Construir el analizador sintctico correspondiente. 5. Construir una gramtica independiente del contexto que describa el lenguaje {anbpcm+pdn+m | m,n,p>0}. Construir una gramtica LL(1) que describa el mismo lenguaje. 6. Sea el lenguaje formado por todas las palabras con las letras a y b, con doble nmero de a que de b, pero con todas las b seguidas, situadas en un extremo de la cadena. Construir una gramtica LL(1) que lo reconozca. 7. Sea el lenguaje {ab(ab)nac(ac)n | n>=0}. Construir una gramtica LL(1) que lo reconozca y el analizador sintctico correspondiente. Analizar las siguientes palabras: ababacac, abacac. 8. Dada la gramtica A ::= B a | a B ::= A b | b C ::= A c | c Construir una gramtica LL(1) equivalente y un analizador sintctico descendente que analice el lenguaje correspondiente. Analizar las siguientes palabras: abab, baba, ababa, babab.

Captulo 4. Anlisis sintctico

185

9. Sea el lenguaje formado por todas las palabras no vacas con las letras a y b, con el mismo nmero de a que de b, pero con todas las b seguidas, sin ninguna restriccin para las a. Construir una gramtica LL(1) que lo reconozca. Construir un analizador sintctico descendente que la analice. Analizar las siguientes palabras: bbaa, baba. 10. Sea el siguiente lenguaje sobre el alfabeto {a,b}: (aa*+)b+bb*. Construir una gramtica LL(1) que reconozca el mismo lenguaje. Escribir el analizador sintctico descendente correspondiente. Utilizando el analizador anterior, analizar las siguientes cadenas: aaab, aabb. 11. Sea el siguiente lenguaje sobre el alfabeto {a,b}: {am b cm an b cn | m,n>0} U {bp | p>0}. Construir una gramtica LL(1) que reconozca el mismo lenguaje. Escribir el analizador sintctico top-down correspondiente. Utilizando el analizador anterior, analizar las siguientes cadenas: abbc, bbb. 12. Construir una gramtica que reconozca el lenguaje {an b m | 0<=n<m}. Convertir la gramtica anterior a la forma normal de Greibach. Convertir la gramtica anterior a la forma LL(1). Escribir un analizador sintctico descendente que analice el lenguaje anterior. 13. Sea el siguiente lenguaje sobre el alfabeto {a,b}: { an bn | n0 } U { a }. Construir una gramtica LL(1) que reconozca el mismo lenguaje. Escribir el analizador sintctico topdown correspondiente. Utilizando el analizador anterior, analizar las siguientes cadenas: a, b, ab, aab, aabb. 14. Construir una gramtica LL(1) que reconozca el lenguaje {an bm cn+m | n,m>=0}. Escribir un analizador sintctico descendente que analice el lenguaje anterior. 15. Construir una gramtica LL(1) para el lenguaje {an bm cn | n,m>=0} y programar el analizador sintctico correspondiente. 16. Utilizar la tabla de anlisis de la Figura 4.30 para realizar el anlisis sintctico de la cadena id*id+id. Es sintcticamente correcta la cadena? 17. Utilizar la tabla de anlisis de la Figura 4.37 para realizar el anlisis sintctico de la cadena i+(i+i). Es sintcticamente correcta la cadena? 18. Utilizar la tabla de anlisis de la Figura 4.37 para realizar el anlisis sintctico de la cadena (i+i. Es sintcticamente correcta la cadena? 19. Utilizar la tabla de anlisis SLR(1) de la Figura 4.47 para realizar el anlisis sintctico de la siguiente cadena y determinar si es sintcticamente correcta o no. begin dec; ejec; ejec end

186

Compiladores e intrpretes: teora y prctica

20. Utilizar la tabla de anlisis SLR(1) de la Figura 4.47 para realizar el anlisis sintctico de la siguiente cadena y determinar si es sintcticamente correcta o no. begin dec; ejec; end 21. Dada la gramtica independiente del contexto que se puede deducir de las siguientes reglas de produccin en las que el axioma es el smbolo E: (1)E::=E+E (2)E::=E*E (3)E::=i Construir el diagrama de estados del analizador SLR(1) y la tabla de anlisis para determinar si la gramtica es o no SLR(1). Obsrvese que esta gramtica es ambigua ya que no establece prioridad entre las operaciones aritmticas. Analizar el efecto de la ambigedad en los posibles conflictos de la gramtica y el significado que aporta a la ambigedad solucionarlos mediante la seleccin de una de las operaciones en conflicto. Comprobar los resultados en el anlisis de la cadena i*i+i*i+i. 22. Utilizar la tabla de anlisis LR(1) de la Figura 4.56 para realizar el anlisis sintctico de la cadena aaxbb y determinar si es sintcticamente correcta o no. 23. Utilizar la tabla de anlisis LR(1) de la Figura 4.56 para realizar el anlisis sintctico de la cadena ax y determinar si es sintcticamente correcta o no. 24. Repetir el ejercicio anterior con la tabla LALR(1) de la Figura 4.62. 25. Construir la tabla de anlisis sintctico ascendente para la siguiente gramtica que corresponde a al subconjunto (dedicado a la declaracin de variables) de la gramtica de un lenguaje de programacin imaginario. <declaration> ::= <mode> <idlist> <mode> ::= bool <mode> ::= int <idlist> ::= <id> <idlist> ::= <id> , <idlist> Obsrvese que en la regla 1 aparece un espacio en blanco entre los smbolos no terminales <mode> e <idlist>. Considerar el smbolo <declaration> como el axioma. Contestar razonadamente a las siguientes preguntas La gramtica del apartado anterior es una gramtica LR(0)? Por qu? La gramtica del apartado anterior es una gramtica SLR(1)? Por qu? Utilizando la tabla de anlisis desarrollada en el apartado (1), analizar la siguiente declaracin: int x,y

Captulo 4. Anlisis sintctico

187

26. La tabla de anlisis sintctico SLR(1) para la siguiente gramtica est incompleta. Considerar que S es el axioma. S ::= id (L) S ::= id L ::= L ::= S Q Q ::= Q ::=, S Q

Accin id 0 1 2 3 4 5 6 7 8 9 10 d2 d2 d6 d8 d2 acc ( ) , $

Ir a S 1 L Q

4 7

9 10

26.1. Completar las casillas sombreadas detallando los clculos realizados al efecto. 26.2. Rellenar las casillas que correspondan a operaciones de reduccin detallando los clculos realizados al efecto. 27. Realizar el anlisis de la cadena var int inst utilizando la tabla de anlisis SLR(1) que se proporciona y que corresponde a la gramtica S ::= S inst S ::= S var D S ::= D ::= D ident E D ::= D ident sep D ::= int D ::= float E ::= S fproc

188

Compiladores e intrpretes: teora y prctica

Accin inst 0 1 2 3 4 5 6 7 8 9 10 11 r2 r6 r7 r3 r4 d2 r5 r8 r2 r6 r7 r3 r4 d2 r5 r8 r5 r8 r4 d7 r6 r7 d10 d5 d6 r2 r6 r7 r3 r4 d11 r5 r8 r3 d2 var r3 d3 r1 ident sep int

Ir a float fproc $ r3 r3 acc r1 r3 r6 r7 r3 r4 r5 r8 9 8 S 1 D E

28. Dada la siguiente gramtica (considerar que E es el axioma) E ::= (L) E ::= a L ::= L,E L ::= E Cul sera el resultado de aplicar la operacin de clausura o cierre al estado formado por el elemento LR(1) E::=(L) {$} (o en una notacin equivalente, el elemento LR(1) (1,1,$))? Contestar razonadamente. 29. Sea el lenguaje sobre el alfabeto {a,b} formado por todas las palabras que empiezan por a y acaban por b. Construir una gramtica SLR(1) que reconozca el mismo lenguaje y su tabla de anlisis. 30. Construir una gramtica SLR(1), con la cadena vaca en alguna parte derecha, que reconozca el lenguaje {anbm| 0n<m}. Construir tambin la tabla de anlisis y utilizarla en el anlisis de las cadenas abb, aab. La gramtica anterior es LR(0)? Por qu? 31. Construir una gramtica SLR(1) que describa el lenguaje {anbn+p+qapcq|n,p,q>=0}. Construir la tabla del anlisis correspondiente. Analizar las cadenas abbac, abbbac y abbbbac. 32. Dado el lenguaje L = { w | w tiene un nmero par de ceros y unos }, construir una gramtica SLR(1) que lo describa. Construir la tabla del anlisis correspondiente.

Captulo 4. Anlisis sintctico

189

33. Construir una gramtica SLR(1) y la tabla de anlisis para el lenguaje {ancmbn | m>0,n>=0}. 34. Sea el siguiente lenguaje sobre el alfabeto {a,b}: {anbn | n>=0} {a}. 34.1. Construir una gramtica SLR(1) que reconozca el mismo lenguaje. 34.2. Construir la tabla de anlisis. 34.3. Utilizando el analizador anterior, analizar las siguientes cadenas: a, b, ab, aab, aabb. 35. Sea el siguiente lenguaje sobre el alfabeto {a,b} : {ambcmanbcn| m,n>0 } {bp| p>0 }. 35.1. Construir una gramtica SLR(1), con la cadena vaca en alguna parte derecha, que reconozca el mismo lenguaje. 35.2. Construir la tabla de anlisis. 35.3. Utilizando la tabla anterior, analizar las siguientes cadenas: abcabc, abbc. 36. Sea el siguiente lenguaje sobre el alfabeto {a,b} representado mediante su expresin regular(aa*+)b+bb*, donde es la palabra vaca. 36.1. Construir una gramtica SLR(1), con la palabra vaca en alguna parte derecha, que reconozca el mismo lenguaje. 36.2. Construir la tabla de anlisis. 36.3. Utilizando la tabla anterior, analizar las siguientes cadenas: aaab, aabb. 37. Sea el lenguaje del Ejercicio 4.29. 37.1. Construir una gramtica de precedencia simple que lo reconozca. 37.2. Construir la matriz de relaciones de precedencia. 38. Para el lenguaje del Ejercicio 4.30. 38.1. Construir una gramtica sin la cadena vaca que lo reconozca. 38.2. Construir la matriz de relaciones de precedencia. Explicar por qu no es de precedencia simple. 39. En el lenguaje Smalltalk, los operadores binarios (+,-,*,/) no tienen precedencia intrnseca, sino posicional: el operador situado ms a la izquierda se ejecuta primero, salvo por la presencia de parntesis, que modifican la precedencia de la manera habitual. Los operandos bsicos pueden ser identificadores o constantes numricas: 39.1. Construir una gramtica que represente el lenguaje de las expresiones binarias en Smalltalk. 39.2. Es de precedencia simple esta gramtica? 40. Construir una gramtica de precedencia simple y una matriz de precedencia para el lenguaje del Ejercicio 4.33. 41. Sea el lenguaje del Ejercicio 4.34. 41.1. Construir una gramtica de precedencia simple que lo reconozca. 41.2. Construir la matriz de relaciones de precedencia.

190

Compiladores e intrpretes: teora y prctica

41.3. Utilizando la matriz anterior y el algoritmo estndar, analizar las siguientes cadenas: a, b, ab, aab, aabb. 42. Sea el lenguaje del Ejercicio 4.35. 42.1. Construir una gramtica de precedencia simple, sin la cadena vaca en ninguna parte derecha, que lo reconozca. 42.2. Construir la matriz de relaciones de precedencia. 42.3. Utilizando la matriz anterior y el algoritmo estndar, analizar las siguientes cadenas: abcabc, abbcb, abbc. 43. Sea el lenguaje del Ejercicio 4.36. 43.1. Construir una gramtica de precedencia simple, sin la cadena vaca en ninguna parte derecha, que lo reconozca. 43.2. Construir la matriz de relaciones de precedencia. 43.3. Utilizando la matriz anterior y el algoritmo estndar, analizar las siguientes cadenas: aaab, aabb. 44. Encontrar una gramtica cuyo lenguaje sea el conjunto de los nmeros enteros pares. 45. En la gramtica anterior, calcular las relaciones F , L , F +, L +. 46. En la gramtica anterior, construir los conjuntos first(S), last(S), donde S es el axioma. 47. Demostrar que R+ es transitiva, cualquiera que sea R.

Captulo

Anlisis semntico

5.1 Introduccin al anlisis semntico


5.1.1. Introduccin a la semntica de los lenguajes de programacin de alto nivel
El anlisis semntico es la fase del compilador en la que se comprueba la correccin semntica del programa. En la Seccin 1.13.4 se reflexion acerca de la distincin entre la sintaxis y la semntica de los lenguajes de programacin de alto nivel. Tambin se explic que las gramticas del tipo 0 de Chomsky, el nico tipo de gramtica que tiene la expresividad necesaria para representar todos los aspectos de estos lenguajes, presenta demasiadas dificultades para su diseo y gestin. De esta forma se justifica que, en el tratamiento de los lenguajes de programacin, se distingan las construcciones sintcticas (usualmente independientes del contexto) de las semnticas (usualmente dependientes). En el Captulo 4 se han tratado con detalle los algoritmos necesarios para el anlisis sintctico, que normalmente aborda los aspectos independientes del contexto. Se ha podido comprobar que todos ellos son relativamente simples. El objetivo de este captulo es incorporar la semntica al anlisis del programa que se est compilando. Sera deseable disponer de una herramienta parecida a las gramticas independientes del contexto, a la que se pudiera incorporar de forma sencilla la comprobacin de las condiciones semnticas. Si se dispusiera de ella, el analizador semntico se reducira a una extensin de los algoritmos de anlisis sintctico, para incorporar la gestin de los aspectos semnticos.

192

Compiladores e intrpretes: teora y prctica

En este captulo se ver que las gramticas de atributos proporcionan una herramienta muy adecuada para el anlisis semntico, se explicar cmo pueden solucionar los problemas asociados con la semntica de los programas compilados y se describirn algunas aplicaciones existentes, que reciben como entrada gramticas de atributos y generan de forma automtica analizadores semnticos.

5.1.2. Objetivos del analizador semntico


El analizador semntico es la parte del compilador que realiza el anlisis semntico. Suele estar compuesto por un conjunto de subrutinas independientes, que pueden ser invocadas por los analizadores morfolgico y sintctico. Se puede considerar que el analizador semntico recibe, como entrada, el rbol del anlisis del programa, una vez realizado el anlisis morfolgico y sintctico. Esta distincin es ms bien conceptual, ya que, en los compiladores reales, a menudo estas fases se entremezclan. Suele describirse el anlisis semntico como un proceso mediante el cual se aade al rbol de derivacin una serie de anotaciones, que permiten determinar la correccin semntica del programa y preparar la generacin de cdigo. Por lo tanto, la salida que genera el anlisis semntico, en el caso de que no haya detectado errores, es un rbol de derivacin con anotaciones semnticas. Dichas anotaciones se pueden usar para comprobar que el programa es semnticamente correcto, de acuerdo con las especificaciones del lenguaje de programacin. Hay que comprobar, por ejemplo, que: Cuando se utiliza un identificador, ste ha sido declarado previamente. Se ha asignado valor a las variables antes de su uso. Los ndices para acceder a los arrays estn dentro del rango vlido. En las expresiones aritmticas, los operandos respetan las reglas sobre los tipos de datos permitidos por los operadores. Cuando se invoca un procedimiento, ste ha sido declarado adecuadamente. Adems, el nmero, tipo y posicin de cada uno de sus argumentos debe ser compatible con la declaracin. Las funciones contienen al menos una instruccin en la que se devuelve su valor al programa que las invoc. Ejemplo Se est compilando el siguiente programa, escrito en un lenguaje de programacin ficticio, cuya sintaxis resultar fcil de comprender para cualquier programador: 5.1 begin int A; A := 100; A := A + A; output A end Resulta claro que el programa declara una variable de tipo entero y nombre A, le asigna inicialmente el valor 100 y posteriormente el de la suma de A consigo misma; finalmente, se escri-

Captulo 5. Anlisis semntico

193

be en algn medio externo el ltimo valor de A. La Figura 5.1 resume la accin del anlisis semntico en relacin con este programa.
<Programa> begin <declrcns> <declrcn> <tipo> int <ids> <id> A A ; <sntnc> <asignacion> <sntncs> ; <sntncs> <sntnc> ; <sntncs> end

<id> := <expr>

<asignacion>

<sntnc>

<const.int>

<id> := <expr> <salida>

100

<expr> <id>

+ <expr> <id>

output <expr> <id>

a)

<Programa> begin <declrcns> int A ; <sntnc> <asignacion> int int int <id> := <expr> A <sntncs> ; <sntncs> <sntnc> ; int <asignacion> <sntncs> end

int <declrcn> <tipo> int <ids> <id> A

<sntnc>

<const.int>

<id> := <expr> <salida> int int + <expr> int <id> int A output <expr> int <id> int A

100

<expr> int <id> int

b)

Figura 5.1. Ejemplo de un posible resultado del anlisis semntico. a) Entrada del analizador semntico: el rbol de derivacin. b) Salida: el rbol anotado.

194

Compiladores e intrpretes: teora y prctica

La parte a) de la Figura 5.1 muestra el rbol de derivacin del programa, de acuerdo con una gramtica que no hace falta especificar. Este rbol sera la entrada que recibe el analizador semntico. La parte b) muestra el rbol, tras aadirle la siguiente informacin: En el smbolo no terminal <dclrcn>, asociado con la declaracin de la variable A, se ha aadido el tipo de sta: int. Al smbolo no terminal <dclrcns>, se le ha aadido la lista de identificadores declarados con sus tipos: int A. Esta lista podra utilizarse para aadir en la tabla de smbolos la informacin correspondiente. En la primera aparicin del smbolo no terminal <id> como primer hijo del smbolo <asignacion>, se ha anotado que el tipo del identificador A, que se conoce desde su declaracin, es int. En el smbolo no terminal <expr> que aparece como hermano del recin analizado <id>, se ha anotado que el tipo de la expresin es tambin entero, ya que la constante 100, que es el fragmento de la entrada derivado de <expr>, es un nmero entero. Las anotaciones de los dos ltimos puntos pueden servir para comprobar que la asignacin es correcta, ya que los tipos del identificador y de la expresin son compatibles. En el nodo del smbolo <asignacion>, padre del subrbol estudiado, puede anotarse que se ha realizado una asignacin correcta de valores de tipo entero. El subrbol cuya raz es la ltima aparicin de <asignacion> presenta un caso anlogo al anteriormente descrito: las apariciones del identificador A en la parte derecha de la asignacin obligan a consultar el tipo con que fue declarado. Se trata, por lo tanto, de asignar una expresin de tipo entero a un identificador de tipo entero. Las anotaciones de este subrbol permiten realizar todas las comprobaciones necesarias. El subrbol correspondiente a la ltima aparicin de <expr> en la instruccin que imprime el valor de la variable A contiene anotaciones con el tipo de la variable y el de la expresin.

5.1.3. Anlisis semntico y generacin de cdigo


En funcin del procedimiento utilizado para generar el programa objeto, se distinguen los siguientes tipos de compiladores (vase la Figura 5.2): Compiladores de un solo paso: integran la generacin de cdigo con el anlisis semntico. Estos compiladores generan directamente el cdigo a partir del rbol de la derivacin. En este caso, las llamadas a las rutinas que escriben el cdigo ensamblador suelen entremezclarse con el anlisis semntico. Compiladores de dos o ms pasos: en el primer paso de la compilacin, el analizador semntico genera un cdigo abstracto denominado cdigo intermedio. En un segundo paso se realiza la generacin del cdigo definitivo a partir del cdigo intermedio. A veces se separa tambin la optimizacin de cdigo, la cual se realiza en un tercer paso independiente.

Captulo 5. Anlisis semntico

195

COMPILADOR DE UN PASO Anlisis semntico

Fuente

Objeto

COMPILADOR DE DOS O MS PASOS Anlisis semntico Generacin Cdigo

Fuente

Cdigo intermedio

Objeto

Figura 5.2. Esquema reducido de la compilacin en uno y dos pasos.

Las representaciones intermedias facilitan la optimizacin de cdigo. En este libro se van a describir dos tipos de representaciones intermedias: la notacin sufija, que se utiliza especialmente para las expresiones aritmticas, y la que utiliza tuplas o vectores (usualmente cudruplas) para representar las instrucciones que deben ser ejecutadas. La notacin sufija intenta sacar provecho de que la mayora de los procesadores disponen de una pila para almacenar datos auxiliares, y de que la evaluacin de expresiones con esta notacin puede realizarse con facilidad mediante el uso de una pila auxiliar. Para la representacin intermedia que utiliza tuplas, se abstraen primero las operaciones disponibles en un lenguaje ensamblador hipottico, lo suficientemente genrico para poder representar cualquier ensamblador real. El objetivo de esa abstraccin es decidir el nmero de componentes de las tuplas y la estructura de la informacin que contienen. Por ejemplo, es frecuente considerar que la primera posicin sea ocupada por la operacin que se va a realizar, las dos siguientes por sus operandos y la cuarta y ltima por el resultado. La cercana de esta representacin a los lenguajes simblicos (procesados por ensambladores) facilita la generacin del cdigo. La abstraccin introducida por las tuplas independiza esta representacin de los detalles correspondientes a una mquina concreta, lo que ofrece ventajas respecto a su portabilidad. Cuando se utilizan representaciones intermedias, la generacin de cdigo se reduce a un nuevo problema de traduccin (de la representacin intermedia al lenguaje objeto final), con la ventaja de que las representaciones intermedias son mucho ms fciles de traducir que los lenguajes de programacin de alto nivel. Las representaciones intermedias podran considerarse parte del anlisis semntico, ya que proporcionan formalismos para la representacin de su resultado. Sin embargo, en este libro se ha decidido describirlas con detalle en el captulo dedicado a la generacin de cdigo. Por un lado, las tcnicas y algoritmos necesarios para generar tuplas son anlogos a los necesarios para generar cdigo simblico o en lenguaje de la mquina, lo que aconseja que ambos tipos de generadores de cdigo sean descritos en el mismo captulo. Para simplifi-

196

Compiladores e intrpretes: teora y prctica

car la exposicin, tambin se incluir en el captulo de generacin de cdigo la otra representacin intermedia: la notacin sufija. En general, los compiladores de un solo paso suelen ser ms rpidos, pero ms complejos, por lo que existen muchos compiladores comerciales construidos en dos y tres pasos.

5.1.4. Anlisis semntico en compiladores de un solo paso


En los compiladores de un paso, no se utilizan representaciones intermedias, ya que la generacin del cdigo objeto se entremezcla con el anlisis semntico. En estos compiladores resulta ms complicado utilizar tcnicas de optimizacin de cdigo y de gestin de memoria. La Figura 5.3 muestra un esquema de esta situacin. En la parte izquierda de la figura se ven las dos primeras fases de la compilacin, que han sido explicadas en los captulos anteriores. En la parte inferior se muestra la cadena de unidades sintcticas correspondientes al programa del Ejemplo 5.1: (<palabra clave>,begin) (<tipo>,int)(<id>,A)(<simb>,;) (<id>,A)(<simbm>,:=)(<cons int>,100)(<simb>,;) (<id>,A)(<simbm>,:=)(<id>,A)(<simb>,+)(<id>,A)(<simb>,;) (<palabra clave>,output)(<id>,A) (palabra clave>,end) Se ha utilizado una representacin de pares para cada unidad sintctica, en los que el primer elemento representa el tipo y el segundo el texto concreto que corresponde en el programa a esa unidad. En este ejemplo se utilizan los siguientes nombres de unidades sintcticas: <palabra clave>, para las palabras claves del lenguaje. <tipo>, para las palabras que representan tipos en las declaraciones de variables y procedimientos. <id>, para los identificadores de las variables y los procedimientos. <simb>, para caracteres especiales. <simbm>, para palabras formadas por ms de un carcter especial. <cons int>, para nmeros enteros. En el rbol de la parte izquierda de la Figura 5.3 aparece el resultado del anlisis sintctico: el rbol de derivacin de la parte a). A veces es necesario modificar la tabla de smbolos durante los anlisis morfolgico y sintctico. La parte superior muestra esa posibilidad. La parte derecha de la Figura 5.3 contiene el resultado del anlisis semntico. En la mitad superior est el rbol de derivacin con anotaciones semnticas; en la inferior, un posible cdigo simblico equivalente al programa de partida. Como el compilador es de un solo paso, el cdigo debe generarse mientras se realiza el anlisis semntico.

<Programa>
Tipo Valor

Elemento

begin <sntncs> end <sntncs> <sntnc> int <asignacion> ; <sntncs> <sntnc> ; int <asignacion> <declrcn> int <tipo> <ids> int int <id> <id> := <expr> A <const.int> A

<kf> A

int

<declrcns> int A

<Programa> <sntncs> end

begin

<declrcns>

int
; <sntnc> ; <sntncs> <sntncs>

<sntnc>

<declrcn>

<sntnc>

<salida>

<tipo>

<ids> <asignacion> <sntnc>

<asignacion>

<id> := <expr> int int + <expr> int <id> int

int

<id>

<id> := <expr>

100 A

<expr> int <id> int A

output <expr> int <id> int A A

A <id> := <expr> <salida>

<const.int>

100 A <id> <id> <id> <expr> + <expr> output <expr>

segment .data _A dd 0 segment .codigo global _main _main

(<palabra clave>,begin) (<tipo>,int)(<id>,A)(<simb>,;)

(<id>,A)(<simbm>,;=) (<const int>,100)(<simb>,;) (<id>,A)(<simbm>,;=) (<id>,A)(<simb>,+)(<id>,A)(simb>,;) (<palabra clave>,output)(<id>,A) (<palabra clave>,end)

A. Sint. y Sem.
Cdigo

Captulo 5. Anlisis semntico

Anlisis morfo+sintctico

push dword 100 pop eax mov [_A]. eax push dword [_A] pop edx add eax,edx push eax pop eax mov [_A], eax push dword [_A} pop eax push eax call imprime_entero add esp, 4 call imprime_fin_linea ret

197

Figura 5.3. Esquema grfico detallado del proceso de compilacin en un solo paso.

198

<Programa>
Tipo Valor

Elemento

begin <sntncs> <sntnc> ; <sntnc> ; int <asignacion> <sntncs> <declrcn> int <tipo> <ids> int int <id> <id> := <expr> A A <const.int>

<kf> A

int

<declrcns> int A

end

<Programa> <sntncs> end

<sntncs>

begin

<declrcns>

int
<sntnc> ; <sntnc> ; <sntncs> <sntncs>

int <asignacion>

<sntnc>

<declrcn>

<salida>

<tipo>

<ids> <asignacion> <sntnc>

<asignacion>

<id> := <expr> int int + <expr> int

int

<id>

<id> := <expr>

100

<expr> int <id> int A

output <expr> int <id> int A <id> int A

A <id> := <expr> <salida>

<const.int>

Compiladores e intrpretes: teora y prctica

100 A <id> <id> <id> <expr> + <expr> output <expr>

segment .data _A dd 0 segment .codigo global _main _main

(<palabra clave>,begin) (<tipo>,int)(<id>,A)(<simb>,;)

A. Sem.
Cdigo

(<id>,A)(<simbm>,;=) (<const int>,100)(<simb>,;) (<id>,A)(<simbm>,;=) (<id>,A)(<simb>,+)(<id>,A)(simb>,;) (<palabra clave>,output)(<id>,A) (<palabra clave>,end)

Anlisis morfo+sintctico Fichero intermedio

Generacin cdigo, Optimizacin G. Memoria

push dword 100 pop eax mov [_A]. eax push dword [_A] POP edx add eax,edx push eax pop eax mov [_A], eax push dword [_A} pop eax push eax call imprime_entero add esp, 4 call imprime_fin_linea ret

Figura 5.4. Esquema grfico detallado del proceso de compilacin en ms de un paso.

Captulo 5. Anlisis semntico

199

5.1.5. Anlisis semntico en compiladores de ms de un paso


Cuando se disea un compilador de ms de un paso, se utilizan representaciones intermedias para facilitar las fases de optimizacin de cdigo y gestin de memoria que se realizarn en los pasos siguientes. La Figura 5.4 muestra grficamente esta situacin. Obsrvese que la salida del anlisis semntico es una representacin intermedia, que en la figura aparece contenida en un fichero. Las representaciones intermedias no tienen necesariamente que ocupar espacio en disco. En la Figura 5.4 se ve cmo un compilador, construido de acuerdo con este esquema, puede terminar la fase de generacin de cdigo optimizado realizando pasos adicionales sobre la representacin intermedia.

5.2 Gramticas de atributos


5.2.1. Descripcin informal de las gramticas de atributos y ejemplos de introduccin
Informalmente, se llamar atributos de un smbolo de la gramtica a toda informacin aadida en el rbol de derivacin por el analizador semntico, asociada a los smbolos de los nodos anotados. El Ejemplo 5.1 describe de forma simplificada el valor de esos atributos y sugiere cmo calcularlos. La otra componente importante de las gramticas de atributos es el algoritmo de clculo de los valores. Todos estos aspectos se presentarn con ms detalle mediante ejemplos. Ejemplo Considrese la gramtica independiente del contexto para las expresiones aritmticas enteras asociada a las siguientes reglas de produccin: 5.2 E::=E+E E::=-E E::=E*E E::=(E) E::=i E::=c Se supone que los smbolos terminales c e i hacen referencia a las constantes numricas y los identificadores, respectivamente. Los lenguajes de programacin suelen proporcionar reglas para determinar correctamente el valor y el tipo de las expresiones aritmticas. En nuestro ejemplo, el ltimo aspecto es trivial, ya que todas van a ser de tipo entero, pero en futuros ejemplos se podr comprobar su importancia. Es evidente que los smbolos de la gramtica que representan a los operandos tienen que poseer informacin respecto al tipo y el valor de stos, mientras los que se refieren a los operadores deben realizar la operacin correspondiente, es decir, aplicar las reglas del lenguaje para calcular el valor y el tipo de la expresin en la que aparecen.

200

Compiladores e intrpretes: teora y prctica

Si se considera la expresin (3+4)*5, que pertenece al lenguaje de la gramtica anterior, se puede concluir que su valor ser 35 y su tipo entero. La correccin del valor se basa en que 3+4=7 y 7*5=35. El tipo es entero, porque todos los operandos elementales lo son y los operadores no modifican el tipo. Es decir, como 3 y 4 son enteros, 7 (su suma) tambin lo es, y el producto de la suma por otro entero (5) es tambin un valor entero (35). Es evidente tambin que algunas partes de la expresin tienen que ser procesadas antes que otras. Por ejemplo, no se puede evaluar la expresin completa antes que la subexpresin (3+4). Lo mismo ocurre con el tipo. La Figura 5.5 muestra el rbol del anlisis de esta expresin, anotado para gestionar su tipo y su valor. Un rectngulo con lnea discontinua resalta las tareas propias del analizador morfolgico: al reconocer en la entrada las constantes 3, 4 y 5, tiene que indicar al analizador sintctico que ha identificado la unidad c, que sus valores son 3, 4 y 5, y su tipo entero (representado por i de int). Las flechas ascendentes sugieren el orden en el que se pueden realizar las anotaciones: primero las hojas, y las dos subexpresiones (3+4)y 5 antes que la expresin completa. La Figura 5.5 indica que las acciones que hay que realizar para evaluar la expresin y deducir su tipo se pueden resumir de la siguiente forma: el valor de las constantes es el del nmero asociado a ellas, y su tipo es entero; el valor de la suma es la suma de los valores de sus operandos, y su tipo es entero, como ellas; lo mismo ocurre con el producto. Los parntesis no modifican el valor ni el tipo de la expresin que contienen. Es fcil comprobar que estas acciones no
E E v:7 t:i v:35 t:i

E v:5 t:i

v:7 t:i

v:3 t:i

v:4 t:1

v:3 t:i (

v:4 t:i ) *

v:5 t:i

Figura 5.5. rbol de anlisis de la expresin (3+4)*5, anotado para calcular su tipo y su valor.

Captulo 5. Anlisis semntico

201

dependen de la expresin concreta, sino de la regla, es decir, que todas las sumas, productos, constantes y expresiones entre parntesis se tratan de la misma manera. Otra observacin importante tiene que ver con el hecho de que, en este ejemplo, todas las flechas que aparecen en la figura, que indican el orden de realizacin de las operaciones, son ascendentes, es decir: la informacin que se asigna a la parte izquierda de la regla se ha calculado utilizando nicamente informacin procedente de la parte derecha de la misma regla. Se puede plantear la posibilidad de que no siempre ocurra esto: existen casos en los que la informacin que se desea asociar a algn smbolo de la parte derecha necesite de la informacin asociada a otro smbolo de la parte derecha o de la parte izquierda de la regla? El prximo ejemplo tiene como objeto responder esta cuestin. Ejemplo Considrese la gramtica asociada a las siguientes reglas de produccin, donde se considera que el smbolo D es el axioma: 5.3 D::=TL T::=int T::=real L::=L,i L::=i Esta gramtica podra pertenecer a la parte declarativa de algn lenguaje de programacin, ya que T representa diferentes tipos de datos (en particular entero o int, o real). Es fcil comprobar que esta gramtica genera declaraciones de identificadores de tipo entero o real, de forma que en cada instruccin puede declararse un nico identificador o una lista de ellos separados por comas. Vamos a estudiar la instruccin int var1, x, y, en la que se declaran tres identificadores de tipo entero. El objetivo de este ejemplo es asociar a cada uno de ellos el tipo con que han sido declarados. Dicho tipo se conoce desde que se detecta el smbolo terminal int. El tipo es el mismo para todos, y tiene que anotarse para cada uno de los identificadores de la lista. La Figura 5.6 muestra el rbol anotado con los tipos de los identificadores. Por convenio, inicialmente se asigna a los identificadores un valor igual a 0. Obsrvese que las flechas que sugieren un posible orden en las anotaciones indican que es imprescindible conocer en primer lugar el tipo del smbolo no terminal T. Dicho tipo puede usarse para anotar, si es necesario, el tipo de la raz del rbol (D) y el del smbolo L, hermano de T. A partir de aqu, se sabe que el identificador y es de tipo entero, as como var1 y x, mediante las dos apariciones ms profundas del smbolo L. Las acciones que completaran las anotaciones del rbol pueden resumirse as: el tipo del smbolo no terminal D debe ser igual al de su primer hijo T, y tambin pasar a ser el de su segundo hijo L. El tipo de este smbolo (L) pasar a ser el de su primer hijo (L) y el del identificador y. El tipo del identificador x ser el de su padre (L) y tambin lo ser del otro hijo, que termina en el identificador var1. Por lo tanto, en la regla D::=TL, el tipo de la raz se calcula utilizando el de su hijo T, y el de L se calcula tambin de la misma manera. En las dos apariciones de la regla L::=L,i y en la de la regla L::=i, el tipo del padre se utiliza para calcular el de sus hijos (L e i).

202

Compiladores e intrpretes: teora y prctica

D t:i T t:i L t:i

t:i L

t:i L

v:0 t:i v:0 t:i i int var1 , i v:0 t:i , i

Figura 5.6. rbol de anlisis de la expresin int var1, x, y, anotado para calcular su tipo.

Es fcil imaginar cmo se podra incorporar aqu la gestin de la tabla de smbolos de un compilador. Una vez que se conoce el nombre y el tipo de cada identificador, se podra comprobar que no colisiona con ningn otro elemento de la tabla de smbolos, antes de realizar su insercin en ella. Las conclusiones de estos ejemplos son las siguientes: Para la realizacin del anlisis semntico se necesita asociar cierta informacin a cada smbolo de la gramtica, as como describir las acciones necesarias para calcular el valor de dicha informacin en cada punto del rbol de anlisis. En las prximas secciones se ver que la informacin semntica se formaliza mediante los atributos semnticos, que se calculan mediante la ejecucin de acciones semnticas. Puede ser conveniente disponer de informacin global, que no dependa de ningn smbolo y sea accesible desde cualquier regla de la gramtica. La informacin que se asocia a cada smbolo depende slo de ste, es decir, ser la misma para todas las apariciones del mismo smbolo. Evidentemente, el valor concreto que reciba dicha informacin depender del lugar en el que aparezca cada smbolo. Las acciones que hay que realizar para calcular los valores de los atributos dependen de las reglas, no de los smbolos: en cada aplicacin de la misma regla en el rbol de anlisis se aplicarn las mismas acciones para calcular los valores asociados a sus smbolos.

Captulo 5. Anlisis semntico

203

5.2.2. Descripcin formal de las gramticas de atributos


El objetivo de esta seccin es definir formalmente los conceptos que se han presentado informalmente mediante ejemplos en la seccin anterior. Se llama gramtica de atributos1 a una extensin de las gramticas independientes del contexto a las que se aade un sistema de atributos. Un sistema de atributos est formado por: Un conjunto de atributos semnticos que se asocia a cada smbolo de la gramtica. Los datos globales de la gramtica, accesibles desde cualquiera de sus reglas, pero no asociados a ningn smbolo concreto. Un conjunto de acciones semnticas, distribuidas por las reglas de produccin. De forma semejante a las variables en los lenguajes de programacin de alto nivel, un atributo semntico se define como un par, compuesto por un tipo de datos (la especificacin de un dominio o conjunto) y un nombre o identificador. En algunos ejemplos de este captulo, debido a su simplicidad, el dominio de los atributos es irrelevante y se omitir. En cada momento, cada atributo semntico puede tener un valor nico que tiene que pertenecer a su dominio. Dicho valor puede modificarse en las acciones semnticas, sin ningn tipo de restriccin. Una accin semntica es un algoritmo asociado a una regla de la gramtica de atributos, cuyas instrucciones slo pueden referirse a los atributos semnticos de los smbolos de la regla y a la informacin global de la gramtica de atributos. El objetivo de la accin semntica es calcular el valor de alguno de los atributos de los smbolos de su regla, sin restricciones adicionales. En este captulo se utilizar la siguiente notacin para las gramticas de atributos: A = (T, N, S, P, K) Se mantiene la estructura de las gramticas independientes del contexto. A cada smbolo de la gramtica lo acompaa la lista de sus atributos semnticos entre parntesis. El nombre de cada atributo sigue al de su dominio. Se utiliza la misma notacin que en el resto de los ejemplos de pseudocdigo. En los smbolos sin atributos semnticos se omitirn los parntesis. Las reglas de produccin se modifican para distinguir distintas apariciones del mismo smbolo. A cada regla de produccin lo acompaa su accin semntica, en la que tambin se utilizar la misma sintaxis de los ejemplos de pseudocdigo. Las instrucciones de la accin se escriben entre llaves. Para referirse a un atributo semntico de un smbolo dentro de las acciones semnticas, se escribir el nombre del atributo tras el del smbolo, separados por un punto .. Si alguna regla no necesita realizar ninguna accin semntica, se escribir {}. La informacin global se aade como nueva componente adicional, al final de la gramtica de atributos (K).
A lo largo de la historia de los lenguajes formales ha habido diversas ideas que han desembocado en las gramticas de atributos. Algunas slo proponen nombres distintos para el mismo concepto; otras s describen aspectos diferentes y han tenido relevancia en distintos momentos. En este texto se utilizar la denominacin gramticas de atributos para unificar y clarificar la exposicin.
1

204

Compiladores e intrpretes: teora y prctica

Ejemplo A continuacin se muestra la gramtica de atributos del Ejemplo 5.2: 5.4 A5_4={ T={+, *, (, ), c(valor,tipo), i(valor,tipo)}, N={E(valor,tipo)}, E, P={ E::=Ei+Ed { E.valor = Ei.valor+Ed.valor; E.tipo = Ei.tipo;}, E::=-Ed, { E.valor = -Ed.valor; E.tipo = Ed.tipo; }, E::=Ei*Ed { E.valor = Ei.valor*Ed.valor; E.tipo = Ei.tipo;}, E::=(Ed) { E.valor = Ed.valor; E.tipo = Ed.tipo}, E::=i { E.valor = i.valor, E.tipo = i.tipo}, E::=c { E.valor = c.valor, E.tipo = c.tipo} }, K=} Ejemplo A continuacin se muestra la gramtica de atributos del Ejemplo 5.3: 5.5 A5_5={ T={int(tipo), real(tipo), i(tipo)}, N={D(tipo),T(tipo),L(tipo)}, D, P={ D::=TL { L.tipo = T.tipo; D.tipo = L.tipo;}, T::=int {T.tipo = entero;}, T::=real {T.tipo = real;}, L::=Ld,i { Ld.tipo = L.tipo; i.tipo = L.tipo;}, L::=i {i.tipo = L.tipo;} }, K=}

Captulo 5. Anlisis semntico

205

5.2.3. Propagacin de atributos y tipos de atributos segn su clculo


Se llama propagacin al clculo del valor de los atributos en funcin del valor de otros. Se define as una relacin de dependencia entre los atributos que aparecen en el rbol de anlisis de una instruccin, ya que los atributos asociados a un nodo dependen de los atributos necesarios para calcular su valor. Las flechas que sugieren el orden de clculo de los atributos en las Figuras 5.5, 5.6 y 5.7 muestran realmente el proceso de la propagacin de atributos o, lo que es lo mismo, su relacin de dependencia. En la Seccin 5.3.5 se estudiar con ms detalle dicha relacin de dependencia y su importancia para el anlisis semntico. Los atributos semnticos se pueden clasificar segn la posicin, en la regla de produccin, de los smbolos cuyos atributos se utilicen en el clculo. Se distinguen dos grupos: Atributos sintetizados: Son los atributos asociados a los smbolos no terminales de la parte izquierda de las reglas de produccin cuyo valor se calcula utilizando los atributos de los smbolos que estn en la parte derecha correspondiente. Obsrvese la siguiente regla de la gramtica del Ejemplo 5.4: E::=Ei+Ed { E.valor = Ei.valor+Ed.valor; E.tipo = Ei.tipo;}, Los atributos valor y tipo del smbolo no terminal de la parte izquierda (E) se calculan a partir de los de la parte derecha (Ei y Ed), por lo que se puede afirmar que son sintetizados. Atributos heredados: El resto de las situaciones originan atributos heredados, es decir, atributos asociados a smbolos de la parte derecha de las reglas cuyo valor se calcula utilizando los atributos de la parte izquierda o los de otros smbolos de la parte derecha. En este grupo es necesaria la siguiente distincin: Atributos heredados por la derecha, cuando el clculo del valor de un atributo utiliza atributos de los smbolos que estn situados a su derecha. Atributos heredados por la izquierda, cuando slo se utilizan los que estn a su izquierda, ya sea en la parte derecha de la regla o en la parte izquierda. Obsrvese la siguiente regla de la gramtica del Ejemplo 5.5: D::=TL { L.tipo = T.tipo; D.tipo = L.tipo;} El atributo tipo del smbolo no terminal L lo hereda de su hermano izquierdo (T). Vanse tambin las siguientes reglas: L::=Ld,i { Ld.tipo = L.tipo;

206

Compiladores e intrpretes: teora y prctica

i.tipo = L.tipo;} L::=i {i.tipo = L.tipo;} El atributo tipo del smbolo terminal i se hereda del padre (en ambos casos L). Ejemplo Muestra una gramtica de atributos que rene los dos ltimos ejemplos, para describir un lenguaje de programacin un poco ms realista. El lenguaje contiene la parte declarativa del 5.6 Ejemplo 5.5 y las expresiones aritmticas del Ejemplo 5.4. Se aade un nuevo axioma, para indicar que esta gramtica genera programas completos, y un nuevo smbolo no terminal (A), que realiza la asignacin de una expresin a un identificador mediante el smbolo =. La gramtica incorpora una tabla de smbolos, donde se conserva el tipo con el que se declaran los atributos, que ayuda a comprobar la correccin semntica de las instrucciones en las que aparecen dichos atributos. A5_6={ T={+, *, (, ), c(valor,tipo), i(valor,tipo), int(tipo), real(tipo), i(tipo), = }, N={Programa, A(tipo), E(valor,tipo), D(tipo),T(tipo),L(tipo)}, Programa, P={ Programa::=DA {}, A::=i = E { SI iTablaSimbolos E.tipo=i.tipo ENTONCES A.tipo=i.tipo;} E::=Ei+Ed { E.valor = Ei.valor+Ed.valor; E.tipo = Ei.tipo;}, E::=-Ed, { E.valor = -Ed.valor; E.tipo = Ed.tipo; }, E::=Ei*Ed { E.valor = Ei.valor*Ed.valor; E.tipo = Ei.tipo;}, E::=(Ed) { E.valor = Ed.valor; E.tipo = Ed.tipo}, E::=i { E.valor = i.valor, E.tipo = i.tipo}, E::=c { E.valor = c.valor, E.tipo = c.tipo} D::=TL { L.tipo = T.tipo;

Captulo 5. Anlisis semntico

207

D.tipo = L.tipo;}, T::=int {T.tipo = entero;}, T::=real {T.tipo = real;}, L::=Ld,i { Ld.tipo = L.tipo; i.tipo = L.tipo; insertar(TablaSimbolos, i, L.tipo);}, L::=i { i.tipo = L.tipo; insertar(TablaSimbolos, i, L.tipo);} }, K={TablaSimbolos}} En esta gramtica, TablaSimbolos es una tabla de smbolos en la que se conserva (al menos) informacin sobre el tipo de cada identificador. Asociada a esta tabla se dispone de la operacin insertar(TablaSimbolos, identificador, tipo), mediante la que se deja constancia en la tabla de que se ha declarado el identificador cuyo nombre es el segundo argumento y cuyo tipo es el tercero. Obsrvese que la regla A::=i = E { SI iTablaSimbolos E.tipo=i.tipo ENTONCES A.tipo=i.tipo;} realiza las comprobaciones semnticas sobre la correspondencia de tipos entre la expresin y el identificador al que se asigna su valor. Obsrvese tambin que las dos reglas en las que aparece el smbolo terminal i en la parte declarativa insertan dicho identificador en la tabla de smbolos. L::=Ld,i { Ld.tipo = L.tipo; i.tipo = L.tipo; insertar(TablaSimbolos, i, L.tipo);} L::=i { i.tipo = L.tipo; insertar(TablaSimbolos, i, L.tipo);} } La Figura 5.7 muestra el anlisis del programa int var1, x, y x = (3+4)*5 Tambin puede observarse el contenido de la tabla de smbolos al final del anlisis, que inicialmente se encuentra vaca.

208

Compiladores e intrpretes: teora y prctica

Programa D t:i T t:i t:i L t:i L v:3 t:i E t:i L A TS y(int) x(int) var1(int) E E v:7 t:i v:7 t:i E v:4 t:i cv:5 t:i ) * 5 t:i E v:35 t:i E v:5 t:i

v:0 i t:i int var1

i x

v:0 t:i ,

i v:0 t:i y

v:0 i t:i x = (

v:3 c t:i 3

c v:4 t:i 4

Figura 5.7. rbol de anlisis del Ejemplo 5.6.

Las flechas que sugieren el orden de evaluacin de los atributos muestran que es necesario procesar primero el subrbol izquierdo que contiene la parte declarativa. Dentro de este subrbol se aplica lo explicado en la Figura 5.6. Al terminar el proceso de dicho subrbol, la tabla de smbolos contiene toda la informacin que necesita. Al analizar la asignacin, el identificador al que se le asigna nuevo valor (x) es buscado en la tabla de smbolos, para comprobar que ha sido declarado antes de ser usado y para consultar el tipo con el que se lo declar (int). Esta informacin est contenida en la anotacin del nodo del smbolo i, padre del nodo x. A continuacin puede procesarse el subrbol de la expresin (el que tiene como raz la aparicin menos profunda del smbolo no terminal E). En este subrbol se aplica lo explicado en la Figura 5.5. Finalmente, la asignacin puede concluirse correctamente en el nodo etiquetado con el smbolo no terminal A. Tanto el identificador como la expresin son de tipo entero.

5.2.4. Algunas extensiones


Es frecuente que las gramticas de atributos contengan acciones semnticas situadas entre los smbolos de la parte derecha de las reglas. Hay varias razones para permitirlo: por un lado, se ofrece mayor flexibilidad al diseador de la gramtica; por otro, hay algunos analizadores semnticos que pueden procesar las acciones en esa posicin. Ejemplo La siguiente gramtica de atributos presenta una versin simplificada de las instrucciones de asignacin de expresiones a identificadores: 5.7

Captulo 5. Anlisis semntico

209

A5_7={ T={=, +, cte(valor, tipo), id(valor, tipo)}, N={ Asignacin(valor, tipo), Expresin(valor, tipo), Trmino{valor, tipo}}, Asignacin, P={ Asignacin ::= id { Comprobacin en la tabla de smbolos de que el identificador ha sido declarado y recuperacin de su tipo; } = Expresin { Comprobacin de la compatibilidad de los tipos Expresin.tipo e id.tipo; id.valor = Expresin.valor;}, Expresin ::= Expresind + Trmino { Comprobacin de compatibilidad de los tipos Expresind.tipo y Trmino.tipo; Expresin.tipo = 2 tipoSuma(Expresind.tipo, Trmino.tipo); Expresin.valor=Expresind.valor+Trmino.valor;}, Expresin ::= Trmino { Expresin.tipo = Trmino.tipo; Expresin.valor = Trminao.valor;}, Trmino ::= id { Comprobacin en la tabla de smbolos de que el identificador ha sido declarado y recuperacin de su tipo; Trmino.tipo = id.tipo; Trmino.valor = id.valor;}, Trmino ::= cte { Trmino.tipo = cte.tipo; Trmino.valor = cte.valor; } }, K=} En este ejemplo, la informacin relacionada con el tipo y los valores se almacena con el mismo criterio de los ejemplos anteriores. La primera accin semntica no est situada al final de la primera regla.
2 La funcin tipoSuma toma como argumentos dos tipos y determina el tipo que le correspondera, con esos tipos, a la operacin suma de dos operandos.

210

Compiladores e intrpretes: teora y prctica

En este captulo se supondr, casi siempre, que las gramticas de atributos tienen las acciones semnticas situadas al final de la regla, y cuando esto no se aplique se indicar explcitamente que la gramtica no cumple dicha condicin. Siempre es posible transformar una gramtica de atributos con acciones semnticas en cualquier posicin en otra equivalente con todas las acciones semnticas al final de las reglas. El algoritmo correspondiente realiza los siguientes pasos, mientras existan reglas de produccin con la siguiente estructura: N::= X1 X2 ... Xi-1 {Instrucciones} Xi ... Xn 1. Se aade un nuevo smbolo no terminal a la gramtica (por ejemplo, Y) sin atributos semnticos asociados. 2. Se aade la siguiente regla de produccin: Y::= {Instrucciones} 3. Se sustituye la regla inicial por la siguiente: N::= X1 X2 ... Xi-1 Y Xi ... Xn Es fcil comprobar que el lenguaje generado por ambas gramticas independientes del contexto es el mismo, y que la informacin semntica no ha cambiado. En este ejemplo se obtendra la siguiente gramtica: A5_7,={T, N{M}, Asignacin, P {M::= { Comprobacin en la tabla de smbolos de que el identificador ha sido declarado y recuperacin de su tipo; }}, K=} El problema de este enfoque es que los nuevos smbolos no terminales no recogen ninguna semntica del problema, ya que son meros marcadores. Adems, es posible que no se desee aadir reglas-. En situaciones reales, en las que puede haber muchas reglas de produccin, a veces conviene modificar la gramtica un poco ms, para que cada smbolo y cada regla retengan algo de significado. En tal caso, podra conseguirse lo mismo con el siguiente algoritmo: 1. Se aade un nuevo smbolo no terminal a la gramtica, para representar todos los smbolos de la parte derecha, desde su comienzo hasta el smbolo seguido por la accin semntica (por ejemplo, X1_i-1). Este nuevo smbolo no tiene atributos semnticos asociados. 2. Se aade la siguiente regla de produccin: X1_i-1::= X1 X2 ... Xi-2 Xi-1 {Instrucciones} 3. Se sustituye la regla inicial por la siguiente: N::= X1_i-1 Xi ... Xn

Captulo 5. Anlisis semntico

211

Por ejemplo, se podra pensar que en la gramtica anterior se necesitan realmente dos reglas de produccin para la asignacin. La primera conservara la estructura de la actual, pero sustituyendo el smbolo terminal id por el nuevo no terminal id_asignado. El nuevo smbolo tendr una regla en la que, junto con la accin semntica, tambin se arrastra el resto de la derivacin: Asignacin ::= id_asignado Expresin { Comprobacin de la compatibilidad de los tipos Expresin.tipo e id.tipo; id_asignado.valor = Expresin.valor;},

id_asignado ::= id { Comprobacin en la tabla de smbolos de que el identificador ha sido declarado y recuperacin de su tipo; id_asignado.tipo = id.tipo;}, Resulta fcil comprobar la equivalencia del resultado de este enfoque, basado en el segundo algoritmo y las dos gramticas anteriores. Obsrvese que no se ha aadido ninguna regla-, aunque s una regla de redenominacin.

5.2.5. Nociones de programacin con gramticas de atributos


En el desarrollo del analizador semntico de un compilador, es necesario solucionar con gramticas de atributos las dificultades asociadas a todas las construcciones del lenguaje de programacin considerado. En esta seccin se va a reflexionar, mediante ejemplos, sobre algunas caractersticas del diseo de las gramticas de atributos. Es fcil comprobar que la posibilidad de especificar cualquier algoritmo en las acciones semnticas de las reglas implica que las gramticas de atributos tengan la misma potencia que las Mquinas de Turing: son capaces de expresar el algoritmo asociado a cualquier tarea computable. Por lo tanto, podra considerarse que las gramticas de atributos constituyen un lenguaje de programacin en s mismo. El objetivo de los prximos puntos es destacar las peculiaridades de este nuevo lenguaje de programacin. Todos los ejemplos que se van a exponer en esta seccin estn relacionados con el lenguaje de los parntesis, compuesto por expresiones escritas exclusivamente con parntesis equilibrados, en los que cada parntesis abierto se cierra dentro de la expresin siguiendo las normas habituales de las expresiones matemticas. El lenguaje considerado en estos prrafos tiene la

212

Compiladores e intrpretes: teora y prctica

peculiaridad de que la expresin completa est encerrada siempre entre parntesis. Es decir, las siguientes palabras pertenecen al lenguaje descrito: ( () () ) ( ()(())() )

Pero las siguientes no pertenecen a l: ( () () () Todos los ejemplos utilizan como punto de partida la siguiente gramtica independiente del contexto: G5_8={ T={ (, ) }, N={ <lista>, <lista_interna>}, <lista>, P={<lista> ::= ( <lista_interna> ) <lista_interna>::=<lista_interna>(<lista_interna>) <lista_interna>::=} } El axioma representa la expresin completa: una lista interna entre parntesis. La lista interna ms sencilla es la palabra vaca. La regla recursiva para la lista interna la describe como una lista interna junto a otra encerrada entre parntesis. Es fcil comprobar la correspondencia entre esta gramtica y el lenguaje especificado. Consideraremos los dos problemas siguientes: Clculo de la profundidad (o nivel de anidamiento) de la expresin. El concepto queda definido por los siguientes ejemplos: (), tiene profundidad 1 ( () ( ( ) ) ), tiene profundidad 3, debido a la sublista derecha Clculo del nmero de listas de la expresin por ejemplo, (), contiene una lista ( () ( ( ) ) ) contiene 3 listas ( () () () () ) contiene 5 listas ( ( ((())) () )) contiene 6 listas Aunque los ejemplos son casos particulares pequeos, las conclusiones extradas de ellos se pueden aplicar al diseo de gramticas de atributos en general. Ejemplo La siguiente gramtica de atributos calcula la profundidad de la lista estudiada. El diseo de esta gramtica se basa en el clculo clsico de la profundidad de una lista con un algoritmo recursivo 5.8 que aproveche la estructura de las reglas de la gramtica. De esta forma, basta con introducir un nico atributo de tipo entero (profundidad).

Captulo 5. Anlisis semntico

213

En la regla del axioma <lista>::=(<lista_interna>), habr que aadir una unidad a la profundidad de la <lista_interna>. Una de las reglas para el smbolo no terminal <lista_interna> sirve para finalizar la recursividad: <lista_interna>::=, que corresponde claramente a una lista de profundidad 0. En cambio, en la regla recursiva: <lista_interna>::=<lista_interna>1(<lista_interna>2) hay que determinar cul de las sublistas tiene la mayor profundidad. Si es la primera, la profundidad de la lista interna completa coincidir con la suya; si es la segunda, ser necesario incrementarla en una unidad, pues est encerrada entre parntesis. Vase la gramtica completa: A5_8={ T={ (, ) }, N={<lista>(entero profundidad;), <lista_interna>(entero profundidad)}, <lista>, P={ <lista>::= ( <lista_interna> ) { <lista>.profundidad= <lista_interna>.profundidad + 1; IMPRIMIR(PROFUNDIDAD:,<lista>.profundidad);}, <lista_interna>::=<lista_interna>1 (<lista_interna>2) { SI ( <lista_interna>1.profundidad > <lista_interna>2.profundidad ) <lista_interna>.profundidad = <lista_interna>1.profundidad ; EN OTRO CASO <lista_interna>.profundidad = <lista_interna>2.profundidad + 1 ;}, <lista_interna>::= { <lista_interna>.profundidad = 0;} }, K=} La Figura 5.8 muestra el clculo de la profundidad de la lista (()(())) mediante la gramtica de atributos de este ejemplo. Se han utilizado flechas discontinuas para indicar la sntesis de los atributos. Los nombres de los atributos han sido sustituidos por sus iniciales. Se puede observar que es posible solucionar este problema utilizando nicamente atributos sintetizados, porque lo que se desea calcular slo depende de una parte concreta de toda la expresin: la sublista ms profunda.

214

Compiladores e intrpretes: teora y prctica

<lista>

p:3

<lista_interna>

p:2

<lista_interna> p:1

<lista_interna> p:1

<lista_interna> p:0

<lista_ interna> p:0

<lista_interna> p:0

<lista_ interna> p:0

Figura 5.8. Clculo de la profundidad de la lista (()(())) para el Ejemplo 5.8.

Ejemplo La siguiente gramtica de atributos calcula el nmero de listas de la expresin. La estructura de las reglas de la gramtica independiente del contexto sugiere que la nica regla asociada al axio5.9 ma, <lista>::=(<lista_interna>), debe sintetizar un atributo semntico cuyo valor sea el nmero total de listas de la expresin. Si este atributo se llama num_listas_total, est claro que su valor correcto se obtendr sumando una unidad al nmero de listas, calculado por el smbolo no terminal <lista_interna>. Para ilustrar el clculo de listas de una lista interna se puede considerar el siguiente ejemplo: ( ( ((())) () )) Se supondr que la cadena se recorre de izquierda a derecha. Es fcil comprobar que el nmero de listas correspondiente a la lista resaltada es igual a 3. A su derecha hay una lista ms, y el conjunto est incluido en otras dos listas. Eso hace un total de 6 (3+1+2). Este proceso acumulativo sugiere que cada lista interna debe calcular el nmero de sus listas, sumarlo al nmero de listas encontradas en la cadena que la precede y ofrecer el valor total al resto del anlisis. Puesto que las listas tienen que estar equilibradas, si se identifica una nueva lista por su parntesis de apertura, al llegar a la sublista resaltada anterior se llevarn ya contabilizadas 2 listas. El clculo que debe realizar la lista resaltada consistir en aadir las suyas (3) y ofrecer el valor total (5) al resto del proceso. Tras la parte resaltada slo queda una lista ms, que completar el valor correcto. Puede utilizarse un atributo para recibir el nmero de listas de la parte procesada (se lo denominar num_listas_antes) y otro para el que se ofrezca al resto del anlisis (num_listas_despues).

Captulo 5. Anlisis semntico

215

Hay que distribuir el clculo del nmero de listas del smbolo <lista_interna> entre las reglas donde aparece. Puesto que sus reglas son recursivas, comenzaremos por aquella que permite dar por terminada la recursin: <lista_interna>::=, donde es evidente que no hay que hacer nada, pues esta regla no aade nuevas listas, por lo que el valor de num_listas_despues coincidir con el de num_listas_antes. Tambin est claro el clculo necesario para la regla recursiva <lista_interna>::= <lista_interna>1(<lista_interna>2). El valor de <lista_interna>.num_listas_antes es el nmero de listas encontradas hasta el momento, y es el mismo que habr hasta <lista_interna>1. El nmero de listas antes de procesar <lista_interna>2 tiene que aadir 1 (por la lista que aparece de forma explcita) despus de procesar <lista_interna>1. El nmero de listas despus de procesada esta regla, coincide con el obtenido despus de procesar <lista_interna>2.

Falta asociar un valor inicial al atributo num_listas_antes del smbolo no terminal <lista_interna> en la regla del axioma. Es evidente que la lista que aparece de forma explcita en dicha regla es la primera de la expresin completa, por lo que el valor del atributo tiene que ser 1. Vase la gramtica de atributos completa: A5_9={ T={ (, ) }, N={ <lista>(entero num_listas_total;), <lista_interna>( entero num_listas_antes; entero num_listas_despues)}, <lista>, P= {<lista> ::= ( <lista_interna> ) { <lista_interna>.num_listas_antes = 1; <lista>.num_listas_total = <lista_interna>.num_listas_despues; IMPRIMIR(ELEMENTOS,<lista>.num_listas_total);}, <lista_interna>::=<lista_interna>1(<lista_interna>2) { <lista_interna>1.num_listas_antes = <lista_interna>.num_listas_antes; <lista_interna>2.num_listas_antes = <lista_interna>1.num_listas_despues+1; <lista_interna>.num_listas_despues = <lista_interna>2.num_listas_despues;}, <lista_interna>::= { <lista_interna>.num_listas_despues = <lista_interna>.num_listas_antes;}}, K=}

216

Compiladores e intrpretes: teora y prctica

<lista>

t:4

<lista_interna>

a:1 d:4

<lista_interna>

a:1 d:2

<lista_interna> a:3 d:4

<lista_interna> a:1 d:1

<lista_ interna> a:2 d:2

<lista_interna> a:3 d:3

<lista_ interna> a:4 d:4

Figura 5.9. Clculo del nmero de listas de la expresin (()(())) para el Ejemplo 5.9.

La Figura 5.9 muestra el clculo del nmero de listas de la expresin (()(())) mediante la gramtica de atributos de este ejemplo. Se han utilizado dos tipos de flechas discontinuas para distinguir la herencia de la sntesis de atributos. Se pueden extraer las siguientes conclusiones: El axioma tiene asociado un atributo sintetizado de tipo entero (num_listas_total), que representa el nmero total de listas de la expresin. Las listas internas tienen dos atributos de tipo entero, uno (num_listas_antes) heredado a veces de su padre, a veces de su hermano ms a la izquierda, que indica el nmero de listas que haba en la expresin antes del proceso de esta lista interna, y el otro (num_listas_despues) sintetizado, que recoge las modificaciones en el nmero de listas debidas a la regla. El atributo heredado es necesario, porque el valor que se quiere calcular puede depender de partes diferentes de la expresin. Ejemplo A continuacin se muestra otra gramtica de atributos que tambin calcula el nmero de lis5.10 tas de la expresin. Puede compararse con la del ejemplo anterior. En este caso se ha aprovechado la posibilidad de utilizar, como informacin global, una variable de tipo entero (num_elementos) que se declara e inicializa con el valor 0 en la ltima componente de la gramtica (K). Para obtener el valor correcto, bastar con incrementar el valor de dicho atributo en una unidad cada vez que aparece una lista de forma explcita en la parte derecha de alguna regla.

Captulo 5. Anlisis semntico

217

A5_10={ T={ (, ) }, N={<lista>(), <lista_interna>()}, <lista>, P={ <lista>::=( <lista_interna> ) { num_elementos = num_elementos + 1; IMPRIMIR(HAY ,num_elementos, LISTAS );}, <lista_interna>::= <lista_interna> ( <lista_interna> ) { num_elementos++;}, <lista_interna>::={ }}, K={entero num_elementos=0;} } La comparacin de esta gramtica con la del Ejemplo 5.9 ofrece una conclusin interesante: los atributos heredados pueden sustituirse fcilmente por informacin global. La importancia de esta conclusin se analizar con ms detalle en las prximas secciones, en las que se ver que esta sustitucin puede ser necesaria en determinadas circunstancias.

Incorporacin del analizador semntico 5.3 al sintctico


5.3.1. Dnde se guardan los valores de los atributos semnticos?
La primera pregunta que hay que responder, al incorporar el anlisis semntico al sintctico, es dnde se pueden guardar los valores de los atributos que se han aadido a la gramtica independiente del contexto. El analizador sintctico manipula los smbolos de la gramtica movindolos entre la entrada y la pila del anlisis. De forma general, los analizadores semnticos sustituyen los smbolos manipulados por el analizador semntico por una estructura de datos, que contendr, adems de la unidad sintctica que se va a analizar, los atributos semnticos especificados en la gramtica. Desde este momento se supondr que los algoritmos descritos en el Captulo 4, en lugar de smbolos, manipulan estructuras que contienen la informacin semntica de cada smbolo. Esta misma modificacin es necesaria en el analizador morfolgico. La estructura de cada unidad sintctica generada por dicho analizador debe incorporar la informacin semntica que lleva asociada. Por ejemplo, cuando se reconozca el nombre de una variable, el analizador morfolgico debe proporcionar dicho nombre, porque es necesario para las acciones semnticas de bsqueda e insercin en la tabla de smbolos; esta informacin puede sustituirse por un puntero al elemento de la tabla asociado a la variable. De igual manera, cuando se analice una constante, se debe proporcionar tambin su valor.

218

Compiladores e intrpretes: teora y prctica

5.3.2. Orden de recorrido del rbol de anlisis


En las secciones y captulos anteriores se ha dicho que el analizador sintctico y la anotacin semntica pueden recorrer en un orden determinado el rbol de derivacin de los programas compilados. En esta seccin se reflexionar con ms profundidad sobre las repercusiones que tiene el orden de recorrido en la construccin de compiladores. Los analizadores descendentes siguen el que se conoce como recorrido en profundidad por la izquierda con vuelta atrs, lo que significa que, entre todos los nodos posibles, siempre se contina por uno de los ms profundos (profundidad). En caso de empate, se toma el nodo que aparezca lo ms a la izquierda posible (por la izquierda). Siempre que se llegue a una hoja, hay que volver hasta la ltima decisin tomada y elegir uno de los nodos no visitados (con vuelta atrs). La Figura 5.10 muestra grficamente el recorrido que se hara siguiendo este orden sobre el rbol de la Figura 5.6. Obsrvese que los nodos se visitan en el siguiente orden (se resaltan en cursiva los nodos visitados ms de una vez): D, T, int, T, D, L, L, L, i, var1, i, L, L, ,, L, i, x, i, L, L, ,, L, i, y, i, L, D. Los analizadores ascendentes siguen el orden determinado por la reduccin de los asideros que se van encontrando, realizando desplazamientos cuando la reduccin no es posible. La Figura 5.11 muestra grficamente este recorrido para el rbol de la Figura 5.6.

D T

i int var1 ,

i x ,

i y

Figura 5.10. Recorrido de un analizador descendente sobre el rbol de anlisis de la Figura 5.6.

Captulo 5. Anlisis semntico

219

D T

i int var1 ,

i x ,

i y

Figura 5.11. Recorrido de un analizador ascendente sobre el rbol de anlisis de la Figura 5.6.

Obsrvese que los nodos se visitan en el siguiente orden: int, T, var1, i, L, ,, x, i, L, ,, y, i, L, D. En las secciones anteriores de este captulo se ha sealado que las acciones semnticas definen una relacin de dependencia en funcin de la propagacin de los atributos. Esa relacin induce el orden en que se puede aadir la informacin semntica mientras se recorre el rbol. Las Figuras 5.5, 5.6 y 5.7 mostraban grficamente, mediante flechas, dicha relacin. Por lo tanto, al procesar el programa de entrada, se realizarn al menos dos recorridos que requieren un orden determinado. Cabra plantearse las siguientes preguntas: el orden en que se realiza el anlisis sintctico y el exigido por el anlisis semntico son siempre compatibles? En caso negativo, es necesario conseguir que lo sean? En caso afirmativo, es siempre posible hacerlos compatibles? Para contestar a la primera pregunta basta analizar la Figura 5.12. En ella se superponen los rdenes de los analizadores de las Figuras 5.10 y 5.11 al que sugieren las dependencias de los atributos. En la parte a), que corresponde al analizador descendente, ambos rdenes son compatibles, ya que el recorrido de la flecha discontinua encuentra los nodos en el mismo orden que sugieren las flechas continuas. Sin embargo, en la parte b), que corresponde al analizador ascendente, los rdenes no son compatibles. Aunque el nodo T se visita en el orden adecuado (antes que cualquiera de los otros smbolos no terminales), al llegar al nodo var1, y posteriormente al L, no es posible aadir la informacin de su tipo porque, a pesar de que es el mismo que el de T, lo recibe de su hermano L y ese nodo todava no ha sido visitado.

220

Compiladores e intrpretes: teora y prctica

D t:i T t:i L t:i a) t:i L t:i L

v:0 t:i i int var1 ,

v:0 i t:i x ,

i y

v:0 t:i

D t:i T t:i L t:i b) t:i L t:i L

v:0 t:i i int var1 ,

v:0 i t:i x ,

i y

v:0 t:i

Figura 5.12. Comparacin de los rdenes de recorrido del rbol por las dependencias entre los atributos y por los analizadores sintcticos.

Captulo 5. Anlisis semntico

221

Para contestar a la segunda pregunta hay que tener en cuenta el nmero de pasos del compilador. Si el compilador va a realizar dos o ms pasos, no ser necesario: en el primer paso, el analizador sintctico construir el rbol de anlisis; en el segundo, el analizador semntico lo anotar. En las prximas secciones se ver que este esquema da lugar a la tcnica ms general del anlisis semntico. En cambio, si el compilador es de slo un paso, resulta necesario conseguir que los dos rdenes sean compatibles, ya que, en otro caso, sera imposible realizar el anlisis semntico. Para contestar a la tercera pregunta, es preciso definir algn concepto auxiliar adicional.

5.3.3.

Tipos interesantes de gramticas de atributos

Esta seccin introduce algunos subconjuntos interesantes de las gramticas de atributos: Gramticas de atributos con atributos sintetizados. Son aquellas en las que todos los atributos son sintetizados. Las gramticas de atributos A5_4, A5_8 y A5_10, de los ejemplos con el mismo nmero, son de este tipo. Es interesante sealar el caso de la gramtica A5_10, en la que se utiliza tambin informacin global. Gramticas de atributos que dependen nicamente de su izquierda. Son aquellas en las que todos sus atributos son o bien sintetizados o bien heredados de sus padres o de smbolos que aparecen en la parte derecha de la regla a la izquierda del smbolo estudiado. Las gramticas de atributos A5_5, A5_6 y A5_9, de los ejemplos con el mismo nmero, son de este tipo. Es interesante sealar el caso de la gramtica A5_9, ya que sus atributos dependen slo de su izquierda, y es equivalente a la gramtica A5_10, que slo tiene atributos sintetizados e informacin global. Cabe ahora plantearse la siguiente cuestin: existen gramticas de atributos que no pertenezcan a alguno de los dos tipos anteriores? La respuesta a esta pregunta es afirmativa. El hecho de que todos los ejemplos de este captulo puedan incluirse en alguna de las dos categoras anteriores es puramente casual. Ejemplo La siguiente gramtica proporciona un ejemplo en el que se hereda de smbolos a la derecha del 5.11 estudiado. Es una variante del Ejemplo 5.3, en el que la especificacin del tipo se escribe a la derecha de la lista de identificadores. Sus reglas de produccin son las siguientes (se ha resaltado la regla modificada): D::=LT T::=int T::=real L::=L,i L::=i Si se mantiene la semntica del Ejemplo 5.5, es fcil comprobar que el atributo tipo de L depende del atributo tipo de T que est a su derecha. Lo extrao de este tipo de construcciones no debe sugerir que no sean posibles, sino constatar la naturalidad con la que se ha incorporado

222

Compiladores e intrpretes: teora y prctica

E E v:7 t:i v:35 t:i

E v:5 t:i

v:7 t:i a)

v:3 t:i

v:4 t:i

v:3 t:i (

c v:4 t:i + 4 ) *

c v:5 t:i 5

E E v:7 t:i v:35 t:i

E v:5 t:i

v:7 t:i

v:3 t:i

b) E E v:4 t:i

v:3 t:i (

c v:4 t:i + 4 ) *

c v:5 t:i 5

Figura 5.13. Comparacin de los rdenes de recorrido del rbol por los analizadores sintcticos y por la propagacin de atributos sintetizados.

Captulo 5. Anlisis semntico

223

a nuestra intuicin el diseo de lenguajes cuyas gramticas resultan ms adecuadas para la construccin de compiladores e intrpretes. Las gramticas de atributos que dependen de su izquierda y las gramticas de atributos sintetizados pueden representar las construcciones de todos los lenguajes de programacin de alto nivel con los que se suele trabajar. Los Ejemplos 5.9 y 5.10 muestran cmo los atributos heredados de la gramtica A5_9 pueden sustituirse por el uso de informacin global. Algunos autores distinguen entre gramticas de atributos y definiciones dirigidas por la sintaxis, de forma que las primeras son un subconjunto de las segundas que excluyen efectos laterales, es decir, manipulacin de informacin global. De mantener esa categora, lo que en este captulo se llama gramtica de atributos sera lo que otros autores llaman definicin dirigida por la sintaxis. Como se ha indicado en la nota 1, se ha decidido unificar estos conceptos en el de gramtica de atributos, para ofrecer una visin ms compacta y actual. El inters de estos tipos de gramticas de atributos no se limita a la discusin de su potencia expresiva. Son de importancia crucial para la comunicacin entre los analizadores sintcticos y semnticos. Gramticas de atributos que dependen de su izquierda y analizadores descendentes: Es fcil comprobar que el orden inducido por el recorrido de los dos analizadores es compatible en las gramticas de este tipo. En este sentido, el ejemplo de la Figura 5.12.a) puede generalizarse. La razn es clara: el analizador descendente visita primero los nodos padre y luego los hermanos, de izquierda a derecha, y se es precisamente el orden necesario para la propagacin de los atributos de las gramticas con dependencia de su izquierda. La sntesis de atributos tambin es compatible con los analizadores descendentes gracias al mecanismo de vuelta atrs. La Figura 5.13.a) muestra grficamente un ejemplo. El recorrido de los nodos es el siguiente: E, E, (, E, E, E, c, 3, c, E, E, +, E, E, c, 4, c, E, E, E, ), E, E, *, E, E, c, 5, c, E, E. La sntesis de los valores y tipos de las expresiones se puede completar cuando, al volver atrs, se visita de nuevo las partes izquierdas de las reglas. Gramticas con atributos sintetizados y analizadores ascendentes: Es fcil comprobar que el orden inducido por el recorrido de los analizadores ascendentes y de las gramticas con atributos sintetizados es compatible. La Figura 5.13.b) muestra grficamente un ejemplo. La razn es que los analizadores ascendentes desplazan la entrada hasta encontrar un asidero, momento en el que se reduce la regla para continuar el anlisis, buscando la siguiente reduccin posible. La sntesis de los atributos slo puede realizarse cuando se tiene seguridad de haber procesado la parte derecha completa de la regla, por lo que la reduccin es el momento instante ideal para la propagacin.

5.3.4. Tcnica general del anlisis semntico en compiladores de dos o ms pasos


En general, puede presentarse el caso de disponer de una gramtica de atributos que no se desea modificar, junto con un analizador sintctico cuyo orden de anlisis sea incompatible con el de

224

Compiladores e intrpretes: teora y prctica

aqulla. En tal caso, es de inters disponer de una tcnica general para realizar el anlisis semntico. Dicha tcnica exige un compilador de dos o ms pasos y puede resumirse en el siguiente esquema: 1. Construir el rbol del anlisis sintctico. 2. Determinar las dependencias entre los atributos mediante el estudio de las acciones semnticas de la gramtica. 3. Determinar un orden entre los atributos del rbol, compatible con las dependencias obtenidas en el paso anterior. 4. Establecer un recorrido del rbol compatible con el orden del paso 3. 5. La ejecucin de las acciones semnticas que aparecen al recorrer el rbol segn indica el paso 4 completa el anlisis semntico del programa compilado. Sin embargo, todos los ejemplos de este captulo y la mayora de los problemas reales pueden solucionarse sin necesidad de utilizar esta tcnica general. En los prximos prrafos se ver que la tcnica general transforma algunos aspectos del anlisis semntico en la solucin de un problema clsico de lgebra: la construccin de un grafo que representa una relacin y la determinacin de un recorrido sobre el grafo, compatible con la relacin y que visite todos los nodos. Determinacin de las dependencias entre los atributos: En los casos ms sencillos, puede hacerse por simple inspeccin visual, como se hizo en los ejemplos de las Figuras 5.5, 5.6 y 5.7. Tambin se puede utilizar, como tcnica general, un grafo de dependencias. Para ello hay que tener en cuenta las siguientes consideraciones: Las instrucciones de las acciones semnticas pueden representarse de la siguiente manera, que tiene en cuenta nicamente la propagacin de los atributos: b= f(c1,...,cn) donde tanto b como ci,,i{1,..,n} son atributos, y f representa el clculo mediante el cual, a partir de los valores de c1,...,cn, se obtiene el de b. En este caso, se dir que el atributo b depende de los atributos c1,...,cn. Los efectos laterales que pueden modificar la informacin global de la gramtica pueden representarse de la misma manera: g(K,c1,...,cn) donde K es la informacin global. Antes de aplicar el algoritmo de creacin del grafo, se crea un nuevo atributo ficticio (a) para que la expresin anterior se transforme en la siguiente: a=g(K,c1,...,cn) que se trata como cualquier otra instruccin. La Figura 5.14 muestra el pseudocdigo de un algoritmo para la construccin del grafo de dependencias.

Captulo 5. Anlisis semntico

225

grafo ConstruirGrafoDependencias (arbol as, gramatica_atributos ga) { nodo n; atributo_semntico a; accion_semantica acc; grafo gd = vaco; `Recorrer cada nodo (n) del rbol sintctico as `Recorrer cada atributo (a) del smbolo de n AadirNodo (nuevo_nodo(a), gd); `Recorrer cada nodo (n) del rbol sintctico as `Recorrer acciones (acc=b:=f(c1,...,ck)) de n `Para i de 1 a k AadirArco (nuevo_arco(ci,b),gd); }
Figura 5.14. Pseudocdigo para la construccin del grafo de dependencias entre los atributos de una gramtica.

Determinacin de un orden compatible con las dependencias: Lo ms frecuente es que esto pueda hacerse directamente sobre el rbol de anlisis. Las Figuras 5.15, 5.16 y 5.17 muestran un orden posible para los rboles de las Figuras 5.5, 5.6 y 5.7.

E E v:7 t:i

9
v:35 t:i E v:5 t:i

6
v:7 t:i

5
v:4 t:i

2
v:3 t:i E E

v:3 t:i (

v:4 t:i

v:5 t:i

Figura 5.15. Un orden compatible con las dependencias entre atributos del rbol de la Figura 5.5.

226

Compiladores e intrpretes: teora y prctica

D t:i T t:i

L t:i

1
t:i L

3 4

t:i L

v:0 t:i i

v:0 t:i

v:0 t:i

6
int var1 , x

7
, y

Figura 5.16. Un orden compatible con las dependencias entre atributos del rbol de la Figura 5.6.

P D t:i T t:i A

2
t:i L

TS y(int)

t:i

19

x(int) var1(int)

E v:7 t:i

v:35 t:i

15 11
v:3 t:iE E v:7 t:i

18

E v:5 t:i

17

t:i L t:i L

14
E

v:4 t:i13 cv:5 t:i

v:0 i t:i

i v:0 t:i

i v:0 t:i

v:0 i t:i x

v:3 c t:i

c v:4 t:i

6
int var1 x

7
, y

9=

10
( 3 4

12
) * 5

16

Figura 5.17. Un orden compatible con las dependencias entre atributos del rbol de la Figura 5.7.

Captulo 5. Anlisis semntico

227

Dependencias circulares: Se dice que una gramtica tiene dependencias circulares cuando existen al menos dos atributos b y c, e instrucciones en las acciones semnticas con la siguiente estructura (el orden que ocupan los atributos como argumentos de las funciones es irrelevante): b= fb(c,d1,...,dn) c= fc(b,a1,...,am) Esto significa que es posible que exista un rbol en el que aparezca un nodo etiquetado con el smbolo b y otro con el smbolo c, tal que, de acuerdo con la primera instruccin, b tenga que ser analizado antes que c, y de acuerdo con la segunda tenga que seguirse el orden inverso. Las dependencias circulares presentan una dificultad insalvable para el anlisis semntico. La nica solucin es considerar que las gramticas con dependencias circulares estn mal diseadas, y refinarlas hasta que se elimine el problema. Si las gramticas de atributos se consideran como un nuevo lenguaje de programacin, este error de diseo sera similar al de programar, con un lenguaje de programacin imperativo, un bucle o una funcin recursiva sin condicin de salida, lo que dara lugar a una ejecucin permanente.

5.3.5. Evaluacin de los atributos por los analizadores semnticos en los compiladores de slo un paso
Se ha dicho anteriormente que en el diseo de compiladores de un solo paso es necesario compatibilizar el orden de recorrido inducido por el analizador sintctico utilizado con el que precisa la relacin de dependencia entre los atributos. Tambin se ha dicho que las gramticas de atributos que dependen de su izquierda aseguran la compatibilidad con los analizadores descendentes, mientras que las que slo tienen atributos sintetizados aseguran la compatibilidad con los analizadores ascendentes. Para completar el anlisis semntico en este caso, slo queda describir cmo se puede realizar la evaluacin de los atributos. De forma general, se pueden seguir las siguientes indicaciones: En los analizadores que utilizan tablas de anlisis (por ejemplo ascendentes) se pueden evaluar los atributos cuando se completa la parte derecha de las reglas (en el momento de su reduccin). En ese instante se sacan de la pila los smbolos asociados con la parte derecha de la regla (junto con su informacin semntica) y lo nico que hay que aadir al algoritmo es el clculo de la informacin semntica del smbolo no terminal de la parte izquierda, antes de ubicarlo en la posicin adecuada para continuar el anlisis. Este clculo es posible, ya que los atributos sintetizados slo necesitan la informacin semntica de los smbolos de la parte derecha, y esa informacin est disponible cuando se realiza la reduccin. Los analizadores que permiten ms libertad en la posicin de las acciones semnticas (vase la Seccin 5.2.4) pueden ejecutarlas a medida que las encuentran. Un ejemplo de esta situacin son los analizadores descendentes recursivos. La tcnica para su construccin, descrita en el Captulo 4, codificaba una funcin recursiva para cada smbolo no terminal de la gramtica. Lo nico que hay que aadir al algoritmo es la codificacin de las

228

Compiladores e intrpretes: teora y prctica

rutinas semnticas, e invocarlas en la posicin que ocupen en la parte derecha de la regla. La correccin del diseo de la gramtica asegurar que se dispone de los valores de todos los atributos necesarios para la ejecucin correcta de la accin semntica. En el desarrollo de cada compilador concreto, siempre es posible extender este modelo, aunque no sea mediante el uso de una tcnica general. Todos los analizadores semnticos utilizan una pila semntica para guardar los valores de los atributos de los smbolos. Las tcnicas generales proponen un tratamiento estndar de la pila mediante las funciones push y pop. En el desarrollo de un compilador concreto, sera posible consultar la pila semntica de una manera ms flexible, ya que se trata de una estructura propia del analizador. En ese caso, se podra calcular un conjunto ms amplio de atributos: todos los que dependan de los valores de los atributos que se encuentran en la pila en un momento dado.

Gramticas de atributos para el anlisis semntico 5.4 de los lenguajes de programacin


El objetivo de esta seccin es sugerir, de la manera ms genrica posible, cmo se pueden solucionar los problemas relacionados con las construcciones ms frecuentes que se utilizan en los lenguajes de programacin de alto nivel. Se supondr que se est especificando una gramtica de atributos sintetizados con informacin global, pues este tipo de gramticas es compatible con los compiladores de un solo paso que utilicen analizadores sintcticos, tanto ascendentes como descendentes. El lector ser capaz de solucionar otros problemas concretos adaptando el contenido de esta seccin mediante el uso de las nociones de la Seccin 5.2.5. A continuacin se recuerdan algunos de los aspectos ms generales de los lenguajes de programacin de alto nivel: El tipo de dato apuntador o, ms popularmente, puntero. Algunos lenguajes de programacin permiten declarar un tipo de dato que apunta a objetos de otros tipos. Su peculiaridad principal es que slo ocupa la memoria precisa para contener la direccin a la que apunta, y no depende del tamao del objeto apuntado. Su existencia permite modificar los datos apuntados mediante uno cualquiera de los punteros que apunten a ellos. El siguiente fragmento de cdigo C muestra un ejemplo: Ejemplo int * p_int; 5.12 int *** p_p_p_int; int a; a = 5; p_int = &a; /* Se imprime 5, el valor de a y de *p_int */ printf(%d\n, *p_int); (*p_int)++;

Captulo 5. Anlisis semntico

229

/* Se imprime 6, el valor de a y de *p_int */ printf(%d\n, a); En la primera instruccin se declara un apuntador a un dato de tipo entero. La segunda demuestra la posibilidad de anidar niveles mltiples de punteros. En la quinta instruccin, p_int pasa a apuntar al identificador a (&a es la direccin del identificador a), de forma que el valor apuntado por el puntero (*p_int) es el mismo que el de la variable a, como indica la sexta instruccin, y se puede modificar el valor de a mediante p_int, como demuestran las dos ltimas instrucciones. El tipo de dato apuntador abre la discusin sobre dos formas de gestionar la memoria: la memoria esttica y la memoria dinmica. En la Seccin 10.1 se encontrar una descripcin detallada de este tema. Los procedimientos, funciones o subrutinas presentan dificultades, tanto en su declaracin como en su invocacin. Entre el analizador semntico y el generador de cdigo, se tienen que gestionar la memoria asignada a las variables automticas (las variables locales de los procedimientos) y el convenio de llamadas utilizado: el mecanismo mediante el que el programa que invoca comunica al programa invocado el valor de sus argumentos y la manera en la que el procedimiento o funcin devuelve el control al programa que la invoca, as como el valor de retorno, si existe. En la Seccin 10.1 se encontrar una descripcin completa de estos aspectos.

5.4.1. Algunas observaciones sobre la informacin semntica necesaria para el anlisis de los lenguajes de programacin de alto nivel
Para la gestin de los lenguajes de alto nivel es necesario tener en cuenta cierta informacin, que podra organizarse de la siguiente manera: Atributos semnticos asociados a los operandos: Se llama operando a los elementos de un programa asociados a un valor, como variables, funciones, etiquetas, etc. Estos datos deben llevar la siguiente informacin semntica asociada: Su nombre: el identificador por el que se los reconoce. Su tipo: vase, por ejemplo, la Tabla 6.2, en el captulo siguiente, dedicado a la generacin de cdigo. Su direccin: para una variable, puede ser una posicin en la memoria, un puntero a la tabla de smbolos, o el nombre de un registro. En variables de tipo array indexadas (como en la expresin v[3]del lenguaje C) es preciso especificar dos valores: el nombre de la variable y el desplazamiento necesario para localizar el elemento concreto. En el caso de los punteros, puede que baste con especificar su nombre (como en la instruccin *p del lenguaje C) o que se necesite tambin un desplazamiento u offset, como en *(p+4).

230

Compiladores e intrpretes: teora y prctica

Su nmero de referencias: si el lenguaje permite utilizar el tipo de dato apuntador, puede ser necesario anotar su nivel de anidamiento. Informacin global: Los mecanismos mencionados a continuacin afectan a atributos que suelen ser heredados, o almacenados en informacin global. Tabla de smbolos. Tiene que ser accesible desde todas las reglas de la gramtica. Desde algunas, ser actualizada para insertar nuevos identificadores o modificar la informacin que se conoce sobre ellos. Desde otras, ser consultada para comprobar la correccin del programa. Lista de registros usados. Se utiliza en la generacin de cdigo y se refiere a los atributos relacionados con la direccin donde est almacenado un objeto, para el caso de los que ocupan registros. Para asegurar la correccin del programa objeto resultado de la compilacin, el analizador semntico tiene que proporcionar mecanismos para que los registros no sean modificados por error, cuando su informacin todava es til. Informacin para la gestin de etiquetas. Como se explicar en la Seccin 6.1, en el captulo sobre generacin de cdigo, la estructura bsica de control de flujo en los lenguajes simblicos (ensambladores) y de la mquina es el salto, tanto condicional como incondicional, a una direccin o etiqueta, situada en el espacio de instrucciones. Utilizando nicamente saltos a etiquetas, un compilador que genere cdigo simblico o de mquina tiene que generar cdigo equivalente a las estructuras de control del flujo de programa que se utilizan en los lenguajes de alto nivel (instrucciones condicionales, bucles, etc.). El hecho de que las etiquetas tengan que tener un nombre nico dentro del cdigo, junto con la posibilidad de mezclar estructuras o estructuras anidadas en el programa fuente (como instrucciones del tipo if-then-else dentro de otras estructuras ifthen-else), obliga a articular mecanismos para controlar que las etiquetas sean distintas y que los saltos que conducen a ellas sean coherentes. Informacin para la gestin del tipo de los identificadores. En los lenguajes de programacin se dan dos circunstancias frecuentes que suelen requerir atributos heredados o informacin global para su representacin con gramticas de atributos. La primera es la gestin del tipo de los identificadores en las instrucciones en las que se puede declarar una lista de variables del mismo tipo en una sola instruccin. La segunda es la declaracin de los argumentos de las funciones. Las prximas secciones resumen de manera intuitiva la gestin semntica asociada con las construcciones ms frecuentes de los lenguajes de programacin. Se puede encontrar un ejemplo completo de la aplicacin de estas ideas a la construccin de un compilador para un lenguaje sencillo en http://www.librosite.net/pulido

5.4.2. Declaracin de identificadores


La mayora de los lenguajes de programacin proporcionan una sintaxis para la declaracin de identificadores, semejante a la siguiente regla de produccin: <declaracion> ::= <tipo> <identificador>

Captulo 5. Anlisis semntico

231

Algunos permiten especificar una lista de identificadores en lugar de uno solo. El analizador semntico tendr que encargase de las siguientes tareas: Tras procesar el smbolo no terminal <tipo>, el analizador morfolgico debe proporcionar, como valor de su atributo, el tipo de dato que se est declarando. Esta informacin tendr que propagarse, modificando tal vez de forma adecuada la informacin global correspondiente, para que est accesible ms tarde, cuando se haya procesado el nombre del identificador. Tras procesar el smbolo no terminal <identificador>, debe consultarse la tabla de smbolos para comprobar que no se ha declarado previamente la misma variable, recuperar el tipo de la declaracin, e insertar el identificador nuevo en la tabla de smbolos. Los distintos lenguajes de programacin facilitan diversos tipos de datos (arrays, apuntadores), junto con las condiciones que tienen que satisfacer para su declaracin correcta. La accin semntica de esta regla debe gestionar toda la informacin necesaria para cumplir esas condiciones.

5.4.3. Expresiones aritmticas


Las expresiones aritmticas son, posiblemente, la parte del lenguaje que resulta ms compleja para el anlisis semntico y la generacin de cdigo. El analizador semntico tiene que asegurar que se satisfacen las restricciones de tipo de los operandos que aparecen en las expresiones. Esto supone aplicar las normas de transformacin de tipos compatibles (enteros de distinto tamao, reales de distinta precisin, transformaciones entre los tipos entero y real, etc.). Es tarea del generador obtener un programa objeto que evale correctamente las expresiones. La Seccin 6.1.2 explica un procedimiento general para la gestin de estos dos aspectos. Este tipo de generacin requiere que el analizador semntico mantenga y actualice la informacin relativa al tipo, nmero de referencias y direccin de almacenamiento de los operandos, as como la lista de registros disponibles. Una de las dificultades para la generacin de cdigo para calcular expresiones es la determinacin del lugar donde se almacenarn los resultados intermedios. El sistema ofrece un conjunto limitado de registros. El analizador semntico tendr que comprobar si quedan registros disponibles, y en caso contrario localizar espacio en la pila o en la memoria esttica. Una vez asignada memoria a un resultado intermedio, tendr que actualizar la informacin semntica correspondiente para continuar el anlisis adecuadamente.

5.4.4. Asignacin de valor a los identificadores


La mayora de los lenguajes de programacin proporcionan una sintaxis para la asignacin de valor a los identificadores, semejante a la siguiente regla de produccin: <asignacion> ::= <identificador> <expresion>

232

Compiladores e intrpretes: teora y prctica

El analizador semntico tendr que encargase de las siguientes tareas: El analizador morfolgico propaga la informacin semntica del nombre del identificador. Tras procesar el smbolo <identificador>, hay que consultar la tabla de smbolos para comprobar que ya ha sido declarado y recuperar su informacin semntica asociada, que incluye su tipo. Tras procesar el smbolo no terminal <expresion>, habr que comprobar la compatibilidad entre los tipos de la expresin y el identificador, para que la asignacin sea correcta.

5.4.5. Instrucciones condicionales


La mayora de los lenguajes de programacin utilizan la estructura de instrucciones condicionales indicada por la siguiente regla de produccin: <condicional>::=if <expresion> then <instruccion> Como se ha mencionado anteriormente, al generar cdigo en lenguaje simblico, hay que construir un fragmento de programa objeto equivalente con saltos a etiquetas, lo que obliga a que el analizador semntico gestione las etiquetas de manera adecuada. A continuacin se muestra el esquema de un fragmento de cdigo simblico equivalente a esta instruccin condicional: ; Cdigo de la expresin (el valor est en el registro EAX) CMP EAX, 0 JE FIN_THEN ; Cdigo de la instruccin de la rama ENTONCES FIN_THEN: Bastar con que la instruccin de la rama then contenga otra instruccin condicional para que las dos etiquetas FIN_THEN colisionen, originando un error en el programa. Este problema puede solucionarse fcilmente si se aade a la etiqueta algn carcter que la haga nica. Esta solucin obliga al analizador semntico a conservar la informacin necesaria para que cada estructura tenga accesible la etiqueta adecuada. Tambin tendra que asegurar que la expresin que se comprueba en la condicin es del tipo permitido por las especificaciones del lenguaje de programacin (entero, booleano, etc.). La mayora de los lenguajes de programacin facilitan instrucciones ms potentes, como el ifthen-else, que se tratar de manera anloga: <condicional>::=if <expresion> then <instruccion> else <instruccion>

Captulo 5. Anlisis semntico

233

5.4.6. Instrucciones iterativas (bucles)


Casi todos los lenguajes de programacin proporcionan instrucciones iterativas con una sintaxis parecida a la de la siguiente regla de produccin: <bucle> ::= while <expresion> do <instruccion> Estas instrucciones presentan las mismas peculiaridades que las condicionales, tanto en lo relativo a las etiquetas, como en lo referente a las comprobaciones de tipo de la condicin.

5.4.7. Procedimientos
La mayora de los lenguajes de programacin permiten declarar funciones y subrutinas con una sintaxis parecida a la de la siguiente regla de produccin: <subrutina>::=<tipo><identificador>(<lista_argumentos>) <declaracion> <instruccion> El analizador semntico se encargar de realizar las siguientes comprobaciones: El tipo y el identificador se tratan de manera anloga a la de la declaracin de variables, excepto que se tiene que indicar en la tabla de smbolos que el identificador representa una funcin. La declaracin de la lista de argumentos implica actualizar la informacin que se conserva al respecto, que tiene que ser accesible en el momento de la invocacin del procedimiento. Lo ms frecuente es incluirla en la tabla de smbolos. La mayora de los lenguajes de programacin realizan llamadas o invocaciones a los procedimientos con una sintaxis similar a la de la siguiente regla de produccin: <expresion>::=<identificador>(<lista_expresiones>) El analizador semntico debe realizar las siguientes tareas: Tras procesar el smbolo <identificador>, cuyo nombre debe propagarse, hay que comprobar en la tabla de smbolos que se ha declarado un procedimiento con ese nombre, y recuperar la informacin que describe la lista de sus argumentos. Tras procesar el parntesis de cierre, se podr completar la verificacin de la correspondencia del nmero, tipo y orden de los argumentos de la invocacin y los de la declaracin.

Algunas herramientas para la generacin 5.5 de analizadores semnticos


En la actualidad se encuentran disponibles en Internet, de forma gratuita, diversas herramientas que generan analizadores sintcticos a partir de gramticas en BNF que cumplan ciertas condiciones, y que a veces pueden generar tambin el analizador semntico, si se les proporciona una

234

Compiladores e intrpretes: teora y prctica

gramtica de atributos. Una de las ms populares y conocidas es yacc (yet another compiler compiler), objetivo de esta seccin. Yacc est incluida en las distribuciones estndares de Unix y Linux. Otra herramienta, compatible con yacc, es Bison, que est disponible tanto para Windows como para Linux. Aunque lo que se diga en esta seccin es aplicable tanto a yacc como a Bison, por motivos histricos slo se har referencia a yacc. La aplicacin yacc toma como entrada un fichero, que contiene una gramtica con atributos sintetizados e informacin global, y genera una aplicacin escrita en el lenguaje de programacin C, que implementa un analizador sintctico ascendente LALR(1), junto con el correspondiente analizador semntico, aunque excluye el analizador morfolgico, que debe obtenerse de manera independiente. Esta seccin no tiene por objeto proporcionar un manual exhaustivo de la herramienta, sino sugerir indicaciones prcticas para comprobar el funcionamiento de algunas de las gramticas de atributos utilizadas en el captulo y para que, posteriormente, el lector pueda beneficiarse de su ayuda en el diseo de las mismas. Puede encontrarse documentacin ms detallada sobre yacc, tanto en Internet (http://www.librosite.net/pulido), como en la bibliografa especializada [1]. El resto de la seccin explicar la estructura bsica del fichero fuente de yacc, las directivas que se utilizan para describir la gramtica de atributos de entrada, la notacin con la que deben escribirse las reglas y las acciones semnticas, as como algunas consideraciones prcticas fundamentales para trabajar con yacc. Estos conceptos se ilustrarn mediante la solucin con yacc de los ejemplos A5_8 y A5_10 de gramticas de atributos.

5.5.1. Estructura del fichero fuente de yacc


El fichero de entrada para yacc tiene tres secciones separadas por una lnea, que slo contiene los caracteres %%, sin espacios a la izquierda. Las tres secciones son: La seccin de definiciones. La seccin de reglas. La seccin de cdigo de usuario. La Figura 5.18 muestra un esquema del fichero de entrada de yacc.

Seccin de definiciones: %{ /* delimitadores de cdigo C */ %} %% Seccin de reglas: %% Seccin de funciones de usuario


Figura 5.18. Estructura del fichero de entrada para yacc.

Captulo 5. Anlisis semntico

235

5.5.2. Seccin de definiciones


En esta seccin se incluyen las definiciones propias de yacc y las declaraciones escritas en lenguaje C que necesite el analizador semntico. Estas ltimas deben separarse de las primeras, delimitndolas mediante dos lneas que slo contienen, respectivamente, los caracteres %{ y %}, sin espacios a la izquierda (vase la Figura 5.18). Entre estas dos lneas se escribirn las instrucciones declarativas necesarias, escritas en C. En la parte de declaraciones propias de yacc se especifican aspectos generales, mediante el uso de directivas. Directiva %union: permite declarar los atributos que se van a asociar con los smbolos de la gramtica. Su efecto es equivalente a declarar una estructura de datos de tipo union en el lenguaje C. Directiva %type: asocia los atributos especificados mediante la directiva %union con los smbolos no terminales de la gramtica. Por lo tanto, tiene que haber una directiva %type por cada smbolo que posea atributos semnticos. Directiva %token: define los smbolos terminales de la gramtica que no estn representados literalmente. Puede utilizarse tambin para indicar los atributos semnticos asociados a los smbolos terminales, con la misma notacin que la directiva anterior. Directiva %start: especifica el axioma de la gramtica. Es opcional; si no se utiliza, el axioma ser la parte izquierda de la regla que aparece en primer lugar en el fichero de entrada de yacc. Ejemplo A lo largo de esta seccin se va a solucionar con yacc la gramtica de atributos A5_8. A ello se 5.13 dedicar el Ejemplo 5.12, utilizndose siempre el mismo nmero de ejemplo en los pasos sucesivos de la escritura del fichero fuente yacc para dicha gramtica. En este ejemplo, en las acciones semnticas y en otras funciones de usuario, se escribirn mensajes por la salida estndar mediante la instruccin fprintf(stdout, ); Para poder utilizar la funcin fprintf, es preciso incluir el archivo de definiciones stdio.h. Esto tiene que hacerse en la seccin de declaraciones, en la parte reservada a las instrucciones en lenguaje C. Por tanto, el fichero fuente para este ejemplo debe contener la siguiente seccin de declaracin C: %{ #include <stdio.h> %}

236

Compiladores e intrpretes: teora y prctica

Por otra parte, todos los smbolos de la gramtica A5_8 utilizan el mismo atributo de tipo entero, llamado profundidad, por lo que la directiva %union necesaria es la que se muestra a continuacin: %union { int profundidad; } Para especificar que todos los smbolos no terminales de la gramtica A5_8 tienen el atributo profundidad, tienen que especificarse las siguientes instrucciones: %type <profundidad> lista %type <profundidad> lista_interna Esta gramtica no asigna atributos semnticos a los smbolos terminales, por lo que no se necesita la directiva %token. Por otra parte, el axioma es el smbolo no terminal lista. Se puede aadir la siguiente instruccin: %start lista Uniendo las componentes anteriores, la seccin de definiciones del archivo de entrada para la gramtica A5_8 ser: %{ #include <stdio.h> %} %union { int profundidad; } %type <profundidad> lista %type <profundidad> lista_interna %start lista Ejemplo Como segundo ejemplo, se preparar el fichero fuente yacc para la gramtica A5_10. En este 5.14 caso tambin se utilizar el archivo de definiciones stdio.h. Tambin se debe inicializar la informacin global de la gramtica. En este ejemplo, se utilizar una variable global de tipo entero para representar el nmero de elementos, que inicialmente tomar el valor 0. La seccin de declaraciones en lenguaje C ser: %{ #include <stdio.h> int num_elementos = 0; %} En la gramtica A5_10 los smbolos no tienen atributos, porque slo se utiliza informacin global. Por tanto, no se necesita la directiva %union. Por la misma razn, tampoco se precisa la di-

Captulo 5. Anlisis semntico

237

rectiva %type. Tampoco se asignan atributos semnticos a los smbolos terminales, por lo que no se utilizar la directiva %token. Finalmente, el axioma es el smbolo no terminal lista. Se puede utilizar la siguiente instruccin: %start lista Uniendo las componentes anteriores, la seccin de definiciones del archivo de entrada para la gramtica A5_10 ser: %{ #include <stdio.h> int num_elementos = 0; %} %start lista

5.5.3. Seccin de reglas


Sintaxis para las reglas de produccin independientes del contexto: Se utiliza una notacin parecida a la notacin BNF. Por ejemplo, la regla X::=Y1Y2...Yn se escribe de la siguiente forma: X : Y1 Y2...Yn donde los espacios alrededor del smbolo : son opcionales. Cuando en la regla aparecen smbolos terminales, pueden escribirse literalmente si se cumplen las siguientes condiciones: Su longitud es igual a 1, como en los smbolos de apertura y cierre de parntesis, ( y ). El analizador morfolgico los devuelve literalmente, mediante instrucciones del tipo return (; return ); Se utiliza la notacin del lenguaje C para representar caracteres. Por ejemplo: lista: ( lista_interna ) Es posible utilizar otros mecanismos de comunicacin entre los analizadores morfolgico y sintctico. Por ejemplo, se podra haber definido el smbolo INICIO_LISTA en lenguaje C, para representar el smbolo terminal (. Para ello, tendran que haberse realizado las siguientes acciones: El analizador semntico tiene que saber que existe un smbolo terminal llamado INICIO_LISTA. Eso se hace en la seccin de declaraciones, con la directiva %token, de la siguiente manera: %token INICIO_LISTA

238

Compiladores e intrpretes: teora y prctica

Gracias a esto, en el programa C generado por yacc se definir un smbolo con este nombre. Cuando encuentre el carcter (, el analizador morfolgico tiene que devolver al analizador semntico este smbolo, por ejemplo, as: if((c=fgetc(stdin))==()return INICIO_LISTA; En las reglas de produccin se utilizar el nombre del smbolo en los lugares donde apareca el smbolo terminal (. lista: INICIO_LISTA lista_interna ) Para especificar una regla-, es suficiente omitir la parte derecha de la regla. As, la regla lista_interna::= se escribira en yacc as: lista_interna: Las reglas deben separarse mediante el smbolo ;. Por ejemplo: lista_interna: lista_interna: lista_interna ( lista_interna ) ; ;

Cuando varias reglas comparten la misma parte izquierda, puede utilizarse el smbolo | para separar sus partes derechas, como en la notacin BNF. El ejemplo anterior podra escribirse tambin de la siguiente manera: lista_interna: | ; lista_interna ( lista_interna ) ;

Sintaxis para las acciones semnticas: Las acciones semnticas asociadas a una regla se escriben a continuacin de sta encerradas entre llaves. En el caso de que una regla no tenga ninguna accin semntica asociada, debe escribirse {} Dentro de la accin semntica se escriben instrucciones en el lenguaje C. En ellas, pueden aparecer los smbolos de la tabla 5.1, que tienen significado especial para yacc, y que permiten acceder a la informacin semntica de la gramtica. Se supone, en la tabla, que se est describiendo la regla X : Y1 Y2...Yn).

Tabla 5.1. Smbolos que se pueden utilizar en las acciones semnticas asociadas a las reglas yacc. Smbolo $$ $1 $n Significado Valor semntico de X Valor semntico de Y1 Valor semntico de Yn (n tiene que ser un nmero)

Captulo 5. Anlisis semntico

239

Ejemplo A continuacin se muestra la seccin de reglas yacc para la gramtica A5_8. 5.15 lista: ( lista_interna ) { $$ = $2 + 1; fprintf(stdout, PROF. TOTAL= %d\n, $$); }; lista_interna: lista_interna ( lista_interna ) { if ( $1 > $3 ) $$ = $1 ; else $$ = $3+1 ; }; lista_interna: { $$ = 0; }; Ejemplo A continuacin se muestra la seccin de reglas yacc para la gramtica A5_10. 5.16 lista: ( lista_interna ) { num_elementos ++; fprintf(stdout, NUM. LISTAS= %d\n, num_elementos ); }; lista_interna: lista_interna ( lista_interna ) { num_elementos++; }; lista_interna: { };

5.5.4. Seccin de funciones de usuario


En esta seccin, el programador debe incluir las funciones escritas en lenguaje C que considere oportunas y que puedan utilizarse en las acciones semnticas. Entre ellas, las ms significativas son las siguientes: La funcin int main(): Es el programa principal y debe invocar a una funcin llamada yyparse, que realiza el anlisis semntico completo. La funcin int yylex(): Yacc supone que esta funcin realiza el anlisis morfolgico. Slo hay que tener en cuenta que tiene que devolver el valor 0 cuando se localiza el final

240

Compiladores e intrpretes: teora y prctica

del fichero de entrada, y que las unidades sintcticas se representan mediante nmeros enteros. En la direccin de Internet http://www.librosite.net/pulido se dan indicaciones sobre el uso de unidades sintcticas de estructura ms compleja. Ejemplos En ambos casos, puede utilizarse el siguiente cdigo: 5.15 int main() y 5.16 { return( yyparse()); } int yylex() { int c; c=fgetc(stdin); while (( c != EOF ) && ( c != ( ) && ( c != ) ) ) c = fgetc(stdin); if ( c == EOF ) return 0; else return c; }

5.5.5. Conexin entre yacc y lex


Ya se ha explicado en la Seccin 3.9 que lex es una herramienta, similar a yacc, que genera automticamente analizadores morfolgicos. La funcin que genera lex para que se pueda invocar el analizador se llama int yylex(). La coincidencia de nombres no es casual, pues es muy frecuente utilizar lex para generar el analizador morfolgico empleado por yacc. En la direccin de Internet http://www.librosite.net/pulido est disponible una explicacin del uso de estas herramientas para la implementacin de un compilador completo.

5.6 Resumen
Tras el estudio de este captulo, el lector ser capaz de abordar el desarrollo de un analizador semntico y de incorporarlo al compilador o intrprete del lenguaje de programacin estudiado. Para ello, se describen previamente los objetivos generales del analizador semntico y su relacin con el resto de las componentes de un compilador o de un intrprete, tanto en los casos de anlisis en un paso como en los de dos o ms pasos.

Captulo 5. Anlisis semntico

241

Posteriormente se dedica una seccin a la descripcin detallada de la herramienta ms utilizada en el anlisis semntico: las gramticas de atributos. La seccin comienza con una presentacin informal, mediante ejemplos, de las extensiones que hay que aadir a las gramticas independientes para que puedan hacerse cargo del anlisis semntico. As surgen de forma natural los conceptos de atributo semntico y las diferentes formas de calcular sus valores, a saber, sntesis y herencia. Como consecuencia de esto se descubre la existencia de una relacin de dependencia entre los atributos, que sugiere un orden en el recorrido del rbol de anlisis para poder completar el proceso de anotacin semntica. Tras esta introduccin informal, se da una definicin formal de los conceptos presentados previamente, lo que completa la explicacin de las gramticas de atributos. A continuacin se analizan distintas tcnicas para el diseo de las gramticas de atributos para la solucin de problemas concretos. Mediante ejemplos de fcil comprensin, se resaltan las condiciones que tiene que cumplir un problema para que se pueda resolver con cada uno de los tipos de atributos estudiados: sintetizados y heredados. Se indica cmo se pueden sustituir los atributos heredados por informacin global a la gramtica. Aunque los ejemplos son casos particulares sencillos, las conclusiones se pueden generalizar. Tras explicar qu son y cmo funcionan las gramticas de atributos, se dedican dos secciones a su uso en el anlisis semntico de los lenguajes de programacin. La primera describe cmo se conecta una gramtica de atributos con los analizadores sintcticos para completar el analizador semntico, tanto en el caso de los analizadores ascendentes como en el de los descendentes. La segunda seccin analiza las dificultades asociadas a las construcciones ms frecuentes de los lenguajes de programacin de alto nivel, y cmo se solucionan mediante una gramtica de atributos. La ltima seccin del captulo describe yacc, una herramienta de libre distribucin que genera automticamente analizadores sintcticos y semnticos a partir de la descripcin de su gramtica de atributos. El objetivo de esa seccin es dotar al lector de una herramienta que ayuda a comprobar la correccin de las gramticas de atributos.

5.7 Bibliografa
[1] Levine, J. R.; Mason, T., y Brown, D.: Lex & yacc. Unix Programming Tools, OReilly & Associates, Inc., 1995.

5.8 Ejercicios
1. Construir una gramtica de atributos que represente el lenguaje de los nmeros en punto flotante del tipo [-][cifras][.[cifras]][e[-][cifras]]. Debe haber al menos una cifra en la parte entera o en la parte decimal, as como en el exponente, si lo hay. La gramtica de atributos tiene que ser capaz de calcular el valor del nmero.

242

Compiladores e intrpretes: teora y prctica

2.

Construir una gramtica de atributos que represente el lenguaje de las cadenas de caracteres correctas en el lenguaje de programacin C. La gramtica de atributos tienen que ser capaz de almacenar en una variable auxiliar global la cadena procesada. Disear una gramtica de atributos para expresiones aritmticas en las que los operadores son la divisin (/), la suma (+) y el producto (*). Los operandos pueden ser letras del alfabeto. La gramtica de atributos tiene que gestionar el tipo de las expresiones. Para ello aplicar las siguientes reglas: Las letras del alfabeto se supone que representan variables declaradas como enteras. Las sumas y productos tienen el mismo tipo que sus operandos; en el caso de mezclar enteros y reales, la expresin completa ser de tipo real. La divisin genera una expresin de tipo real independientemente del tipo de sus operandos. Construir el rbol de propagacin de atributos en el anl0isis semntico de la siguiente expresin: a/(b+c*d)

3.

Captulo

Generacin de cdigo
El mdulo de generacin de cdigo de un compilador tiene por objeto generar el cdigo equivalente al programa fuente escrito en un lenguaje diferente. En funcin del tipo de lenguaje objetivo, se distinguen distintos tipos de compiladores: Cross-compilers (compiladores cruzados): traducen de un lenguaje de alto nivel a otro lenguaje de alto nivel. Compiladores que generan cdigo en lenguaje simblico: generan un cdigo intermedio que despus deber ser procesado por un ensamblador. Compiladores que generan cdigo en lenguaje de la mquina: generan directamente programas ejecutables (*.EXE) o bien (esto es mucho ms frecuente) en un formato especial de cdigo mquina (*.OBJ) que contiene informacin adicional, y que despus ser procesado por un programa enlazador (linker), que generar el programa ejecutable a partir de uno o ms programas en formato OBJ, algunos de los cuales pueden estar contenidos en bibliotecas (libraries) que suelen proporcionarse junto con el compilador, y que contienen funciones y subrutinas prefabricadas de uso general. En este captulo se va a suponer que el compilador genera cdigo simblico (ensamblador), pues los ejemplos resultan mucho ms legibles, pero todo lo que se diga podr aplicarse a cualquier otro tipo de compilador. En los ejemplos se supondr que el cdigo generado es comprensible por un ensamblador tpico aplicable a la familia 80x86 a partir del microprocesador 80386, en modo de funcionamiento de 32 bits (vase la Seccin 10.1).

Generacin directa de cdigo ensamblador 6.1 en un solo paso


Una instruccin del ensamblador genrico para el 80x86 que vamos a utilizar tiene la siguiente sintaxis: etiqueta: cdigo_instruccin operandos

244

Compiladores e intrpretes: teora y prctica

Los operandos de las instrucciones pueden ser registros, direcciones, constantes o expresiones. Existen distintos tipos de registros. En los ejemplos de esta seccin se utilizarn los siguientes registros de 32 bits: EAX (acumulador), EBX, ECX, EDX, ESP (puntero a la pila), EBP, ESI y EDI. La direccin donde se almacena el valor de una variable se representar anteponiendo el smbolo de subrayado (_) al nombre de la variable. Para referirse al contenido de una posicin de memoria, es necesario encerrar su direccin entre corchetes. Por ejemplo, la instruccin mov eax,[_x] carga el contenido de la variable x (de direccin _x) en el registro EAX. Cuando una instruccin, por ejemplo mov, hace referencia a una posicin de memoria y a un registro, el tamao de la posicin de memoria se adapta por omisin al tamao del registro. En cambio, si hace referencia a dos posiciones de memoria, es preciso especificar el tamao de la zona de memoria afectada por la instruccin. Por ejemplo, si se trata de una doble palabra, se usar el indicador dword. La instruccin mov op1,op2 copia el contenido del segundo operando en el primero. Por ejemplo, la instruccin mov eax,ebx copia el contenido del registro EBX en el registro EAX. La instruccin fld operando, donde el operando es una variable en punto flotante, introduce el contenido del operando en la primera posicin de la pila de registros en punto flotante y empuja hacia abajo los contenidos anteriores de dicha pila. La instruccin fstp operando, donde el operando es una variable en punto flotante, extrae de la pila de registros en punto flotante el contenido de la primera posicin de la pila (y lo elimina de ella) y lo almacena en el operando. La instruccin fild operando, donde el operando es una variable entera, convierte el valor del operando a punto flotante, introduce el resultado en la primera posicin de la pila de registros en punto flotante y empuja hacia abajo los contenidos anteriores de dicha pila. La instruccin fistp operando, donde el operando es una variable entera, extrae de la pila de registros en punto flotante el contenido de la primera posicin de la pila (y lo elimina de ella), convierte dicho valor al tipo entero y lo almacena en el operando. Instrucciones de manejo de la pila: usualmente, la pila de una aplicacin de 32 bits gestiona datos con tamao de dobles palabras. Al introducir datos nuevos, la pila se extiende hacia posiciones de memoria con direcciones ms pequeas, por lo que el valor del puntero a la pila (el registro ESP) disminuye en cuatro unidades cuando se introduce un dato en la pila y aumenta en cuatro unidades cuando se extrae un dato de la pila. La instruccin push operando resta 4 al contenido del registro ESP (puntero a la pila) y a continuacin inserta el operando en la pila.

Captulo 6. Generacin de cdigo

245

La instruccin pop operando copia el contenido que est situado en la cima de la pila (es decir, en la direccin ms baja) sobre el operando, y a continuacin suma 4 al contenido del registro ESP. Por ejemplo, la instruccin pop eax almacena el contenido de la cima de la pila en el registro EAX. Instrucciones aritmticas. La instruccin add op1,op2 suma los dos operandos enteros y almacena el resultado en el primero. La instruccin sub op1,op2 resta el segundo operando entero del primero y almacena en el resultado en el primero. La instruccin mul operando, donde el operando es un entero con un tamao de 32 bits, lo multiplica por el contenido de EAX. El resultado se almacena en la concatenacin de los registros EDX y EAX. La instruccin div operando, donde el operando es un entero con un tamao de 32 bits, divide la concatenacin de los registros EDX y EAX por el operando. El cociente se almacena en el registro EAX, y el resto de la divisin en el registro EDX. Las instrucciones fadd operando, fsub operando, fmul operando y fdiv operando, donde el operando es una variable en punto flotante, suman, restan, multiplican o dividen (respectivamente) el operando con el contenido de la primera posicin de la pila de registros en punto flotante y almacenan el resultado en la misma posicin de la pila. La instruccin neg operando sustituye el contenido del operando por el complemento a dos de su valor original (es decir, le cambia el signo). Instrucciones lgicas. La instruccin and op1,op2 lleva a cabo la operacin lgica AND, bit a bit, entre los dos operandos, y almacena el resultado en el primero. La instruccin or op1,op2 lleva a cabo la operacin lgica OR, bit a bit, entre los dos operandos, y almacena el resultado en el primero. La instruccin xor op1,op2 lleva a cabo la operacin lgica XOR, bit a bit, entre los dos operandos, y almacena el resultado en el primero. La instruccin de comparacin cmp op1,op2 resta el segundo operando entero del primero, sin almacenar el resultado en ningn sitio. La operacin afecta a los indicadores (flags) de la unidad aritmtico-lgica, como si la operacin se hubiera realizado realmente. El contenido de estos indicadores puede utilizarse posteriormente por instrucciones de salto. La instruccin fcmp operando, donde el operando es una variable en punto flotante, resta el operando del contenido de la primera posicin de la pila de registros en punto flotante y modifica adecuadamente los indicadores, sin almacenar el resultado en ningn sitio. Instrucciones de salto. La instruccin jmp etiqueta (salto incondicional) salta a la direccin especificada por la etiqueta.

246

Compiladores e intrpretes: teora y prctica

Despus de una instruccin de comparacin cmp op1,op2 pueden aparecer las siguientes instrucciones de salto condicional: je etiqueta salta a etiqueta si op1 es igual a op2. jne etiqueta salta a etiqueta si op1 es distinto de op2. jl etiqueta salta a etiqueta si op1 es menor que op2. jle etiqueta salta a etiqueta si op1 es menor o igual que op2. jg etiqueta salta a etiqueta si op1 es mayor que op2. jge etiqueta salta a etiqueta si op1 es mayor o igual que op2. La instruccin jz etiqueta salta a la direccin especificada por la etiqueta si el indicador de resultado cero est encendido, es decir, si el resultado de la ltima operacin realizada fue cero. De igual manera, la instruccin jnz etiqueta salta a la direccin especificada por la etiqueta si el indicador de resultado cero est apagado, es decir, si el resultado de la ltima operacin realizada fue distinto de cero. Instrucciones de llamada y retorno de una subrutina. La instruccin call etiqueta invoca a la subrutina de nombre etiqueta. Para ello, primero almacena en la pila la direccin de la siguiente instruccin a ejecutar (la direccin de retorno) y despus salta a la direccin de memoria correspondiente a etiqueta. La instruccin inversa a la anterior (ret) extrae de la pila el valor de la siguiente instruccin que se va a ejecutar y transfiere el control a dicha instruccin.

6.1.1. Gestin de los registros de la mquina


Cada tipo de computadora funciona con su propio lenguaje de la mquina y posee una unidad aritmtico-lgica propia. Dicha unidad contiene cierto nmero de registros de trabajo de acceso muy rpido, en los que se puede almacenar informacin y operar con ella realizando sumas, restas, comparaciones, etc. Existen mquinas (ms bien antiguas) que poseen un solo registro de trabajo especial, denominado acumulador. En otras mquinas la unidad aritmtico-lgica contiene cierto nmero (por ejemplo, 32) de registros idnticos e intercambiables. Por ltimo, algunas mquinas, como la serie INTEL 80x86, poseen un nmero reducido de registros de trabajo con propiedades no exactamente idnticas. Estos registros son diferentes segn que el modo de trabajo de estas mquinas sea de 16 o de 32 bits. Como se ha indicado, en 32 bits los registros desempean papeles diferentes y se llaman EAX (acumulador), EBX (registro base de indexacin), ECX (registro contador), EDX (registro complementario del acumulador), ESP (puntero a la pila), EBP (registro base para las variables automticas en la pila, vase la Seccin 10.1), ESI y EDI (registros de copia origen y destino). En 16 bits, los registros correspondientes se denominan AX, BX, CX, DX, SP, BP, SI y DI. Adems, existen algunos nombres adicionales que se refieren a partes de los registros anteriores, como AH y AL (Bytes superior e inferior de AX), BH y BL, CH y CL, DH y DL. Las instrucciones de la mquina pueden hacer uso de los registros, o bien trabajar directamente sobre la memoria direccionable. Sin embargo, numerosas operaciones (clculo de expresiones, comparacin de los valores de dos variables, etc.) exigen que al menos uno de los

Captulo 6. Generacin de cdigo

247

operandos se encuentre copiado sobre uno de los registros de trabajo (a veces, como se ha visto al mencionar las instrucciones mul y div, debe encontrarse en un registro concreto). Por ello, una parte muy importante de todo generador de cdigo tiene que ver con la carga o copia de los valores de las variables sobre los registros de trabajo, as como la gestin de los registros, pues al ser varios, en un momento dado podran contener el valor de ms de una variable. Si la unidad aritmtico-lgica estuviese provista de un solo registro acumulador (como ocurre en mquinas antiguas y, hasta cierto punto, en las mquinas INTEL, pues en stas el registro EAX desempea un papel distinguido en ciertos casos), es conveniente disponer en el compilador de una rutina que el generador de cdigo puede utilizar para asegurarse de que una u otra de las variables que toman parte en un clculo determinado se encuentra cargada en el acumulador, y en caso contrario realice la carga de una de ellas. En algunas operaciones conmutativas (como la suma o la comparacin de la igualdad), no nos importa cul de las dos variables est cargada en el acumulador, pues basta que sea una cualquiera de ellas. En otras operaciones no conmutativas, como la resta o la divisin, interesa, en cambio, especificar cul de los dos operandos (normalmente el izquierdo) debe encontrarse en el acumulador. La funcin CAC, escrita en el lenguaje C, asegura todas estas condiciones: int CAC (opd *x, opd *y) { if (AC!=NULL && AC==y) return 1; if (AC!=x) { if (AC!=NULL) GEN (MOV, AC, EAX); GEN (MOV, EAX, x); AC=x; } return 0; } Esta rutina puede invocarse de dos maneras diferentes: CAC (x,y): aplicable a las operaciones conmutativas, indica que se desea cargar el valor de la variable x o de la variable y, indistintamente. CAC (x,NULL): aplicable a las operaciones no conmutativas, indica que se desea cargar el valor de x, exclusivamente. La variable auxiliar AC contiene una estructura especial, llamada plataforma, que almacena informacin sobre la variable que est cargada en el acumulador en un momento dado. Dicha informacin (que coincide con la que se guarda en la pila semntica) indica cul es el nombre de la variable, cul es su tipo, la direccin que se le ha asignado, si se trata de un vector indexado y con qu subndice, o si la variable es accesible a travs de un puntero y con qu desplazamiento. Esta informacin podra sustituirse, toda o en parte, por un puntero al elemento de la tabla de smbolos correspondiente a la variable de que se trate. Esencialmente, la rutina CAC realiza los siguientes pasos: Comprueba si el acumulador contiene ya la variable y (siempre que esta variable exista), en cuyo caso no hace nada y devuelve un 1.

248

Compiladores e intrpretes: teora y prctica

Comprueba si el acumulador contena ya la variable x, en cuyo caso no hace nada y devuelve un 0. Si el acumulador estaba vaco (no contena el valor de ninguna variable), se carga el valor de x en el acumulador y se devuelve un 0. En caso contrario, se genera una instruccin que guarde el valor actual del acumulador en la direccin de memoria asociada a la variable que contena, se carga el valor de x en el acumulador y se devuelve un 0. En cualquier caso, si CAC devuelve 0, significa que x est ahora cargado en el acumulador; si devuelve 1, que es el valor de y el que se encuentra all. La funcin auxiliar GEN aade una instruccin nueva al programa objeto. Esta funcin admite tres argumentos: el cdigo de operacin de la instruccin, el operando izquierdo y el operando derecho. Si el argumento es una cadena de caracteres, se copiar directamente sobre la instruccin generada. Si se trata de una plataforma, la funcin GEN generar el nombre apropiado para el operando. Por ejemplo: GEN(MOV, EAX, x), donde x es una plataforma que define el operando A, generar la instruccin MOV EAX,A. GEN(MOV, EAX, x), donde x es una plataforma que define el operando B[4], generar la instruccin MOV EAX,[_B+4*sizeof(tipo de B)] (si el origen de ndices en el lenguaje fuente es cero). Si en vez de un solo acumulador existe un conjunto de registros intercambiables, la variable AC podra sustituirse por un vector de variables de tipo plataforma, cada uno de cuyos elementos contendr informacin sobre el operando contenido en el registro correspondiente. Ser preciso distinguir los registros en punto fijo de los de punto flotante. Por otra parte, algunos registros podran estar reservados para uso interno del compilador (por ejemplo, como ndices de bucles). Una rutina general de carga de registros deber comenzar por seleccionar el registro que se va a utilizar entre todos los disponibles. Para ello hay que tener en cuenta el tipo del registro sobre el que se desea cargar (registros enteros o en punto flotante). Si no hay ninguno del tipo deseado, se elegir uno de los que estn ocupados, despreciando la informacin que contiene (si ya no es necesaria) o guardndola en la posicin de memoria asociada, en caso contrario. Una vez seleccionado el registro, la carga propiamente dicha depender del tipo del objeto que hay que cargar. Para ver con claridad qu clase de operacin se debe realizar en la carga de un operando sobre un registro, es conveniente que el diseador del compilador construya una tabla parecida a la 6.1, que slo contiene columnas para algunos de los tipos posibles. En esta tabla, el nombre T se aplica a una posicin de la memoria del programa objeto que el compilador utilizara como memoria auxiliar, para introducir en ella valores intermedios que no corresponden a ninguna variable del programa fuente. En este caso, sirve como etapa intermedia para la conversin de los datos de tipo entero a punto flotante.

Captulo 6. Generacin de cdigo

249

Tabla 6.1. Generacin de cdigo ensamblador para la carga de un operando sobre un registro entero (RH-RL=RX) o en punto flotante. Carga sobre un registro de tipo entero Tipo del operando que hay que cargar unsigned char XOR RH,RH MOV RL,x XOR RH,RH MOV RL,x MOV T,x FLD T int MOV RX,x constante entera MOV RX,x real FLD x FISTP x MOV RX,x FLD x

punto flotante

FIL D x

MOV T,x FLD T

6.1.2. Expresiones
A continuacin se muestra una gramtica de expresiones tpica, como las que suelen encontrarse en muchos lenguajes de programacin: ::= <exp> + <exp> | <exp> <exp> | <exp> * <exp> | <exp> / <exp> | - <exp> | id | <constante> | ( <exp> ) | ( <compare> ) | <dereference> <compare> ::= <exp> = <exp> | <exp> != <exp> | <exp> > <exp> | <exp> >= <exp> | <exp> < <exp> | <exp> <= <exp> | <compare> + <compare> | <compare> * <compare> | <compare> <constant> ::= <bool_const> | int_const | real_const <bool_const> ::= true | false <exp>

250

Compiladores e intrpretes: teora y prctica

En realidad, la gramtica anterior es ambigua, por lo que el analizador sintctico tendr que emplear otra algo diferente, o bien utilizar algoritmos especiales de desambiguacin (vase el Captulo 4). En este captulo se supondr que los smbolos id, int_const, real_const, true y false son unidades sintcticas terminales, es decir, su construccin ha sido tratada previamente por el analizador morfolgico. Existen tipos muy diversos de expresiones, en funcin del conjunto de valores que se puede calcular. Por ejemplo, se podran aceptar variables y expresiones de los tipos indicados en la Tabla 6.2, que tambin indica el tamao que suelen tener los objetos de los tipos indicados.
Tabla 6.2. Tipos de datos y tamao que ocupa cada elemento. Tipo de dato Boolean char unsigned char short unsigned short long unsigned long int unsigned int float double Tamao de cada elemento 1 bit, o 1, 2 o 4 Bytes 1 Byte 1 Byte 2 Bytes 2 Bytes 4 Bytes 4 Bytes 2 o 4 Bytes 2 o 4 Bytes 4 Bytes 8 Bytes

Dependiendo del lenguaje, un dato de tipo Boolean puede ocupar todos los tamaos indicados en la tabla. En APL, por ejemplo, los datos de este tipo se empaquetan a razn de 8 elementos por Byte, es decir, ocupan 1 bit. En C, los datos booleanos se tratan en realidad como si fuesen de tipo int: ocupan 2 o 4 Bytes (segn que se est usando un modelo de memoria de 16 o de 32 bits, respectivamente; vase la Seccin 10.1). Si su valor es cero, se supone que representan el valor false; en caso contrario, representan el valor true. Para simplificar, en este apartado se supondr que slo existen los siguientes tipos de expresiones: Boolean, char, short y double. Adems, los datos de tipo Boolean no se podrn mezclar en las operaciones con los de los otros tipos, pero podran obtenerse como resultado de operaciones de comparacin realizados con dichos tipos. La construccin de tablas de cdigo generado, como la Tabla 6.1, a la que se hizo referencia en el tratamiento de la carga de un operando en un registro (vase la Seccin 6.1.1), es tambin muy til para generar el cdigo asociado a las operaciones que aparecen en las expresiones, es-

Captulo 6. Generacin de cdigo

251

Tabla 6.3. Generacin de cdigo ensamblador para la operacin suma.


Tipo del operando izquierdo x unsigned char int Registro entero Constante entera double Registro double Tipo del operando derecho y unsigned char Carga x Repite suma Carga y Repite suma Carga Repite suma Intercambio Repite suma Carga y Repite suma Intercambio Repite suma int Registro entero Intercambio Repite suma ADD y,x ADD x,y Intercambio Repite suma Intercambio Repite suma Intercambio Repite suma Constante entera Carga x Repite suma Carga y Repite suma ADD x,y Intercambio Repite suma Intercambio Repite suma double Registro double Carga x Repite suma FIADD x MOV T,x Repite suma FADD x FADD x FADD y

Intercambio Repite suma Carga y Repite suma ADD x,y Intercambio Repite suma Carga y Repite suma Intercambio Repite suma

Carga x Repite suma Carga x Repite suma MOV T,x Repite suma Carga x Repite suma Carga y Repite suma Intercambio Repite suma

pecialmente para las didicas, en las que la tabla ser de doble entrada, en funcin de los tipos respectivos del argumento izquierdo y del derecho. A menudo, el cambio de tipo se puede realizar de forma ms o menos directa a travs de la operacin de carga en registro definida anteriormente. La Tabla 6.3 muestra, como ejemplo, la tabla correspondiente a la operacin suma, cuya regla es <exp> ::= <exp> + <exp>. Las operaciones Carga x y Carga y representan la aplicacin de la Tabla 6.1 a la variable correspondiente. La realizacin de esta operacin modifica la plataforma asociada a uno de los operandos, pues el tipo de la variable en cuestin pasa a ser Registro entero o Registro flotante, una vez realizada la operacin. La Carga tendr lugar sobre un registro entero si ambas variables (x e y) pertenecen a uno de los cuatro primeros tipos de la Tabla 6.3, y sobre un registro flotante (double) en caso contrario. Una vez realizada esta operacin, se vuelve a aplicar la misma tabla sobre la nueva combinacin de plataformas, lo que nos llevar automticamente a una casilla diferente. La operacin Intercambio consiste, como indica su nombre, en intercambiar las dos plataformas: la del operando izquierdo pasar a ser la del derecho, y viceversa. Este intercambio no afecta al resultado de la operacin, pues la suma es conmutativa. Despus de realizada esta operacin, es preciso volver a aplicar la Tabla 6.3, lo que nos llevar a la casilla simtrica de la anterior respecto a la diagonal principal de la tabla. En la operacin MOV T,x, que aparece en dos casillas de la Tabla 6.3, el nombre T se aplica a una posicin de la memoria del programa objeto que el compilador utilizar como memoria auxiliar. Esta operacin tambin modifica la plataforma, pues la correspondiente al operando izquierdo (x, cuyo tipo era registro entero) pasa a apuntar a la variable T, cuyo tipo es variable de tipo int contenida en la memoria.

252

Compiladores e intrpretes: teora y prctica

La programacin de la tabla de la suma se podra hacer como se indica en el seudocdigo siguiente: Label Tabla[n][n] = {LCX, LXG, LXG, LCX, LCX, LCX} {LCY, LCY, L1, LCY, LCX, L2} {LCY, L3, L3, L3, L4, L4} {LXG, LXG, LXG, 0, LCX, L5} {LCY, LCY, LXG, LXG, LCY, L5} {LXG, LXG, LXG, LXG, LXG, L6} L: GOTO Tabla[tipox][tipoy]; LCX: CARGA X; GOTO L; LCY: CARGA Y; GOTO L; LXG: Intercambio (X,Y); GOTO L; L1: GEN (ADD, Y, X); return; L2: GEN (FIADD, Y, NULL); return; L3: GEN (ADD, X, Y); return; L4: GEN (MOV, T, X); GOTO L; L5: GEN (FADD, X, NULL); return; L6: GEN (FADD, Y, NULL); return; En las operaciones no conmutativas (como la resta) se construye una tabla semejante a la 6.3, pero al no poder intercambiar los operandos, el nmero de casillas de la tabla que generan cdigo aumenta. En las operaciones mondicas o unarias (con un solo argumento, normalmente situado a la derecha del operador en casi todos los lenguajes de programacin) la tabla se reduce normalmente a una tabla de entrada simple. La Tabla 6.4 muestra, como ejemplo, la que correspondera a la operacin cambio de signo ( mondico), que corresponde a la regla <exp> ::= - <exp>.
Tabla 6.4. Generacin de cdigo ensamblador para la operacin de cambio de signo. Tipo del operando derecho y unsigned char Carga y Repite int Carga y Repite Registro entero NEG y Constante entera double Carga y Repite Registro double FCHS

Captulo 6. Generacin de cdigo

253

Para una expresin de comparacin correspondiente a las reglas de la gramtica <compare> ::= <exp> = <exp> | <exp> != <exp> | <exp> > <exp> | <exp> >= <exp> | <exp> < <exp> | <exp> <= <exp> es posible construir tambin una tabla que ser muy semejante a la de la operacin resta, sustituyendo las instrucciones SUB y FSUB por las instrucciones de comparacin CMP y FCMP. En cuanto a las reglas <compare> ::= <compare> + <compare> | <compare> * <compare> | <compare> se ha optado en esta gramtica por representar las operaciones OR y AND mediante los mismos smbolos + y * que se utilizan para la suma de nmeros. Esto es posible, porque se ha dicho anteriormente que esta gramtica no admite expresiones que mezclen datos Booleanos y numricos, por lo que esta sintaxis no sera ambigua. En otros lenguajes (como C o APL) se utilizan smbolos diferentes para estos operadores, pues los datos pueden mezclarse, dado que el tipo booleano se reduce, en realidad, a una forma ms de dato numrico.

6.1.3. Punteros
Como se ha explicado en la Seccin 5.4, en muchos lenguajes existe un tipo especial de variables, llamadas punteros, que permiten acceder a la informacin contenida en las variables a las que apuntan por medio de reglas sintcticas ms o menos parecidas a las siguientes: <dereference> ::= deref <id> | deref <dereference> En el lenguaje C, por ejemplo, la unidad sintctica que aqu hemos representado con el smbolo terminal deref es un asterisco. Cuando hay que desreferenciar un puntero, el cdigo generado podra ser el siguiente: mov ebx,id Esta instruccin introduce en el registro de indexacin ebx el contenido del puntero, es decir, la direccin de memoria de la variable a la que ste apunta. A continuacin se genera una plataforma, en la que la direccin del operando correspondiente es [ebx].

254

Compiladores e intrpretes: teora y prctica

6.1.4. Asignacin
La forma tpica de las reglas sintcticas que regulan la asignacin de un valor a una variable es la siguiente: <asignacion> ::= <id> := <exp> | <dereference> := <exp> La primera regla corresponde a la asignacin de valor a una variable ordinaria; la segunda, a la asignacin de valor (una direccin) a un puntero. Dependiendo del lenguaje de que se trate, pueden exigirse condiciones semnticas especiales a las variables afectadas por una asignacin a un puntero. Es posible (y conveniente) construir para la asignacin una tabla parecida a la de la suma (vase la Tabla 6.3), que especifique el cdigo que hay que generar en cada una de las combinaciones posibles de los tipos del identificador situado a la izquierda del smbolo de asignacin y de la expresin situada a la derecha. Esto significa que la asignacin puede considerarse como un operador didico ms, semejante a los operadores aritmticos (suma, resta, etc.), con la nica salvedad de que el operando izquierdo se pasa por referencia, y no por valor, como ocurre con el derecho, y con ambos operandos en las operaciones aritmticas. Por consiguiente, entre los tipos que puede adoptar el operando izquierdo hay que incluir el tipo puntero, lo que significa aadir una lnea ms a la tabla.

6.1.5. Entrada y salida de datos


Las instrucciones de entrada y salida de datos desde memorias o dispositivos externos al programa ejecutable (como el teclado, la pantalla, archivos en disco, etc.) varan mucho con el lenguaje fuente, por lo que no vamos a considerarlas aqu. Baste decir respecto a ellas que normalmente se llevan a cabo mediante llamadas a subrutinas de biblioteca, con lo que el generador de cdigo deber generar, usualmente, una instruccin CALL.

6.1.6. Instrucciones condicionales


Una instruccin condicional tpica que slo tenga parte then podra tener una regla parecida a la siguiente: <condicional> ::= if <exp> then (1) <instruccion> (2) Como se vio en la Seccin 6.1.2, el anlisis del smbolo no terminal <exp> (que en este tipo de instrucciones se reducir a una comparacin) habr generado el cdigo apropiado para que los indicadores sealen adecuadamente el resultado de la operacin. Hay dos maneras de generar el cdigo correspondiente a la instruccin if: Que el cdigo generado por la comparacin almacene en los indicadores el valor true o false, segn corresponda, encendiendo (set) o apagando (reset) uno de los indicadores

Captulo 6. Generacin de cdigo

255

(por ejemplo, el de resultado cero). En tal caso, la instruccin condicional slo tendra que generar la siguiente instruccin en la accin semntica (1) situada en la regla justo a continuacin de la unidad sintctica then: jz fin_then# donde fin_then# es una etiqueta interna generada por el compilador (diferente para cada instruccin condicional, por supuesto). Por otra parte, la accin semntica (2), situada al final de la regla, generara el siguiente cdigo: fin_then#: Es decir, colocara la etiqueta despus del cdigo generado por el smbolo no terminal <instruccion>. Que el cdigo generado por la comparacin se limite a la instruccin de comparacin, y que se aada a la informacin semntica asociada al resultado el tipo de operador de comparacin que acaba de analizarse. La Tabla 6.5 indica el cdigo que habra que generar en funcin de dicho operador. Esto correspondera a la accin semntica (1). La accin (2) sera idntica al caso anterior. Este procedimiento genera cdigo algo ms optimizado que el otro.
Tabla 6.5. Generacin de cdigo condicional asociado a instrucciones de comparacin. Operacin = != < <= > >= Cdigo generado je fin_then jne fin_then jl fin_then jle fin_then jg fin_then jge fin_then

La Figura 6.1 proporciona un esquema grfico de la generacin de cdigo para la instruccin condicional. En dicha figura y las sucesivas, los puntos de la regla donde aparece un nmero encerrado en un crculo indican acciones semnticas. La Figura 6.2 muestra el cdigo que habra que generar para la regla <condicional> ::= if <exp> then (1) <instruccion1> (2) else <instruccion2> (3) La primera accin semntica genera un cdigo exactamente igual al generado por la primera accin semntica de la Figura 6.1. La segunda genera una instruccin de salto incondicional

256

Compiladores e intrpretes: teora y prctica

<condicional> ::= if <exp> then 1 <instruccion> 2

; ; ; exp PILA

<exp>

jz fin_then#
; ; ;

<instruccion>

fin_then#:

Figura 6.1. Generacin de cdigo para la instruccin if-then.

<condicional> ::= if <exp> then

1 <instruccion1> else

<instruccion2> 3

; ; ; exp PILA

<exp>

jz fin_then#
; ; ;

<instruccion1>

jmp fin_ifelse# fin_then#


; ; ;

<instruccion2>

fin_ifelse#:

Figura 6.2. Generacin de cdigo para la instruccin if-then-else.

(jmp) a la etiqueta fin_ifelse# para evitar que se ejecuten las instrucciones de la parte else tras ejecutar las instrucciones de la parte then. Adems se genera una lnea que define la etiqueta fin_then#. Por ltimo, la tercera accin semntica genera nicamente una lnea que contiene la etiqueta fin_ifelse#.

6.1.7. Bucles
La Figura 6.3 muestra el cdigo que habra que generar para la regla <bucle> ::= while (1) <exp> do (2) <instruccion> end (3)

Captulo 6. Generacin de cdigo

257

<bucle> ::= while

<exp > do

<instruccion> end 3

inicio_while#:
; ; ; exp PILA

<exp>

jz fin_while#
; ; ;

<instruccion>

jmp inicio_while# fin_while#:

Figura 6.3. Generacin de cdigo para la instruccin while.

La primera accin semntica genera nicamente una lnea que contiene la etiqueta inicio_while#. La segunda accin semntica genera un cdigo exactamente igual al generado por la primera accin semntica de las Figuras 6.1 y 6.2. El efecto de este cdigo es salir del bucle si el valor de la expresin es igual a 0, es decir, si la expresin es falsa. La tercera accin semntica genera una instruccin de salto incondicional (jmp) a la etiqueta inicio_while#, para continuar con la siguiente iteracin del bucle. Adems genera una lnea que contiene la etiqueta fin_while#. La Figura 6.4 muestra el cdigo que habra que generar para la regla <bucle> ::= repeat (1) <instruccion> until <exp> (2)

<bucle> ::= repeat 1

; ; ; ; ;

<instruccion> until <exp> 2

inicio_repeat#: <instruccion> <exp>

jz fin_repeat# jmp inicio_repeat# fin_repeat#:

Figura 6.4. Generacin de cdigo para la instruccin repeat.

258

Compiladores e intrpretes: teora y prctica

La primera accin semntica genera nicamente una lnea que contiene la etiqueta inicio_repeat#. La segunda accin semntica genera un cdigo similar al generado por la primera accin semntica de las Figuras 6.1 y 6.2. El efecto de este cdigo es salir del bucle si el valor de la expresin es true, es decir, si la expresin es verdadera. En caso contrario, se ejecuta un salto incondicional (jmp) a la etiqueta inicio_repeat#, para continuar con la siguiente iteracin del bucle. Adems se genera una lnea que contiene la etiqueta fin_repeat#.

6.1.8. Funciones
El diseo del manejo de funciones por un compilador implica dar respuesta a las siguientes preguntas: Cmo se comunican los argumentos desde el programa que invoca a la funcin invocada? Cmo se comunican los resultados desde la funcin invocada al programa que invoca? En general, se utiliza una pila para almacenar las variables locales de la funcin (variables automticas) y los argumentos de llamada a la funcin. En algunos lenguajes, como C y C++, los argumentos se guardan en la pila en orden inverso (de derecha a izquierda). En otros lenguajes, se hace al revs. Para pasar el resultado de la funcin al programa que la invoc, se suele utilizar el registro EAX, siempre que dicho resultado quepa en l. De lo contrario se puede usar la pila, o bien la memoria esttica. En la Seccin 10.1, cuando se describa la gestin de memoria para las variables automticas en un compilador, se puede encontrar un ejemplo del cdigo generado en las llamadas a funciones.

6.2 Cdigo intermedio


Como se describi en la Seccin 5.1.3, en el primer paso de la compilacin, en compiladores de dos o ms pasos, el analizador semntico genera un cdigo abstracto, denominado cdigo intermedio. En un segundo paso se realiza la generacin del cdigo definitivo a partir del cdigo intermedio. En esta seccin se estudiarn dos formas de cdigo intermedio: notacin sufija y cudruplas. Los operadores didicos (o binarios) pueden especificarse mediante tres notaciones principales: prefija: el operador didico se analiza antes que sus operandos. infija: el operador didico se analiza entre sus dos operandos. sufija: el operador didico se analiza despus que sus operandos. En los lenguajes de programacin clsicos, los operadores didicos se representan usualmente en notacin infija. La notacin prefija permite al operador influir sobre la manera en que se pro-

Captulo 6. Generacin de cdigo

259

cesan sus operandos, pero a cambio suele exigir mucha ms memoria. La sufija no permite esa influencia, pero optimiza la gestin de memoria y permite eliminar el procesado de los parntesis. Los operadores mondicos slo pueden presentarse en notacin prefija o sufija. En casi todos los lenguajes, la mayor parte de estos operadores suelen utilizar la sintaxis prefija. En Smalltalk se usa siempre la notacin sufija, que tambin puede utilizarse con algunos operadores en los lenguajes C y C++ (por ejemplo, el operador ++). Adems, un rbol sintctico puede representarse en forma de tuplas de n elementos, de la forma (operador, operando1, ..., operandon, resultado). Las tuplas pueden tener longitud variable o fija (con operandos nulos). Las ms tpicas son las cudruplas, aunque stas pueden representarse tambin en forma de tripletes.

6.2.1. Notacin sufija


Llamada tambin notacin postfija o polaca inversa, se usa para representar expresiones sin necesidad de parntesis, eliminando tambin la necesidad de establecer precedencia entre los distintos operadores. La Tabla 6.6 muestra algunos ejemplos.

Tabla 6.6. Algunos ejemplos de expresiones en notacin sufija. Expresin a*b a*(b+c/d) a*(b+c*d) Notacin sufija ab* abcd/+* ab*cd*+

Como puede apreciarse en los ejemplos anteriores, en una expresin en notacin sufija los identificadores aparecen en el mismo orden que en la forma usual de las expresiones, mientras que los operadores aparecen en el orden de su evaluacin, de izquierda a derecha. Un problema que se plantea en la notacin sufija es cmo tratar los operadores mondicos o unarios cuyo smbolo coincide con el de algn operador binario, por ejemplo, el operador de cambio de signo (). Existen dos posibilidades: transformarlos en operadores didicos o binarios, o utilizar un smbolo distinto. Por ejemplo, la expresin a puede convertirse en 0-a o en @a. Si se elige la segunda opcin, la expresin a*(-b+c/d) se representara en notacin sufija como ab@cd/+*. Una vez descrita la notacin sufija, es necesario explicar cmo realizar el compilador las siguientes tareas: Construccin de la notacin sufija durante el anlisis sintctico Generacin de cdigo ensamblador a partir de la notacin sufija

260

Compiladores e intrpretes: teora y prctica

Construccin de la notacin sufija durante el anlisis sintctico


En el anlisis ascendente: Si el analizador sintctico es ascendente, hacemos la siguiente suposicin: cuando aparece un smbolo no terminal V en el asidero, la notacin sufija correspondiente a la subcadena que se redujo a V ya ha sido generada. Para generar la notacin sufija, se utiliza una pila, inicialmente vaca, y se aprovechar el algoritmo de anlisis ascendente descrito en el Captulo 4, con las siguientes acciones adicionales: Cada vez que se realiza una operacin de desplazamiento con un smbolo terminal, se asocia dicho smbolo a la informacin semntica del estado que se introduce en la pila de anlisis. Cada vez que se realiza una operacin de reduccin, se pasa a la pila de notacin sufija la informacin semntica asociada a los estados extrados de la pila de anlisis. Ejemplo Consideremos la gramtica siguiente: 6.1 (1) E ::= E + T (2) E ::= T (3) T ::= i En la Tabla 6.7 aparece la tabla de anlisis para esta gramtica.

Tabla 6.7. Tabla de anlisis ascendente para la gramtica del Ejemplo 6.1. E 0 1 2 3 4 5 d5 r2 r3 d3 r1 r1 r1 d1 T d2 i d3 d4 r2 r3 fin r2 r3 + $

La Figura 6.5 muestra el proceso de generacin de la notacin sufija para la expresin a+b, utilizando la tabla de anlisis de la Tabla 6.7. La informacin semntica asociada a un estado aparece entre parntesis, a continuacin del nmero del estado. En el anlisis descendente: En el anlisis descendente, es posible generar la notacin sufija utilizando tambin una pila inicialmente vaca, que al final del anlisis sintctico contendr la notacin sufija resultante. Para ello, es necesario aadir a las funciones del analizador sintctico algunas instrucciones que introducirn los valores adecuados en la pila.

Captulo 6. Generacin de cdigo

261

Pila de anlisis 0 03(a) 0 02 0 01 014(+) 014(+)3(b) 014(+) 014(+)5 0

Entrada a+b$ +b$ T+b$ +b$ E+b$ +b$ b$ $ T$ $ E$

Notacin sufija

ab

ab+

Figura 6.5. Generacin de la notacin sufija para la expresin a+b en el anlisis ascendente.

Como ejemplo, consideremos la gramtica del Ejemplo 4.6, que se reproduce aqu para mayor claridad. E ::= T + E E ::= T E E ::= T T ::= F * T T ::= F / T T ::= F F ::= i F ::= (E) Las funciones que componen el analizador sintctico descendente para esta gramtica aparecen en las Figuras 4.10 a 4.16. Las funciones correspondientes, modificadas para generar la notacin sufija, pueden verse en las Figuras 6.6 a 6.12. La Figura 6.13 muestra el proceso de la generacin de la notacin sufija para la expresin a+b, utilizando las funciones de las Figuras 6.6 a 6.12. Para mayor claridad, se representan mediante una pila las instrucciones pendientes de ejecucin, es decir, las que se ejecutarn cuando la funcin invocada devuelva el control a la funcin que la invoc. Por ejemplo, cuando la funcin V llama a la funcin E, la instruccin push(+) queda pendiente, y se ejecutar cuando la funcin E termine y devuelva el control a la funcin V.

262

Compiladores e intrpretes: teora y prctica

int E (char *cadena, int i) { if (i<0) return i; switch (cadena[i]) { case 'i': push(id); i++; i = V (cadena, i); break; case '(': i++; i = E (cadena, i); i = C (cadena, i); i = V (cadena, i); break; default: return -1; } return i; }
Figura 6.6. Generacin de notacin sufija: funcin para el smbolo no terminal E.

int V (char *cadena, int i) { if (i<0) return i; switch (cadena[i]) { case '*': case '/': i++; i = T (cadena, i); push(cadena[j]); i = X (cadena, i); break; case '+': case '-': i++; i = E (cadena, i); push(cadena[j]); break; } return i; }
Figura 6.7. Generacin de notacin sufija: funcin para el smbolo no terminal V.

Captulo 6. Generacin de cdigo

263

int X (char *cadena, int i) { if (i<0) return i; switch (cadena[i]) { case '+': case '-': push + i++; i = E (cadena, i); push(cadena[j]); break; } return i; }
Figura 6.8. Generacin de notacin sufija: funcin para el smbolo no terminal X.

int T (char *cadena, int i) { if (i<0) return i; switch (cadena[i]) { case 'i': push(id); i++; i = U (cadena, i); break; case '(': i++; i = E (cadena, i); i = C (cadena, i); i = U (cadena, i); break; default: return -2; } return i; }
Figura 6.9. Generacin de notacin sufija: funcin para el smbolo no terminal T.

264

Compiladores e intrpretes: teora y prctica

int U (char *cadena, int i) { if (i<0) return i; switch (cadena[i]) { case '*': case '/': i++; i = T (cadena, i); push(cadena[j]); break; } return i; }
Figura 6.10. Generacin de notacin sufija: funcin para el smbolo no terminal U.

int F (char *cadena, int i) { if (i<0) return i; switch (cadena[i]) { case 'i': push(id); i++; break; case '(': i++; i = E (cadena, i); i = C (cadena, i); break; default: return -3; } return i; }
Figura 6.11. Generacin de notacin sufija: funcin para el smbolo no terminal F.

Generacin de cdigo ensamblador a partir de la notacin sufija


En los ejemplos subsiguientes, supondremos que se est tratando con variables de tipo entero, de un tamao igual al de los registros de trabajo. El algoritmo de generacin de cdigo ensamblador a partir de una expresin en notacin sufija funciona de la siguiente forma: (Caso 1) Si el prximo smbolo es un identificador id, se genera la instruccin push [_id].

Captulo 6. Generacin de cdigo

265

int C (char *cadena, int i) { if (i<0) return i; switch (cadena[i]) { case ')': i++; break; default: return -4; } return i; }
Figura 6.12. Generacin de notacin sufija: funcin para el smbolo no terminal C.

Pasos de ejecucin E(a+b,0) V(a+b,1) E(a+b,2) V(a+b,3) 3

Pila pendiente a push(+)

Notacin sufija

ab

ab+

Figura 6.13. Generacin de notacin sufija para la expresin a+b en el anlisis descendente.

(Caso 2) Si el prximo smbolo es una constante c, se genera la instruccin push c. (Caso 3) Si el prximo smbolo es un operador didico, por ejemplo, la suma, se generan las instrucciones: pop edx pop eax add eax, edx push eax (Caso 4) Si el prximo smbolo es un operador mondico, por ejemplo, el cambio de signo, se generan las instrucciones: pop eax neg eax push dword eax

266

Compiladores e intrpretes: teora y prctica

La Tabla 6.8 muestra la aplicacin de este algoritmo para generar el cdigo ensamblador para la expresin ab@+.
Tabla 6.8. Generacin de cdigo ensamblador para la expresin ab@+. Caso 1 1 4 Entrada ab@+ b@+ @+ Resultados intermedios push [_a] push [_b] pop eax neg eax push eax pop edx pop eax add eax, edx push eax

Otras instrucciones
La notacin sufija se usa principalmente para representar expresiones aritmticas, pero puede extenderse para representar otro tipo de instrucciones, como las siguientes: La asignacin puede tratarse como un operador binario, cuyos operandos son la parte izquierda y derecha de la asignacin. La instruccin de asignacin a:=b*c+d se representara en notacin sufija como abc*d+:=. Recurdese que el operando izquierdo de la asignacin debe pasarse por referencia, no por valor, como ocurre con la mayor parte de los otros operadores. Las etiquetas asociadas a determinadas instrucciones pueden representarse como etiqueta:. Un salto incondicional a otra instruccin con etiqueta L, puede representarse en notacin sufija como L TR, donde TR significa transferencia incondicional. Un salto condicional a una etiqueta L, que debe realizarse nicamente si el resultado de la ltima operacin aritmtico-lgica efectuada fue igual a cero, puede representarse en notacin sufija como L TRZ, donde TRZ significa transferencia si cero. Utilizando los operadores descritos anteriormente, la instruccin condicional if p then inst1 else inst2 puede representarse en notacin sufija como nsp L1 TRZ nsinst1 L2 TR L1: nsinst2 L2:, donde nsp, nsints1 y nsinst2 corresponden a la representacin en notacin sufija de p, inst1 e inst2, respectivamente. Una expresin con subndices, tal como a[exp1; exp2; ...; expn], puede representarse en notacin sufija como a nsexp1 nsexp2 ... nsexpn SUBIN-n, donde nsexp1, nsexp2, ..., nsexpn corresponden a la representacin en notacin sufija de exp1, exp2, ..., expn, respectivamente.

Captulo 6. Generacin de cdigo

267

6.2.2. Cudruplas
Una operacin didica se puede representar mediante la cudrupla (<operador>, <operando1>, <operando2>, <resultado>) Un ejemplo de cudrupla sera (*,a,b,t) que es equivalente a la expresin a*b. Una expresin se puede representar mediante un conjunto de cudruplas. Por ejemplo, la expresin a*b+c*d es equivalente a las siguientes cudruplas: (*,a,b,t1) (*,c,d,t2) (+,t1,t2,t3) Como ejemplo adicional, se puede considerar la expresin con subndices c:=a[i;b[j]] que es equivalente a las siguientes cudruplas: (*,i,d2,t1) (+,t1,b[j],t2) (:=,a[t2],,c) donde a es una matriz con dimensiones d1 (nmero de filas) y d2 (nmero de columnas), y se supone que el origen de ndices es cero.

Tripletes
Otro formato que se puede utilizar para generar cdigo intermedio son los tripletes, que son similares a las cudruplas, con la nica diferencia de que los tripletes no dan nombre a su resultado, y cuando un triplete necesita hacer referencia al resultado de otro, se utiliza una referencia a dicho triplete. Por ejemplo, la expresin a*b+c*d equivale a los siguientes tripletes: (1) (*,a,b) (2) (*,c,d) (3) (+,(1),(2)) mientras que a*b+1 equivale a los tripletes: (1) (*,a,b) (2) (*,(1),1) Tambin puede utilizarse lo que se conoce como tripletes indirectos, que consiste en numerar arbitrariamente los tripletes y especificar aparte el orden de su ejecucin. Por ejemplo, las instrucciones: a := b*c b := b*c

268

Compiladores e intrpretes: teora y prctica

equivalen a los siguientes tripletes: (1) (*,b,c) (2) (:=,(1),a) (3) (:=,(1),b) y el orden de su ejecucin es (1),(2),(1),(3). Este formato es til para preparar la optimizacin de cdigo, porque es ms fcil alterar el orden de las operaciones o eliminar alguna.

Generacin de cudruplas durante el anlisis sintctico


En el anlisis ascendente: Para generar cudruplas se utiliza el algoritmo de anlisis ascendente descrito en el Captulo 4, junto con una pila auxiliar y las siguientes acciones adicionales: Cada vez que se realiza una operacin de desplazamiento con un smbolo terminal, se introduce dicho smbolo en la pila auxiliar. Cada vez que se realiza una operacin de reduccin con una regla que contiene un operador, se ejecuta la accin semntica asociada a la regla que se reduce, siendo las acciones semnticas de la siguiente forma: Para reglas del tipo U ::= V operador W, la accin semntica extrae de la pila auxiliar los operandos correspondientes V (op1) y W (op2), analiza su compatibilidad, crea la cudrupla (operador,op1,op2,Ti) e introduce Ti en la pila auxiliar. Para reglas del tipo U ::= operador V, la accin semntica extrae de la pila auxiliar el operando v(op1), crea la cudrupla (operador,op1,,Ti) e introduce Ti en la pila auxiliar. Consideremos la gramtica del Ejemplo 6.6 y la tabla del anlisis de la Tabla 6.7. La Figura 6.14 muestra el proceso de generacin de cudruplas para la expresin a+b. En el anlisis descendente: De forma anloga a la generacin de notacin sufija, una posible forma de generar cudruplas en el anlisis descendente consiste en utilizar una pila auxiliar y modificar las funciones del analizador sintctico para incluir instrucciones que introduzcan los valores necesarios en la pila y que generen las cudruplas correspondientes. Las funciones que componen el analizador sintctico descendente para la gramtica de expresiones del Ejemplo 4.6, modificadas para generar las cudruplas correspondientes, aparecen en las Figuras 6.15 a 6.21. La Figura 6.22 muestra el proceso de la generacin de las cudruplas correspondientes a la expresin a+b*c, utilizando las funciones de las Figuras 6.15 a 6.21.

Instrucciones condicionales
Cuando se generan cudruplas para las instrucciones de control, puede ocurrir que en el momento en que se genera una cudrupla de salto no se sepa la cudrupla a la que hay que saltar, porque sta no se ha generado todava. Este problema se soluciona de la siguiente forma: se numeran

Captulo 6. Generacin de cdigo

269

Pila de analisis 0 03 0 02 0 01 014 0143 014 0145 0

Entrada a+b$ +b$ T+b$ +b$ E+b$ +b$ b$ $ T$ $ E$ ab a

Pila auxiliar

(+,a,b,t1)

Figura 6.14. Generacin de cudruplas para la expresin a+b en el anlisis ascendente.

unsigned int E (char *cadena, unsigned int i) { if (i<0) return i; switch (cadena[i]) { case 'i': push(id); i++; i = V (cadena, i); break; case '(': i++; i = E (cadena, i); i = C (cadena, i); i = V (cadena, i); break; default: return -1; } return i; }
Figura 6.15. Generacin de cudruplas: funcin para el smbolo no terminal E.

270

Compiladores e intrpretes: teora y prctica

unsigned int V (char *cadena, unsigned int i) { unsigned int j; if (i<0) return i; switch (cadena[i]) { case '*': case '/': j = i; i++; i = T (cadena, i); cuad(cadena[j], pop(), pop(), gen(Ti)); push(Ti); i = X (cadena, i); break; case '+': case '-': j = i; i++; i = E (cadena, i); cuad(cadena[j], pop(), pop(), gen(Ti)); push(Ti); break; } return i; }
Figura 6.16. Generacin de cudruplas: funcin para el smbolo no terminal V.

unsigned int X (char *cadena, unsigned int i) { unsigned int j; if (i<0) return i; switch (cadena[i]) { case '+': case '-': j = i; i++; i = E (cadena, i); cuad(cadena[j], pop(), pop(), gen(Ti)); push(Ti); break; } return i; }
Figura 6.17. Generacin de cudruplas: funcin para el smbolo no terminal X.

Captulo 6. Generacin de cdigo

271

unsigned int T (char *cadena, unsigned int i) { if (i<0) return i; switch (cadena[i]) { case 'i': push(id); i++; i = U (cadena, i); break; case '(': i++; i = E (cadena, i); i = C (cadena, i); i = U (cadena, i); break; default: return -2; } return i; }
Figura 6.18. Generacin de cudruplas: funcin para el smbolo no terminal T.

unsigned int U (char *cadena, unsigned int i) { if (i<0) return i; unsigned int j; switch (cadena[i]) { case '*': case '/': j = i; i++; i = T (cadena, i); cuad(cadena[j], pop(), pop(), gen(Ti)); push(Ti); break; } return i; }
Figura 6.19. Generacin de cudruplas: funcin para el smbolo no terminal U.

272

Compiladores e intrpretes: teora y prctica

unsigned int F (char *cadena, unsigned int i) { if (i<0) return i; switch (cadena[i]) { case 'i': push(id); i++; break; case '(': i++; i = E (cadena, i); i = C (cadena, i); break; default: return -3; } return i; }
Figura 6.20. Generacin de cudruplas: funcin para el smbolo no terminal F.

unsigned int C (char *cadena, unsigned int i) { if (i<0) return i; switch (cadena[i]) { case ')': i++; break; default: return -4; } return i; }
Figura 6.21. Generacin de cudruplas: funcin para el smbolo no terminal C.

las cudruplas y se usa su nmero para identificarlas, se mantiene una variable global, cl.sig, cuyo valor es el nmero de la siguiente cudrupla a generar, y se introducen en una pila los nmeros de las cudruplas pendientes de completar, es decir, aquellas para las que se desconoca el valor de alguno de sus componentes en el momento en que se generaron.

Captulo 6. Generacin de cdigo

273

Pasos de ejecucin E(a+b*c,0) V(a+b*c,1) E(a+b*c,2)

Pila pendiente a

Pila aux.

Cudruplas

ab cuad(+,pop,pop,t1) push(t1)

V(a+b*c,3) T(a+b*c,4) cuad(*,pop,pop,t2) push(t2) X(a+b*c,5) cuad(+,pop,pop,t1) push(t1) U(a+b*c,5) abc at2 t1 (*,b,c,t2) (+,a,t2,t1)

Figura 6.22. Generacin de cudruplas para la expresin a+b en anlisis descendente.

Para llevar a cabo todas estas acciones, se introducen acciones semnticas en determinados puntos de las reglas correspondientes a la instruccin condicional. En concreto, son necesarias las tres acciones semnticas que aparecen entre parntesis. <condicional> ::= if <expr> (2) then <instr> (1) | if <expr> (2) then <instr1> else (3) <instr2> S(1) La instruccin condicional if <expr> then <instr1> else <instr2> generara la siguiente secuencia de cudruplas: (p-1) (p) (q) (?,?,?,t1) (TRZ,(q+1),t1,) ... (TR,(r),,) Cudruplas correspondientes a <expr> (2): Generar cudrupla (p) | push p Cudruplas correspondientes a <instr1> (3): Generar cudrupla (q) Poner (cl.sig) en top | pop push (q) Cudruplas correspondientes a <instr2> (1) Poner (cl.sig) en top | pop

(q+1) (r)

...

274

Compiladores e intrpretes: teora y prctica

Cuando una componente de una cudrupla aparece marcada en negrita, indica que esa componente queda vaca en el momento de generacin de la cudrupla y que su valor se rellenar posteriormente, cuando se conozca. Al generar la cudrupla (p) no conocemos el valor de (q+1), por lo que la accin semntica (2) mete en la pila el nmero de la cudrupla que se acaba de generar (p) para que se rellene su segunda componente cuando se genere la cudrupla q+1. De la misma forma, al generar la cudrupla (q) no conocemos todava el valor de (r), por lo que la accin semntica (3) mete en la pila el nmero de la cudrupla que se acaba de generar (q), para que se rellene su segunda componente cuando se genere la cudrupla r. La Figura 6.23 muestra el proceso de generacin de cudruplas para la siguiente instruccin: if (a < b) then a:=2 else b:=3;

(1) (<,a,b,,t1) (2) (TRZ, , t1,)

(1) (2) (3) (4)

(<,a,b,,t1) (TRZ,5, t1,) (:=,2,,a) (TR, ,,)

(1) (2) (3) (4) (5) (6)

(<,a,b,,t1) (TRZ, 5, t1,) (:=,2,,a) (TR,6,,) (:=,3,,b)

despus de ejecutar (2)

despus de ejecutar (3)

despus de ejecutar (1)

Figura 6.23. Generacin de cudruplas para una instruccin if-then-else.

La instruccin condicional if <expr> then <instr> generara la siguiente secuencia de cudruplas: (p-1) (?,?,?,t1) (p) (TRZ,(r),t1,) ... (r) Cudruplas correspondientes a <expr> (2): Generar cudrupla (p) | push p Cudruplas correspondientes a <instr> (1): Poner (cl.sig.) en top | pop

Captulo 6. Generacin de cdigo

275

(1) (=,x,3,,t1) (2) (TRZ, , t1,)

(1) (2) (3) (4) (5)

(=,x,3,,t1) (TRZ,5, t1,) (+,y,2,t2) (:=,t2,,x)

despus de ejecutar (2)

despus de ejecutar (1)

2
Figura 6.24. Generacin de cudruplas para una instruccin if-then.

Al generar la cudrupla (p) no conocemos el valor de (r), por lo que la accin semntica (2) mete en la pila el nmero de la cudrupla que se acaba de generar (p) para que se rellene su segunda componente cuando se genere la cudrupla r. La Figura 6.24 muestra el proceso de generacin de cudruplas para la siguiente instruccin: if (x=3) then x:=y+2;

Etiquetas y GOTO
Aunque la programacin estructurada que utilizan la mayor parte de los lenguajes de alto nivel excluye el uso de la instruccin GOTO, todos los compiladores la implementan, pues puede generarse automticamente como consecuencia de algn preproceso previo del programa fuente, como ocurre, por ejemplo, con las instrucciones SQL embebidas. Por otra parte, en algunos lenguajes, como APL, GOTO es la nica estructura disponible para el control de flujo del programa. Para la generacin de cudruplas para las etiquetas asociadas a instrucciones y para las instrucciones GOTO, consideraremos que los identificadores que corresponden a etiquetas se reconocen porque, en el campo valor que se les asocia en la tabla de smbolos, el atributo tipo adopta el valor etiqueta. El campo valor contendr, adems, otros dos atributos: Atributo localizada, cuyos valores podrn ser SI o NO. Atributo nm_cudrupla, cuyo valor ser el nmero de la cudrupla correspondiente a la etiqueta. En la Figura 6.25 aparece la estructura de una tabla de smbolos con los atributos mencionados.

276

Compiladores e intrpretes: teora y prctica

valor id tipo localizada nm_cudrupla

Figura 6.25. Estructura de una tabla de smbolos con identificadores que corresponden a etiquetas.

La Figura 6.26 muestra el pseudocdigo para la generacin de cudruplas para la instruccin <salto> ::= GOTO id

buscar id en la tabla de smbolos; if (no est) { insertar(id,etiqueta,NO,cl.sig); generar cudrupla (TR,,,); } else { if (tipo==etiqueta) { if (localizada==SI) generar cudrupla (TR,nm_cudrupla,,); else if (localizada==NO) { i=nm_cudrupla; cambiar valor de id a (etiqueta,NO,cl.sig); generar cudrupla (TR,i,,); } } else error(); }
Figura 6.26. Generacin de cudruplas para la instruccin GOTO id.

Con una instruccin GOTO etiqueta pueden darse tres casos: El identificador correspondiente a la etiqueta no est en la tabla de smbolos, porque todava no se ha procesado la instruccin en la que aparece la etiqueta. En este caso, se inserta en la tabla de smbolos el identificador correspondiente a la etiqueta, y se genera una cudrupla de salto incondicional con la segunda componente vaca, porque el nmero de la cudrupla a la que hay que saltar no se conocer hasta que se localice la etiqueta. Al insertar el identifica-

Captulo 6. Generacin de cdigo

277

dor en la tabla de smbolos, en el campo nm_cudrupla se almacena el nmero de la cudrupla recin generada, correspondiente al salto incondicional. Es la forma de indicar que esa cudrupla est incompleta y que debe completarse cuando se localice la etiqueta. El identificador correspondiente a la etiqueta est en la tabla de smbolos y el campo localizada contiene el valor SI; es decir, ya se ha procesado la instruccin en la que aparece la etiqueta. ste es el caso ms sencillo: slo es necesario generar una cudrupla de salto incondicional. El nmero de la cudrupla a la que se debe saltar est en el campo nm_cudrupla de la tabla de smbolos. El identificador correspondiente a la etiqueta est en la tabla de smbolos y el campo localizada contiene el valor NO; es decir, todava no se ha procesado la instruccin en la que aparece la etiqueta, pero ya ha aparecido otra instruccin GOTO a la misma etiqueta. En este caso, el campo nm_cudrupla del elemento correspondiente al identificador en la tabla de smbolos contiene el nmero de la cudrupla pendiente de completar, correspondiente a la instruccin GOTO ya procesada. Puesto que pueden aparecer varias instrucciones GOTO a la misma etiqueta antes de que sta sea localizada, el valor de dicho atributo no puede ser un nmero nico, sino una lista de nmeros de cudrupla. El mecanismo utilizado para resolver este problema es el siguiente: en el campo nm_cudrupla de la tabla de smbolos se almacena el nmero de la primera cudrupla pendiente de completar; en la segunda componente de esta cudrupla se almacena el nmero de la siguiente cudrupla pendiente de completar; y as sucesivamente. La Figura 6.27 muestra un ejemplo que ilustra el mecanismo de gestin de cudruplas pendientes de completar para la instruccin GOTO. En dicho ejemplo, la lista de cudruplas pendientes de completar para la etiqueta et1 sera r, q, p.
TABLA DE SMBOLOS valor id tipo ... etiqueta ... localizada ... NO ... nm_cudrupla ... r ...

... et1 ...

FUENTE GOTO et1 ... GOTO et1 ... GOTO et1 ...

CUDRUPLAS (p) (TR,,,) ... (q) (TR,p,,) ... (r) (TR,q,,) ...

Figura 6.27. Gestin de cudruplas pendientes de completar.

278

Compiladores e intrpretes: teora y prctica

buscar id en la tabla de smbolos; if (no est) insertar(id,etiqueta,SI,cl.sig); else if (tipo==etiqueta && localizada==NO){ i=nm_cudrupla; while (i) { j=cudrupla[i][2]; cudrupla[i][2]=cl.sig; i=j; } cambiar valor de id a (etiqueta,SI,cl.sig); } else error();
Figura 6.28. Generacin de cudruplas para la instruccin etiqueta:.

La Figura 6.28 muestra el pseudocdigo para la generacin de cudruplas para la instruccin <etiqueta> ::= id : <instruccion> Con una instruccin del tipo etiqueta:, pueden darse tres casos: El identificador correspondiente a la etiqueta no est en la tabla de smbolos, porque la etiqueta aparece antes de alguna instruccin GOTO etiqueta. En este caso, se inserta el identificador correspondiente a la etiqueta en la tabla de smbolos. El valor del campo nm_cudrupla ser el valor de la variable global cl.sig. El identificador correspondiente a la etiqueta est en la tabla de smbolos, pero no est definido como etiqueta o, si lo est, el campo localizada contiene el valor SI. En tal caso se ha detectado un error, porque la etiqueta ya haba sido definida previamente, en el primer caso como variable, en el segundo como etiqueta (etiqueta duplicada). El identificador correspondiente a la etiqueta est en la tabla de smbolos, definido como etiqueta, y el campo localizada contiene el valor NO. Esto ocurre porque la definicin de la etiqueta aparece despus de una o ms instrucciones del tipo GOTO etiqueta. En este caso, el bucle while que aparece en el pseudocdigo se encarga de completar las cudruplas pendientes. Adems, en la fila correspondiente a la etiqueta en la tabla de smbolos, se asigna el valor SI al campo localizada y el valor de la variable cl.sig al campo nm_cudrupla. La Figura 6.29 muestra el proceso de generacin de cudruplas para el siguiente esqueleto de cdigo, que, aunque no tiene utilidad, sirve para ilustrar todos los casos descritos anteriormente. GOTO L1; a:=3; GOTO L1; a:=4; L1: x:=5; L2: y:=6; GOTO L2;

Captulo 6. Generacin de cdigo

279

valor id (1) (TR,,,) L1 tipo etiqueta localizada NO nm_cudrupla 1

(1) (2) (3) (4) (1) (2) (3) (4) (5) (1) (2) (3) (4) (5) (6)

(TR,,,) (:=.3,aa) (TR,1,,) (:=,5,,x) (TR,4,,) (:=.3,,a) (TR,4,,) (:=,5,,x) (:=,6,,y) (TR,4,,) (:=.3,,a) (TR,4,,) (:=,5,,x) (:=,6,,y) (TR,5,,)

valor id L1 tipo etiqueta localizada NO nm_cudrupla 3

valor id L1 tipo etiqueta localizada S nm_cudrupla 4

valor id L1 L2 tipo etiqueta etiqueta localizada S S nm_cudrupla 4 5

Figura 6.29. Generacin de cudruplas para un ejemplo con etiquetas e instrucciones GOTO.

Si se permiten etiquetas locales a bloques, puede aparecer el siguiente caso: L: ... { ... GOTO L; ...

En un caso como ste, la instruccin GOTO L es ambigua, ya que L puede referirse a la etiqueta externa (que podra haber sido localizada previamente, como en este ejemplo, o tal vez no), o tambin puede referirse a una etiqueta local del bloque que contiene a la instruccin. Esta ambigedad puede resolverse utilizando un compilador en dos pasos, o forzando a que las etiquetas se declaren como el resto de los identificadores. Una tercera forma de resolver la ambigedad sera tratar la etiqueta L que aparece en el bloque como si tuviese que ser local. Si al final del bloque se descubre que no ha sido definida, pasar a considerarse como global. La lista de cudruplas pendientes de completar debera entonces fundirse con la lista que corresponde a la etiqueta L global (si dicha etiqueta no ha sido localizada an). En el caso de que la etiqueta L global ya haya sido localizada, debern completarse las cudruplas pendientes de completar correspondientes a la etiqueta L local. Si la etiqueta L global no estaba en la tabla de smbolos del bloque externo, debe crearse en ella, y su lista de cudruplas pendientes de completar ser la misma que la de la etiqueta L local.

280

Compiladores e intrpretes: teora y prctica

Bucles
Para generar las cudruplas correspondientes a un bucle for son necesarias las cinco acciones semnticas que aparecen entre parntesis. <bucle> ::= for <id> = <n1> (1) , <n2> (2) <CD1> do <instr> end S5 <CD1> ::= , <n3> (3) | (4) El contenido de las acciones semnticas es el siguiente: (1): generar cudrupla (:=,n1,,id) i=cl_sig (2): generar cudrupla (TRG,,id,n2) generar cudrupla (TR,,,) (3): generar cudrupla (+,id,n3,id) generar cudrupla (TR,i,,) cudrupla[i+1][2]=cl.sig (4): generar cudrupla (+,id,1,id) generar cudrupla (TR,i,,) cudrupla[i+1][2]=cl.sig (5): generar cudrupla (TR,(i+2),,) cudrupla[i][2]=cl.sig La Figura 6.30 muestra el proceso de generacin de cudruplas para el bucle for x = n1,n2,n3 do a:=a+1; end Como puede apreciarse en la Figura 6.39, la accin semntica (4) no se ejecuta en este caso, porque dicha accin slo se ejecuta si no aparece el valor n3.

2 (1) (:=,1,,x) (2) (TRG, ,x,10) (3) (TR, ,,) (1) (2) (3) (4) (5) (:=,1,,x) (TRG, ,x,10) (TR,6,,) (+,x,2,x) (TR,2,,) (1) (2) (3) (4) (5) (6) (:=,1,,x) (TRG, ,x,10) (TR,6,,) (+,x,2,x) (TR,2,,) (+,a,1,a) (1) (2) (3) (4) (5) (6) (7) (:=,1,,x) (TRG,8,x,10) (TR,6,,) (+,x,2,x) (TR,2,,) (+,a,1,a) (TR,4,,)

(1) (:=,1,,x)

despus de ejecutar (1)

despus de ejecutar (2)

despus de ejecutar (3)

despus de generar cudruplas para <instr>

despus de ejecutar (5)

Figura 6.30. Generacin de cudruplas para la instruccin for x=n1,n2,n3 do a:=a+1; end.

Captulo 6. Generacin de cdigo

281

Generacin de cdigo ensamblador a partir de cudruplas


Una forma de generar cdigo ensamblador a partir de cudruplas consiste en implementar un procedimiento para cada uno de los operadores que pueden aparecer en la primera posicin de una cudrupla. Cada cudrupla se traducir en una llamada a uno de estos procedimientos. Los procedimientos realizarn llamadas al procedimiento GEN, que escribir en el fichero que contiene el cdigo ensamblador las instrucciones correspondientes, y a la funcin CAC, que cargar el valor de una variable en el acumulador, tal como se indic en la Seccin 6.1.1. Una cudrupla del tipo (+, O1, O2, R) se traduce a una llamada al procedimiento SUMA(O1, O2, R). SUMA (opd *x, opd *y, opd *z) { if (CAC (x, y)) GEN(ADD, EAX, y) else GEN (ADD, EAX, x); AC=z; } Este mismo procedimiento podra aplicarse a otras funciones didicas conmutativas, como la multiplicacin, en la que se sustituira ADD por MUL. Una cudrupla del tipo (-, O1, O2, R) se traduce a una llamada al procedimiento RESTA(O1, O2, R). RESTA (opd *x, opd *y, opd *z) { CAC (x, NULL); GEN(SUB, EAX, y) AC=z; } Este mismo procedimiento podra aplicarse a otras funciones didicas conmutativas, como la divisin, en la que se sustituira SUB por DIV. Para funciones mondicas, como el cambio de signo, podra aplicarse un procedimiento como el siguiente: NEG (opd *x, opd *z) { CAC (x, NULL); GEN (NEG EAX); AC=z; } Una cudrupla del tipo (@, O1,, R) se traduce a una llamada al procedimiento NEG(O1, R). Como ejemplo, en la Figura 6.31 aparecen las cudruplas correspondientes a la expresin a*((a*b+c)-c*d), as como el cdigo ensamblador generado a partir de ellas.

282

Compiladores e intrpretes: teora y prctica

Cudrupla (*,a,b,t1)

Llamada a CAC CAC(a,b)

Se genera MOV AC,A MUL AC,B ADD AC,C MOV T2,AC MOV AC,C MUL AC,D MOV T3,AC MOV AC,T2 SUB AC,T3 MUL AC,A

Valor de AC a t1 t2

(+,t1,c,t2) (*,c,d,t3)

CAC(t1,c) CAC(c,d)

c t3

(-,t2,t3,t4)

CAC(t2,NULL)

t2 t4 t5

(*,a,t4,t5)

CAC(a,t4)

Figura 6.31. Cdigo ensamblador para la expresin a*((a*b+c)-c*d).

6.3 Resumen
Este captulo describe el mdulo de generacin de cdigo de un compilador, cuyo objeto es generar el cdigo equivalente al programa fuente, escrito en un lenguaje diferente. En primer lugar, se describe el proceso de la generacin directa de cdigo ensamblador en un solo paso. Se utiliza un ensamblador tpico, aplicable a la familia 80x86 a partir del microprocesador 80386, en modo de funcionamiento de 32 bits. Se describe cmo se realiza la gestin de los registros de la mquina y cmo se genera cdigo para las expresiones, tanto aritmticas como de comparacin, y para la desreferenciacin de punteros. Adems, se analizan instrucciones de distintos tipos: asignaciones, entrada y salida de datos, condicionales, bucles y llamadas a funciones. Se dedica otro apartado a dos formas de cdigo intermedio: la notacin sufija y las cudruplas. Para ambas notaciones, se describe su generacin en anlisis ascendente y descendente. En relacin con la notacin de cudruplas, se estudia tambin con detalle su generacin para las instrucciones condicionales, las de salto a etiquetas y los bucles. En los compiladores de dos o ms pasos, a partir del cdigo intermedio se realiza la generacin del cdigo definitivo, por lo que en este captulo se describe tambin el proceso de generacin de dicho cdigo a partir de cada una de las dos notaciones consideradas.

6.4 Ejercicios
1. Convertir en cudruplas el programa C fac=1; for (i=0; i<n; i++) fac*=i+1;

Captulo 6. Generacin de cdigo

283

2.

Poner en notacin sufija la expresin (-a+2*b)/a+(c/d-1)/a*a

3.

Convertir en cudruplas el programa C fac=1; while (n>1) fac*=n;

4.

Poner en notacin sufija la expresin: (a-2/b)/a*a+(-c*d+3)/a

5.

Convertir en cudruplas el programa C int a,b,c,d,i; ... a = b+c; for (i=0; i<a; i++) d+=(b+c)*i;

6.

Dada la expresin a=(a+b)*c+3/(a+b) generar las cudruplas equivalentes.

7.

Poner en notacin sufija la expresin 2+((-a-b)*c+d)/(a*(-a-1))

8.

Construir las cudruplas equivalentes a las instrucciones siguientes: if (a=b) then do i:=1,n+1 a:=(-b)-a*7 end else a:=a+1

9.

Convertir a notacin sufija la expresin siguiente: (-b)-a*(x+7)2

10.

Dada la expresin if ((a+b)<(c*d)) a=a+b-(a+b)/(c*d) else a=c*d-(a+b)/(c*d) generar las cudruplas equivalentes.

11.

Poner en notacin sufija la expresin a+((-2-b)c+d)/(3*(-b-a))

284

Compiladores e intrpretes: teora y prctica

12.

Generar las cudruplas equivalentes para el siguiente programa: int f(int y) { int x,z; z=1; ...... if (y>0) for (x=1; x<y; ) { x*=2; z*=2; } else x=0; z*=2; return x; }

13.

Generar las cudruplas equivalentes para el siguiente programa: int x, y, z, m, n, p; ...... m = y + z; x = 1; while (x < n) { p =(y+z)*x; x++; }

14.

Generar las cudruplas equivalentes para el siguiente programa: int a = 2, b = 8, c = 4, d; for(i=0; i<5; i++){ a = a * (i* (b/c)); d = a * (i* (b/c)); }

15.

Generar las cudruplas equivalentes para el siguiente programa: int a; float b; ...... a = 4 + 3; a = 5; b = a + 0.7;

Captulo

Optimizacin de cdigo

La optimizacin de cdigo es la fase cuyo objetivo consiste en modificar el cdigo objeto generado por el generador de cdigo, para mejorar su rendimiento. Esta fase puede realizarse, bien en un paso independiente, posterior a la generacin de cdigo, o bien mientras ste se genera. Cuando los programadores escriben directamente cdigo en el lenguaje objeto (sea ste un lenguaje simblico o de alto nivel), pueden aplicar toda su experiencia y habilidad en la generacin de un cdigo suficientemente eficiente. La generacin de cdigo por parte de compiladores e intrpretes ha de ser automtica y general y, por ello, es difcil que mantenga la pericia del experto humano. Los sistemas automticos no llegan, en general, a realizar su trabajo con la misma calidad que los expertos humanos. Los beneficios de la automatizacin son distintos, y compensan con creces la disminucin inherente en la calidad del resultado: aplicacin del conocimiento en lugares y situaciones en las que no sera posible la presencia de un experto humano; incremento de la productividad; independencia respecto a factores subjetivos. Se sabe que la optimizacin absoluta es indecidible, es decir, no puede saberse con certeza si una versin concreta de cdigo objeto es la ms eficiente posible. El objetivo de esta fase slo puede ser, por tanto, proporcionar una versin que mejore en algo el cdigo generado. Otra peculiaridad de esta fase es que puede interferir en los objetivos de otras partes de los compiladores e intrpretes, como, por ejemplo, la depuracin. Los procesadores de lenguaje que permiten realizar depuraciones muestran al programador las instrucciones del programa fuente mientras el programa objeto se est ejecutando, permiten observar y modificar los valores que toman las variables del programa, as como continuar la ejecucin o detenerla de nuevo cuando se considere conveniente. Sin embargo, como resultado de la optimizacin, el cdigo asociado con algunas secciones del programa fuente podra desaparecer, lo que hara imposible su depuracin. El objetivo de este captulo es mostrar algunas tcnicas e ideas cuya aplicacin pueda dar lugar a alguna mejora en el cdigo objeto generado. Las especificaciones de los compiladores e intrpretes reales son las que determinan el diseo final de la estrategia de optimizacin.

286

Compiladores e intrpretes: teora y prctica

7.1 Tipos de optimizaciones


Las optimizaciones se pueden dividir en dos grandes grupos, en funcin de que se puedan aplicar nicamente en una mquina concreta, o en cualquiera.

7.1.1. Optimizaciones dependientes de la mquina


Para aplicarlas, la mquina debe proporcionar las herramientas necesarias. Se describirn los siguientes ejemplos de este tipo de optimizaciones: Minimizacin del uso de registros en mquinas en las que no se disponga de un conjunto de registros muy grande. Puede llegar a generarse cdigo que utilice slo un registro. Esta cuestin se ha analizado anteriormente, en la Seccin 6.1.1. Uso de instrucciones especiales de la mquina, que supongan una optimizacin respecto al uso de construcciones ms generales, presentes en todos los lenguajes de mquina. Reordenacin de cdigo: algunas arquitecturas son ms eficientes cuando las operaciones se ejecutan en un orden determinado. Modificando el cdigo para sacar provecho de ese orden se puede optimizar el programa objeto.

7.1.2. Optimizaciones independientes de la mquina


Mejoran la eficiencia sin depender de la mquina concreta utilizada. En este captulo se explicarn con detalle los siguientes ejemplos de este tipo de optimizacin: Ejecucin parcial del cdigo por parte del compilador, en lugar de retrasar su ejecucin al programa objeto. Eliminacin de cdigo que resulta redundante, porque previamente se ha ejecutado un cdigo equivalente. Cambio de orden de algunas instrucciones, que puede dar lugar a un cdigo ms eficiente. Es frecuente que los bucles sean poco eficientes, porque se ejecuten en su cuerpo instrucciones que podran estar fuera de l, o porque la reiteracin inherente al bucle multiplique la ineficiencia causada por el uso de operaciones costosas, cuando podran utilizarse otras menos costosas y equivalentes.

7.2 Instrucciones especiales


Algunas mquinas tienen instrucciones especiales, cuyo objetivo es facilitar la codificacin y acelerar la ejecucin, mediante el uso de operadores de mayor nivel de abstraccin. Por ejemplo: Las instrucciones de la mquina TRT en la arquitectura IBM 390, y XLAT en la arquitectura INTEL, permiten realizar la traduccin de un sistema de codificacin (como, por ejemplo,

Captulo 7. Optimizacin de cdigo

287

ASCII) a otro diferente en una sola instruccin de la mquina. Estas instrucciones pueden utilizarse tambin para buscar la primera aparicin de un valor en una serie de datos. La instruccin MOV en la arquitectura IBM 390 permite copiar bloques de memoria de hasta 255 caracteres. De igual manera, la instruccin REP en la arquitectura INTEL permite copiar, comparar o introducir informacin en bloques de memoria, utilizando como registros ndices ESI y EDI. La instruccin TEST en INTEL permite realizar fcilmente varias comparaciones booleanas simultneas. Por ejemplo, la comparacin if (x&4 || x&8), escrita en el lenguaje C, se puede traducir as: TEST x,12 JZ L ... L:

7.3 Reordenacin de cdigo


En algunas circunstancias, la reordenacin de las instrucciones del programa fuente permite reducir el tamao o la complicacin del cdigo objeto. Esto pasa, por ejemplo, cuando hay que calcular varias veces el mismo resultado intermedio: generndolo una sola vez antes de utilizarlo, se puede obtener una versin optimizada. Ejemplo En muchas mquinas, la multiplicacin en punto fijo de dos operandos enteros da como resultado un operando de longitud doble, mientras la divisin acta sobre un operando de longitud doble 7.1 y otro de longitud sencilla para generar un cociente y un resto de longitud sencilla. Una simple reordenacin de las operaciones puede dar lugar a optimizaciones. Supngase un lenguaje fuente en el que la asignacin se realice con el smbolo = y los operadores * , / y % realicen, respectivamente, las operaciones multiplicacin, cociente entero y resto de la divisin. Se supondr que el objetivo de la traduccin es un lenguaje de la mquina o simblico del tipo INTEL, en el que: La instruccin CDQ extiende el signo del registro EAX al registro EDX, para que el operando pase a ocupar el par EDX:EAX. El operador IMUL realiza la multiplicacin entera del registro EAX y una posicin de memoria. El producto se almacena en el par de registros EDX:EAX. El operador IDIV realiza la divisin entera entre el par EDX:EAX y una posicin de memoria. El cociente de la divisin se almacena en el registro EAX y el resto en EDX. El smbolo ; inicia comentarios que terminan con el final de la lnea. Sea la expresin a=b/c*d. Un generador de cdigo poco sofisticado podra generar el siguiente programa objeto equivalente:

288

Compiladores e intrpretes: teora y prctica

MOV CDQ IDIV IMUL MOV

EAX,B EAX,C EAX,D A,EAX

;1 EAX B ;2 EDX:EAX EAX (extensin de signo) ;3 EAX B/C, EDX B%C ;4 EAX(B/C)*D ;5 A EAX

Sin embargo, si la expresin anterior se reordena, aprovechando que la multiplicacin y la divisin son asociativas, podramos generar cdigo para calcular a=b*d/c: MOV IMUL IDIV MOV EAX,B EAX,D EAX,C A,EAX ;1 EAX B ;4 EDX:EAXB*D ;3 EAX B*D/C ;5 A EAX

Este cdigo tiene una instruccin menos que el anterior. Ejemplo En el lenguaje del ejemplo anterior se podran escribir las siguientes instrucciones: 7.2 a=b/c; d=b%c; El siguiente cdigo, escrito en este lenguaje, es equivalente a dicho fragmento fuente: MOV CDQ IDIV MOV MOV CDQ IDIV MOV EAX,B EAX,C A,EAX EAX,B EAX,C D,EDX ;1 EAX B ;2 EDX:EAX EAX (extensin de signo) ;3 EAX B/C, EDX B%C ;4 AEAX(B/C) ;5 EAX B ;6 EDX:EAX EAX (extensin de signo) ;7 EAX B/C, EDX B%C ;8 D EDX(B%C)

El anlisis de este fragmento muestra que las tres primeras instrucciones hacen exactamente lo mismo que las instrucciones quinta, sexta y sptima. Adems, tras la tercera instruccin ya est el resto de la divisin en el registro EDX. Podra aprovecharse esta situacin para reducir el cdigo de la siguiente manera: MOV CDQ IDIV MOV MOV EAX,B EAX,C A,EAX D,EDX ;1 EAX B ;2 EDX:EAX EAX (extensin de signo) ;3 EAX B/C, EDX B%C ;4 AEAX(B/C) ;8 D EDX(B%C)

7.4 Ejecucin en tiempo de compilacin


En algunas secciones del cdigo, casi siempre relacionadas con las expresiones aritmticas y las conversiones de tipo, se puede elegir entre generar el cdigo objeto que realizar todos los clcu-

Captulo 7. Optimizacin de cdigo

289

los o realizar en el compilador parte de ellos, de forma que el cdigo generado tenga que realizar menos trabajo y resulte, por tanto, ms eficiente. Para realizar esta optimizacin, es necesario que el compilador lleve cuenta, de forma explcita y siempre que sea posible, del valor que toman los identificadores en cada momento. Esto puede hacerse directamente en la tabla de smbolos o en una estructura de datos al efecto. Para asegurar que los resultados son correctos, el compilador debe mantener la tabla permanentemente actualizada.

7.4.1. Algoritmo para la ejecucin en tiempo de compilacin


Para explicar esta optimizacin, se describir un algoritmo aplicable a cudruplas. Dado que las cudruplas son una abstraccin de los lenguajes ensambladores y de la mquina, podrn usarlo casi todos los compiladores, aunque no generen esta representacin intermedia: Se supondr que se dispone de una tabla (T), en la que se conservar la informacin de las variables del programa fuente: los identificadores y sus valores. Se selecciona el conjunto de cudruplas objeto de la optimizacin. Casi siempre ser el que corresponda a alguna expresin aritmtica. Se tratan todas las cudruplas en el orden en el que aparecen y se aplica reiteradas veces el siguiente tratamiento, segn su tipo. Para aplicar el tratamiento, hay que utilizar la Tabla 7.1.
Tabla 7.1 Estructura de la cudrupla 1. (op, op1, op2, res), op1 es un identificador y (op1, v1) est en T 2. (op, op1, op2, res), op2 es un identificador y (op2, v) est en la tabla T 3. (op, v1, v2, res), donde v1 y v2 son valores constantes o nulos Tratamiento Se sustituye en la cudrupla op1 por v1 Se sustituye en la cudrupla op2 por v2 Si al evaluar v1 op v2 se produce un error: Se avisa del mismo1 y se deja la cudrupla original. En otro caso: Se elimina la cudrupla. Se elimina de T el par (res, v), si existe. Se aade a T el par (res, v1 op v2). Se elimina de T el par (res, v), si existe. Si v1 es un valor constante, se aade a T el par (res, v1).

4. (=, v1, , res)

1 Lo nico correcto es avisar, ya que puede ser que la cudrupla que presenta el error en realidad nunca se ejecute. Por ejemplo, en la instruccin if (false) a=1/0;.

290

Compiladores e intrpretes: teora y prctica

El tipo de la cudrupla determina el tratamiento adecuado. Hay que consultar la Tabla 7.1 en el orden en que aparecen sus filas y elegir el tratamiento cuya condicin se satisface primero. Es decir, en caso de que sea aplicable ms de un tratamiento, hay que elegir el que est ms arriba en la tabla. El resultado de cada cudrupla se trata reiteradamente hasta que no se produce ningn cambio. Ejemplo Se va a aplicar la optimizacin de ejecucin en tiempo de compilacin al siguiente bloque de programa, escrito en el lenguaje C: 7.3 { int i; float f; i=2+3; i=4; f=i+2.5;

La siguiente secuencia de cudruplas es equivalente al bloque anterior. En ellas se utiliza el operador CIF, que significa convertir entero (integer) en real (float). (+, (=, (=, (CIF, (+, (=, 2, t1, 4, i, t2, t3, 3, , , , 2.5, , t1) i) i) t2) t3) f)

La Tabla 7.2 muestra los pasos del algoritmo para este caso:
Tabla 7.2 Cudruplas T {} (+, 2, 3, t1) {(t1,5)} Caso 3: 2+3 se evala sin errores. Su resultado es 5. Se elimina la cudrupla. No hay ningn par para t1 en T. Se aade a T el par (t1, 5) en q. Caso 2: T contiene el par (t1, 5) Se sustituye en la cudrupla t1 por 5. (=, 5, , i) Caso 4: T no contiene ningn par para i. 5 es un valor constante, se aade (i, 5) a T. Tratamiento

(=,t1, , i)

{(t1,5), (i,5)}

Captulo 7. Optimizacin de cdigo

291

Tabla 7.2 (continuacin) Cudruplas (=, 4, , i) T {(t1,5), (i,4)} {(t1,5), (i,4), (t2,4.0)} Tratamiento Caso 4: Se elimina de T el par (i, 5). 4 es un valor constante, se aade (i, 4) a T. Caso 1: Se sustituye en la cudrupla i por 4. (CIF, 4, , t2) Caso 3: CIF 4 se evala sin errores, su resultado es 4.0. Se elimina la cudrupla. No hay ningn par para t2 en T. Se aade a T el par (t2, 4.0) en. Caso 1: Se sustituye en la cudrupla t3 por 4.0. (+, 4.0, 2.5, t3) Caso 3: 4.0+2.5 se evala sin errores, su resultado es 6.5. Se elimina la cudrupla. No hay ningn par para t3 en T. Se aade a T el par (t3, 6.5) en. Caso 1: Se sustituye en la cudrupla t3 por 6.5. (=, 6.5, , f) Caso 4: No hay ningn par para f en T. 6.5 es un valor constante, se aade (f, 6.5) a T.

(CIF, i, , t2)

(+,t2, 2.5, t3)

{(t1,5), (i,4), (t2,4.0), (t3,6.5)}

(=,t3, , f)

{(t1,5), (i,4), (t2,4.0), (t3,6.5), (f,6.5)}

La Tabla 7.3 describe juntos los resultados de cada paso, que estn resaltados en la columna Tratamiento:
Tabla 7.3 Cudrupla original (+, 2, 3, t1) (=, t1, , i) (=, 4, , i) (CIF, i, , t2) (+, t2, 2.5, t1) (=, t3, , f) Resultado Eliminada (=, 5, , i) (=, 4, , i) Eliminada Eliminada (=, 6.5, , f)

292

Compiladores e intrpretes: teora y prctica

La comparacin de la secuencia original de cudruplas con el resultado del algoritmo muestra claramente la optimizacin obtenida.

7.5 Eliminacin de redundancias


Esta optimizacin tambin se relaciona, casi siempre, con el cdigo generado para las expresiones aritmticas. Como se ha visto en el Captulo 6, para generar cdigo para una expresin es necesario dividirla en una secuencia de muchos clculos intermedios. El lugar donde se van almacenando los resultados parciales debe expresarse de forma explcita en cada uno de los pasos. Es frecuente que muchas operaciones intermedias resulten redundantes, por ejemplo, porque se calcule de nuevo algn dato intermedio que ya est disponible en una variable auxiliar. Ejemplo Considrese el siguiente fragmento de cdigo escrito en el lenguaje de programacin C: 7.4 int a,b,c,d; a = a+b*c; d = a+b*c; b = a+b*c; Este ejemplo es ideal para resaltar redundancias y estudiar tcnicas para eliminarlas, ya que a las tres variables se les asigna el resultado de expresiones muy similares. Con los algoritmos de generacin de cudruplas explicados en el Captulo 6 podra obtenerse la secuencia de cudruplas de la Tabla 7.4.

Tabla 7.4 int a,b,c,d; a = a+b*c; (*, b, c, t1) (+, a, t1, t2) (=, t2, , a) (*, b, c, t3) (+, a,t3, t4) (=, t4, , d) (*, b, c, t5) (+, a,t5, t6) (=, t6, , b)

d = a+b*c;

b = a+b*c;

Obsrvese que a cada instruccin le corresponde un grupo de tres cudruplas con la misma estructura: La primera almacena el valor de b*c en una nueva variable temporal.

Captulo 7. Optimizacin de cdigo

293

La segunda almacena en una nueva variable temporal el valor de la suma con la variable a de la variable temporal creada en la primera cudrupla. La tercera asigna el resultado, contenido en la variable temporal creada en la segunda cudrupla, a la variable correspondiente, segn el cdigo fuente. En este ejemplo, es fcil identificar las redundancias mediante la simple observacin del cdigo: en la primera instruccin son necesarias las tres cudruplas. En la segunda, puesto que b y c no han cambiado de valor (aunque a s ha cambiado), en lugar de calcular de nuevo el valor de su producto, se puede tomar directamente de la variable auxiliar t1. Para la tercera cudrupla, no ha cambiado el valor de ninguna de las tres variables, por lo que el valor de la expresin completa puede tomarse directamente de la variable t4. La secuencia de cudruplas tras esta optimizacin sera la que muestra la Tabla 7.5.
Tabla 7.5 (*, b, c, t1) (+, a,t1, t2) (=, t2, , a) (+, a,t1, t4) (=, t4, , d)

(=, t4,

, b)

El objetivo de esta seccin es proporcionar un algoritmo que automatice la identificacin y reduccin de las redundancias.

7.5.1. Algoritmo para la eliminacin de redundancias


Para ello se utiliza el concepto de dependencia: los identificadores dependen de la cudrupla en la que se les asigna valor; las cudruplas dependen de sus operadores. El concepto de dependencia relaciona, por tanto, las cudruplas y los identificadores, teniendo en cuenta la existencia de las variables auxiliares para los resultados. Como se explic en el Captulo 6, la tcnica para asegurar su gestin correcta consiste en llevar un contador de variables auxiliares, que se incrementa cada vez que se necesita una variable auxiliar nueva. El algoritmo tiene los siguientes pasos: 1. Se asigna la dependencia inicial 1 a cada variable de la tabla de smbolos del compilador. 2. Se numeran las cudruplas que van a ser tratadas por este algoritmo. A la primera de ellas le corresponde el nmero 0.

294

Compiladores e intrpretes: teora y prctica

3. Desde i=0 y mientras i < nmero total de cudruplas, se aplica el siguiente proceso: a. La dependencia de la cudrupla nmero i se calcula sumando 1 al mximo de las dependencias de sus operandos. La dependencia de los operandos constantes se supondr igual a -1. Si la cudrupla nmero i tiene como resultado el identificador id, la dependencia del identificador id se hace coincidir con el nmero de la cudrupla. Se estudia si la cudrupla i es equivalente a otra j anterior (con j<i). Lo es si se cumplen las siguientes condiciones: d. Para las cudruplas que no sean asignaciones, todos sus campos coinciden, excepto el del resultado, y tambin coinciden sus dependencias. Para las cudruplas de asignaciones, la componente del resultado tambin tiene que coincidir.

b. c.

En ese caso se realizan los siguientes cambios en el conjunto de cudruplas: La cudrupla i es sustituida por una nula que apunte a la cudrupla anterior a la que es equivalente. Para ello se utiliza el operador COMO. Estas cudruplas slo se utilizan durante el algoritmo, ya que posteriormente son eliminadas, pues no se necesita generar cdigo para ellas. La nueva cudrupla es (COMO, j, , ). En adelante, en todas las cudruplas en las que aparezca la variable del resultado de la cudrupla i, se sustituye sta por el de la cudrupla j.

e.

Se incrementa en 1 el valor de i.

Obsrvese que, para la ltima sustitucin, anterior al paso e, no es necesario realizar un recorrido por las cudruplas siguientes a la i. Este cambio puede incorporarse al tratamiento de cada cudrupla, siempre que se aada lo siguiente antes del paso a: Cualquiera de las variables que coincida con el resultado de una cudrupla sustituida por otra de tipo (COMO, j, , ) se reemplaza por el resultado de la cudrupla j. A continuacin se aplica el algoritmo al Ejemplo 7.4. Hay cuatro variables en la tabla de smbolos: a, b, c y d. Se les asigna inicialmente una dependencia igual a 1. Se trata la cudrupla nmero 0 (vase la Tabla 7.6).
Tabla 7.6
i Operador Operando Operando Resultado Dependencia Variable Dependencia

t1

a b c d

1 1 1 1

Captulo 7. Optimizacin de cdigo

295

Las dependencias de sus operandos, b y c, son iguales a 1, por lo que le corresponde una dependencia de 1+1=0. Se aade a la tabla el identificador resultado (t1) con una dependencia que coincide con el nmero de la cudrupla en la que toma valor (0). Vase la Tabla 7.7.

Tabla 7.7
i Operador Operando Operando Resultado Dependencia Variable Dependencia

t1

a b c d t1

1 1 1 1 0

Se trata ahora la cudrupla nmero 1. Las dependencias de sus operandos, a y t1, son respectivamente 1 y 0, por lo que se asigna a la cudrupla una dependencia de 0+1=1. Se aade a la tabla el identificador resultado (t1) con una dependencia igual al nmero de la cudrupla (1). Vase la Tabla 7.8.

Tabla 7.8
i Operador Operando Operando Resultado Dependencia Variable Dependencia

0 1

* +

b a

c t1

t1 t2

0 1

a b c d t1 t2

1 1 1 1 0 1

Se trata la cudrupla nmero 2. La dependencia de su operando, t2, es 1, por lo que se asigna a la cudrupla una dependencia de 2. El resultado se asigna a la variable a, por lo que se modifica su dependencia con el nmero de la cudrupla. Vase la Tabla 7.9.

296

Compiladores e intrpretes: teora y prctica

Tabla 7.9
i Operador Operando Operando Resultado Dependencia Variable Dependencia

0 1 2

* + =

b a t2

c t1

t1 t2 a

0 1 2

a b c d t1 t2

2 1 1 1 0 1

Se trata la cudrupla nmero 3. Las dependencias de b y c son iguales a 1, por lo que se asigna a la cudrupla una dependencia de 0. Se aade a la tabla el identificador resultado (t3) con una dependencia igual al nmero de la cudrupla (3). Vase la Tabla 7.10.

Tabla 7.10
i Operador Operando Operando Resultado Dependencia Variable Dependencia

0 1 2 3

* + = *

b a t2 b

c t1

t1 t2 a

0 1 2 0

a b c d t1 t2 t3

2 1 1 1 0 1 3

t3

Antes de terminar con esta cudrupla, se observa que todos sus datos, excepto el identificador del resultado, coinciden con los de la cudrupla nmero 0. Se sustituye la cudrupla por (COMO, 0, , ). Vase la Tabla 7.11. Se conserva entre parntesis la antigua variable resultado de la cudrupla 3. En adelante, si alguna cudrupla utiliza como operando la variable t3, la aparicin de esta variable tendr que ser reemplazada por t1, identificador del resultado de la cudrupla 0, que aparece en la cudrupla auxiliar(COMO, 0, , ). Obsrvese que esta informacin tambin podra deducirse de los datos sobre las dependencias de las variables, ya que la de t3 coincide con el nmero de la cu-

Captulo 7. Optimizacin de cdigo

297

Tabla 7.11
i Operador Operando Operando Resultado Dependencia Variable Dependencia

0 1 2 3

* + = COMO

b a t2 0

c t1

t1 t2 a (t3)

0 1 2 0

a b c d t1 t2 t3

2 1 1 1 0 1 3

drupla donde tom valor. Al acceder a esa cudrupla, se constata que es necesario consultar la nmero 0 para usar su resultado en lugar de t3. Al procesar la cudrupla nmero 4, se observa que uno de sus operadores es t3. Ya se ha dicho anteriormente que esta variable debe sustituirse por t1. Las dependencias de a y t1 son respectivamente 2 y 0, por lo que a la cudrupla se le asigna 3 como dependencia. Se aade la variable del resultado (t4) con el nmero de la cudrupla como dependencia. Vase la Tabla 7.12.
Tabla 7.12
i Operador Operando Operando Resultado Dependencia Variable Dependencia

0 1 2 3 4

* + = COMO +

b a t2 0 a

c t1

t1 t2 a (t3)

0 1 2 0 3

a b c d t1 t2 t3 t4

2 1 1 1 0 1 3 4

t3 t1

t4

Se procesa la cudrupla nmero 5. Su nico operando tiene una dependencia igual a 4, por lo que se le asigna una dependencia de 5. Su resultado es la variable d, por lo que se cambia su dependencia por el nmero de la cudrupla (5). Vase la Tabla 7.13.

298

Compiladores e intrpretes: teora y prctica

Tabla 7.13
i Operador Operando Operando Resultado Dependencia Variable Dependencia

0 1 2 3 4 5

* + = COMO + =

b a t2 0 a t4

c t1

t1 t2 a (t3)

0 1 2 0 3 5

a b c d t1 t2 t3 t4

2 1 1 5 0 1 3 4

t1

t4 d

A la cudrupla 6 se le asigna una dependencia igual a 0 porque sus operandos tienen dependencia 1. Se aade a la tabla la variable de su resultado (t5) con una dependencia igual al nmero de la cudrupla. Vase la Tabla 7.14.
Tabla 7.14
i Operador Operando Operando Resultado Dependencia Variable Dependencia

0 1 2 3 4 5 6

* + = COMO + = *

b a t2 0 a t4 b

c t1

t1 t2 a (t3)

0 1 2 0 3 5 0

a b c d t1 t2 t3 t4 t5

2 1 1 5 0 1 3 4 6

t1

t4 d

t5

Antes de terminar con su proceso, se observa que la cudrupla 6 es como la 0, ya que coinciden todas sus informaciones excepto la variable del resultado. Se sustituye la cudrupla 6 por (COMO, 0, ,). En adelante, las apariciones de la variable t5 sern reemplazadas por t1 (el resultado de la cudrupla 0). Vase la Tabla 7.15.

Captulo 7. Optimizacin de cdigo

299

Tabla 7.15
i Operador Operando Operando Resultado Dependencia Variable Dependencia

0 1 2 3 4 5 6 6

* + = COMO + = COMO *

b a t2 0 a t4 0 b

c t1

t1 t2 a (t3)

0 1 2 0 3 5 0 0

a b c d t1 t2 t3 t4 t5

2 1 1 5 0 1 3 4 6

t1

t4 d (t5)

t5

En la cudrupla 7 es necesario realizar ese cambio. Sus operandos, que pasan a ser a y t1, tienen una dependencia mxima de 2, por lo que se asigna a la cudrupla una dependencia igual a 3. Se aade a la tabla de smbolos la variable resultado (t6) con el nmero de la cudrupla como dependencia. Vase la Tabla 7.16.

Tabla 7.16
i Operador Operando Operando Resultado Dependencia Variable Dependencia

0 1 2 3 4 5 6 7

* + = COMO + = COMO +

b a t2 0 a t4 0 a

c t1

t1 t2 a (t3)

0 1 2 0 3 5 0 3

a b c d t1 t2 t3 t4 t5 t6

2 1 1 5 0 1 3 4 6 7

t1

t4 d (t5)

t5 t1

t6

300

Compiladores e intrpretes: teora y prctica

Antes de terminar con ella, se observa que es como la cudrupla 4. Se realiza el cambio. En la cudrupla 8 aparece la variable resultado de la cudrupla 7 original, que ha de ser cambiada por la de la cudrupla 4 (t4) que tiene como dependencia 4, por lo que se asigna a la cudrupla una dependencia de 5. Se cambia la dependencia de b, que es su variable resultado, asignndole el nmero de la cudrupla. Vase la Tabla 7.17.
Tabla 7.17
i Operador Operando Operando Resultado Dependencia Variable Dependencia

0 1 2 3 4 5 6 7 8

* + = COMO + = COMO COMO =

b a t2 0 a t4 0 4 t6 t4

c t1

t1 t2 a (t3)

0 1 2 0 3 5 0 3 5

a b c d t1 t2 t3 t4 t5 t6

2 8 1 5 0 1 3 4 6 7

t1

t4 d (t5) (t6) b

De esta forma, la secuencia de cudruplas sin redundancias queda como se muestra a continuacin: (*,b,c,t1) (+,a,t1,t2) (=,t2,,a) (+,a,t1,t4) (=,t4,,d) (=,t4,,b) Que coincide con el resultado conseguido a mano.

7.6 Reordenacin de operaciones


Se puede aumentar la eficiencia del cdigo si se tiene en cuenta la conmutatividad y la asociatividad de las operaciones que aparecen en una expresin aritmtica. Entre otras cosas, se puede

Captulo 7. Optimizacin de cdigo

301

adoptar un orden preestablecido para los operandos de las expresiones, que permita identificar subexpresiones comunes; maximizar el uso de operaciones mondicas para aumentar la probabilidad de que aparezcan operaciones equivalentes; o reutilizar variables para los resultados intermedios, de forma que se precise el nmero mnimo de ellas.

7.6.1. Orden cannico entre los operandos de las expresiones aritmticas


Se puede seguir el siguiente orden al escribir las expresiones aritmticas: 1. 2. 3. 4. Trminos que no sean variables ni constantes. Variables indexadas (arrays) en orden alfabtico. Variables sin indexar en orden alfabtico. Constantes.

Esto puede facilitar la localizacin de subexpresiones comunes y la aplicacin de otras optimizaciones. Ejemplo Considrense, como ejemplo, las siguientes expresiones aritmticas: 7.5 a=1+c+d+3; es equivalente a a=c+d+1+3; b=d+c+2; es equivalente a b=c+d+2; Al considerar la segunda versin, se puede reducir el nmero de operaciones al observar que hay una subexpresin comn: c+d. Sin embargo, esta tcnica no asegura siempre el xito, como puede verse en el siguiente ejemplo. Ejemplo Como en el caso anterior, considrense las siguientes expresiones: 7.6 a=1+c+d+3; es equivalente a a=c+d+1+3; b=d+c+c+d; es equivalente a b=c+c+d+d; En este caso, existe una expresin comn (c+d), pero el algoritmo no es capaz de identificarla. Por lo tanto, el alcance de estas optimizaciones es relativo.

7.6.2. Aumento del uso de operaciones mondicas


Esta optimizacin tiene tambin un alcance limitado. Se trata de identificar situaciones como la que muestra el siguiente ejemplo. Ejemplo Considrese la siguiente secuencia de instrucciones: 7.7 a=c-d; b=d-c;

302

Compiladores e intrpretes: teora y prctica

Podra obtenerse la siguiente secuencia equivalente de cudruplas: /* a=c-d; */ (-, c, d, t1) (=, t1, , a) /* b=d-c; */ (-, d, c, t2) (=, t2, , b) Si se analiza la segunda operacin, su primera cudrupla deja en la variable t2 el mismo valor que contiene la variable t1, salvo que tiene el signo contrario. Gracias a esto, se podra obtener la siguiente versin en la que se resalta el nico cambio: /* a=c-d; */ (-, c, d, t1) (=, t1, , a) /* b=d-c; */ (-, t1, , t2) (=, t2, , b) En este caso no se reduce el nmero de cudruplas. La optimizacin consiste en que las operaciones mondicas son, en general, menos costosas que las binarias para la unidad aritmticolgica.

7.6.3. Reduccin del nmero de variables intermedias


Se ha explicado en el Captulo 6 que las variables intermedias que aparecen en las cudruplas acaban siendo, en el cdigo objeto, asignadas a registros, posiciones en la pila del sistema o en la memoria (variables estticas). Todos estos recursos son limitados. El sistema ofrece un nmero no muy grande de registros de propsito general de acceso muy rpido. El tamao de la pila es mayor que el nmero de registros, aunque tambin est limitado, pero el acceso a ella es ms lento. El espacio mayor es el de la memoria esttica, como tambin es mayor el tiempo necesario para acceder a l. Lo ideal sera, por tanto, que el cdigo objeto slo utilizara registros. Esto no es siempre posible: al compilar expresiones aritmticas complicadas, puede necesitarse un nmero de variables intermedias mayor que el de registros. Restringir el nmero de variables intermedias puede permitir que la generacin de cdigo explote formas de almacenamiento de acceso ms rpido. Ejemplo Considrense las dos versiones equivalentes de la siguiente expresin aritmtica: 7.8 (a*b)+(c+d) y ((a*b)+c)+d Es fcil comprobar que la propiedad asociativa de la suma asegura la equivalencia. Esto posibilita dos secuencias de cudruplas tambin equivalentes:

Captulo 7. Optimizacin de cdigo

303

(*,a,b,t1) (+,c,d,t2) (+,t1,c,t1)

(*,a,b,t1) (+,t1,t2,t1) (+,t1,d,t1)

La segunda variante utiliza una variable menos que la primera. Ejemplo A continuacin se presenta una situacin similar. La propiedad conmutativa permite afirmar que las dos expresiones siguientes son equivalentes: 7.9 (a+b)+(c*d) y (c*d)+(a+b) Como en el ejemplo anterior, se puede obtener, a partir de cada una de ellas, una secuencia distinta de cudruplas: (+,a,b,t1) (*,c,d,t2) (+,t1,t2,t1) y (*,c,d,t1) (+,a,t1,t1) (+,t1,b,t1)

Y la conclusin es la misma: la segunda versin utiliza una variable auxiliar menos que la primera. Algoritmo para el clculo del nmero mnimo de variables auxiliares que necesita una expresin La reflexin del apartado anterior justifica este algoritmo, que slo determina el nmero mnimo de variables auxiliares que necesita la expresin, pero no describe cmo generar las cudruplas que las usen. El algoritmo tiene los siguientes pasos: 1. Construir el grafo (o el rbol) de la expresin. La complejidad y la estructura de la expresin determinarn si es necesario un grafo o basta con un rbol para representarla. Los rboles pueden considerarse casos particulares de los grafos, por lo que en el resto del algoritmo se mencionarn slo los grafos. 2. Marcar las hojas del grafo con el valor 0. 3. Recorrer el grafo (desde las hojas hacia los padres) marcando los nodos: a. b. Si todos los hijos de un nodo tienen el mismo valor (j), se marca el nodo con el valor j+1. En otro caso, marcar el nodo padre con el valor mximo de sus hijos.

4. La etiqueta de la raz del grafo es el nmero de variables auxiliares que necesita la expresin. Ejemplo La Figura 7.1 muestra el resultado del algoritmo para dos parejas de expresiones equivalentes: 7.10 (a*b)+(c+d) y ((a*b)+c)+d (a+b)+(c*d) y a+(c*d)+b La primera expresin de cada pareja necesita 2 variables auxiliares, la segunda 1.

304

Compiladores e intrpretes: teora y prctica

(a*b)+(c+d) + 2 * 1 a 0 b 0 c 0 + 1 d 0 a 0 (a+b)+(c*d) + 2 + 1 a 0 b 0 c 0 * 1 d 0 a 0 * 1

((a*b)+c)+d + 1 + 1 c 0 b 0 a+(c*d)+b + 1 + 1 * 1 c 0 d 0 b 0 d 0

Figura 7.1. Ejemplos de aplicacin del algoritmo de determinacin del nmero mnimo de variables auxiliares.

7.7 Optimizacin de bucles


Los bucles son una parte del cdigo muy propensa a la ineficiencia. Su naturaleza multiplica la diferencia de rendimiento entre dos versiones de cdigo equivalentes, que fuera de un bucle no presentaran una mejora significativa. Por esta razn es importante generar el cdigo de manera cuidadosa, para introducir en los cuerpos de los bucles slo lo que sea estrictamente necesario, en versiones que minimicen el coste para la mquina. El primer enfoque identifica los invariantes del bucle; el segundo se llama reduccin de fuerza. Una operacin es invariante respecto a un bucle si ninguno de los operandos de los que depende cambia de valor durante la ejecucin del bucle. La optimizacin consiste en sacar la operacin fuera del bucle. Para estudiar la reduccin de fuerza, ser conveniente considerar un ejemplo previo. Ejemplo Considrese el siguiente bucle, escrito en el lenguaje C: 7.11 for (i=a; i<c; i+=b) {... d=i*k; ...}

Captulo 7. Optimizacin de cdigo

305

Se supone que: b y k no se modifican dentro del cuerpo del bucle. i, variable del bucle, slo se modifica para incrementarla. La nica modificacin del valor de la variable d dentro del cuerpo del bucle es la que se muestra. Se puede comprobar que los valores que toma la variable d son los siguientes: a*k (a+b)*k (a+2*b)*k Es posible obtener una versin equivalente del bucle, que realice menos trabajo en cada iteracin, reduciendo el clculo del valor siguiente de la variable d a un incremento, que se le sumar como si se tratase de una variable del bucle (como la variable i): d=a*k; t1=b*k; for (i=a; i<c; i+=b, d+=t1) {...} Obsrvese: La aparicin de una nueva variable (t1), que se hace cargo de parte del clculo del valor de d. La conversin de d en una variable del bucle, que en lugar de calcularse por completo en el cuerpo del bucle simplemente se incrementa. Que los valores que recibe la variable d en cada ejecucin del bucle coinciden con los indicados anteriormente. La optimizacin consiste en que el clculo que se repite dentro del bucle es una suma, en lugar de un producto, y los productos suelen ser ms costosos para el sistema. Esta tcnica se puede generalizar, como muestra el siguiente algoritmo.

7.7.1. Algoritmo para la optimizacin de bucles mediante reduccin de fuerza


El algoritmo distingue tres partes bien diferenciadas en el bucle: Inicializacin, en la que se asigna el valor inicial a las variables que se utilizan dentro del bucle. Incremento, en la que se actualiza el valor de las variables del bucle. Cuerpo, en la que se realiza el resto de las acciones, que no tienen cabida en las dos partes anteriores.

306

Compiladores e intrpretes: teora y prctica

Ejemplo En el bucle obtenido aplicando la reduccin de fuerza al Ejemplo 7.11: 7.12 d=a*k; t1=b*k; for (i=a; i<c; i+=b, d+=t1) {...} La inicializacin contiene las siguientes instrucciones: d=a*k; t1=b*k; i=a; El incremento contiene las dos instrucciones que aparecen en la seccin correspondiente de la cabecera del bucle: i+=b, d+=t1. El cuerpo est representado por los puntos suspensivos entre las llaves: {...}. El algoritmo describe ciertas transformaciones en las cudruplas del cuerpo del bucle original, dependiendo de su tipo, y distribuye el trabajo correspondiente entre las secciones de inicializacin y de incremento. Las transformaciones se basan en los siguientes conceptos: Variable de bucle: es aquella cuyo nico tratamiento dentro del bucle consiste en recibir un valor en la inicializacin y ser modificada en el incremento. Invariante del bucle: es la variable que no se modifica dentro del bucle. Incremento: es la expresin que se suma a una variable del bucle en la seccin de incremento. El algoritmo toma como entrada la secuencia de cudruplas del cuerpo del bucle, y las procesa en el orden en el que aparecen, realizando el siguiente tratamiento: Si la cudrupla tiene la estructura (*,variable_bucle,invariante_bucle,variable_resultado) Se elimina la cudrupla del bucle. Se aaden a la parte de inicializacin las siguientes cudruplas: (*,variable_bucle,invariante_bucle,variable_resultado) (*,incremento,invariante_bucle,var_temp_nueva) Se aade a la parte de incremento la siguiente cudrupla: (+,variable_resultado,var_temp_nueva,variable_resultado) Si la cudrupla tiene la estructura (+,variable_bucle,invariante_bucle,variable_resultado) Se elimina la cudrupla del bucle. Se aade a la parte de inicializacin la siguiente cudrupla: (+,variable_bucle,invariante_bucle,variable_resultado)

Captulo 7. Optimizacin de cdigo

307

Se aade a la parte de incremento la siguiente cudrupla: (+,variable_resultado,incremento,variable_resultado) Ejemplo Considrese el siguiente bucle escrito en el lenguaje de programacin C: 7.13 for (i=0; i<10; i++) {... a=(b+c*i)*d; ...} Se supondr que las variables b, c y d son invariantes al bucle. La siguiente secuencia de cudruples es equivalente al bucle anterior: INICIALIZACION: (=,0,,,i) CUERPO: ... (*,c,i,t1) (+,b,t1,t2) (*,t2,d,t3) (=,t3,,a) ... INCREMENTO: (+,i,1,i) Obsrvese que se han identificado en el bucle sus tres partes: inicializacin, incremento y cuerpo. La clave del algoritmo consiste en identificar en cada cudrupla las variables del bucle, las invariantes y el incremento. Para identificar en la primera cudrupla(*,c,i,t1) la variable del bucle (i) y el incremento (1) es suficiente consultar la parte incremento del bucle. El tratamiento especifica que se elimine la cudrupla, que se aadan a la parte inicializacin las cudruplas (*,c,i,t1) y (*,c,1,t4) (t4 es una variable temporal nueva) y en la parte incremento (+,t1,t4,t1). A continuacin se muestra el resultado, en el que se resaltan los cambios: INICIALIZACION: (=,0,,,i) (*,c,i,t1) (*,c,1,t4) CUERPO: ... (+,b,t1,t2) (*,t2,d,t3) (=,t3,,a) ..S. INCREMENTO: (+,i,1,i) (+,t1,t4,t1) Se repite el anlisis para la cudrupla (+,b,t1,t2). La parte incremento indica que t1 es variable de bucle y que su incremento es t4. Por tanto, hay que eliminar la cudrupla, aadir a la parte inicializacin la cudrupla (+,b,t1,t2), y a la parte incremento la cudrupla (+,t2,t4,t2). INICIALIZACION: (=,0,,,i) (*,c,i,t1) (*,c,1,t4) (+,b,t1,t2)

308

Compiladores e intrpretes: teora y prctica

CUERPO: ... (*,t2,d,t3) (=,t3,,a) ... INCREMENTO: (+,i,1,i) (+,t1,t4,t1) (+,t2,t4,t2) Es importante sealar que, al ir eliminando cudruplas del cuerpo del bucle, algunas de las cudruplas de las otras partes pueden llegar a ser innecesarias, porque se refieran a variables que ya no estn en el bucle. Esto pasa en la penltima cudrupla de la parte incremento. La variable t1 ya no aparece en el cuerpo del bucle, por lo que la cudrupla que la incrementa se puede eliminar. No se hace lo mismo con la primera cudrupla (+,i,1,i), porque se supone que en los fragmentos omitidos del cuerpo, representados con puntos suspensivos, s puede aparecer la variable i. INICIALIZACION: (=,0,,,i) (*,c,i,t1) (*,c,1,t4) (+,b,t1,t2) CUERPO: ... (*,t2,d,t3) (=,t3,,a) ... INCREMENTO: (+,i,1,i) (+,t2,t4,t2) La consulta de la seccin incremento indica que en la cudrupla (*,t2,d,t3), t2 es variable del bucle y t4 su incremento. El algoritmo indica que se elimine la cudrupla y que se aadan a la parte inicializacin las cudruplas (*,t2,d,t3) y (*,t4,d,t5), donde t5 es una nueva variable temporal. Tambin hay que aadir a la parte incremento la cudrupla (+, t3, t5, t3). Como en el paso anterior, con la cudrupla eliminada desaparecen en el bucle las referencias a la variable t2, por lo que la cudrupla (+,t2,t4,t2) ya no es necesaria y puede eliminarse de la parte incremento. INICIALIZACION: (=,0,,,i) (*,c,i,t1) (*,c,1,t4) (+,b,t1,t2) (*,t2,d,t3) (*,t4,d,t5) CUERPO: ... (=,t3,,a) ... INCREMENTO: (+,i,1,i) (+,t3,t5,t3) La ltima cudrupla del bucle no puede modificarse, ya que no tiene la estructura adecuada.

Captulo 7. Optimizacin de cdigo

309

7.7.2. Algunas observaciones sobre la optimizacin de bucles por reduccin de fuerza


Las llamadas a las funciones dentro de los bucles aaden dificultad a las optimizaciones, ya que pueden utilizar variables globales o incluso modificarlas. El compilador tiene que tener en cuenta estas circunstancias antes de generalizar a las llamadas a funciones el tratamiento propuesto en este algoritmo. La existencia de bucles anidados, si en los cuerpos interiores se puede acceder a variables de los exteriores, tambin dificulta estas optimizaciones y tiene que ser tenido en cuenta por los programadores del compilador. Si los bucles se repiten poco (0 o 1 veces), puede que el aumento de eficiencia no compense el esfuerzo realizado. Estas consideraciones tienen como consecuencia que los compiladores que aplican este tipo de optimizaciones necesiten de dos etapas: en la primera se analiza el cdigo fuente y se obtiene informacin para decidir el tipo de optimizacin que conviene; en una fase posterior se llevan a cabo las optimizaciones.

7.8 Optimizacin de regiones


La optimizacin de un programa completo suele realizarse en varias fases. Primero se aplican algunas optimizaciones parciales, que se aplican slo a fragmentos concretos. Tras esto puede aplicarse un estudio global, que consiste en dividir el programa en regiones, entre las que discurre el flujo del control del programa. Se puede representar el programa completo mediante un grafo y aplicarle tcnicas de simplificacin procedentes del lgebra, que generen un programa ms eficiente. Muchas de las optimizaciones descritas en las secciones anteriores conviene planificarlas de este modo global, porque esto asegura que los bloques internos se tratarn antes de los externos y que no se perder ninguna optimizacin, debido al orden en que se hayan realizado. Supongamos que se divide un programa en sus bloques bsicos de control. Con ellos se puede formar un grafo en el que los nodos son los bloques, mientras los arcos indican que el bloque situado en el origen del arco puede ceder directamente el control de la ejecucin al bloque situado al extremo del arco; es decir, la ejecucin de los dos bloques puede ser sucesiva, aunque no siempre es obligatorio que lo sea, ya que cada bloque puede ser seguido por varios, en ejecuciones diferentes. Se llama regin fuertemente conexa (o simplemente regin), del grafo que representa la divisin de un programa en bloques bsicos, a un subgrafo en el que exista un camino desde cualquier nodo del subgrafo a cualquier otro nodo del subgrafo. Se llama bloque de entrada de una regin al bloque de la regin (puede haber ms de uno) que recibe algn arco desde fuera de la regin. El bloque en que se origina dicho arco, que no pertenece a la regin, se llama predecesor de la regin.

310

Compiladores e intrpretes: teora y prctica

Una vez que se conocen las regiones del programa, se construye con ellas una o ms listas de regiones, de la forma R={R1,R2,...,Rn}, tal que RiRj si ij, e i<j Ri y Rj no tienen bloques en comn, o bien Ri es un subconjunto propio de Rj. A menudo puede haber varias listas posibles, con algunas regiones comunes y otras regiones diferentes. Entre todas ellas, es preciso elegir una sola. Conviene que en dicha lista estn todos los bucles, que normalmente tienen un solo nodo predecesor y un solo nodo de entrada. Cuando haya dos posibilidades, se preferirn las regiones con esta propiedad. Tambin es bueno seleccionar la lista que contenga mayor nmero de regiones. Ejemplo La Figura 7.2 muestra cmo podra dividirse un programa ficticio en ocho bloques bsicos. El 7.14 bloque 1 es el inicio del programa y el bloque 8 su terminacin. Mediante instrucciones de control tipo if-then-else o bucles, los bloques intermedios (del 2 al 7) permiten realizar flujos de ejecucin como los indicados en la figura. En este grafo se pueden distinguir las cinco regiones o subgrafos fuertemente conexos siguientes: (6), (3,5), (2,3,5,6,7), (2,3,4,6,7), (2,3,4,5,6,7). El lector puede comprobar que, desde cualquier nodo de estas regiones, puede alcanzarse cualquier otro nodo de su regin (incluido l mismo). Obsrvese que los bloques 1 y 8 no pertenecen a ninguna regin. En el programa de la Figura 7.2, la regin (3,5) tiene un bloque de entrada, el nodo 3, que recibe un arco desde el nodo 2, que por tanto es un bloque predecesor de esta regin. En cambio, la regin (2,3,5,6,7) tiene dos bloques de entrada: el nodo 2, que recibe un arco desde el nodo 1, y el nodo 6, que recibe otro desde el nodo 4. Obsrvese que los nodos 1 y 4, los dos predecesores de la regin, no pertenecen a ella.

Figura 7.2. Ejemplo de grafo de regiones.

En el ejemplo de la Figura 7.2, una lista vlida sera: (6), (3,5), (2,3,5,6,7), (2,3,4,5,6,7). Otra lista vlida es: (6), (2,3,4,6,7), (2,3,4,5,6,7). Obsrvese que las dos regiones (2,3,4,6,7) y (2,3,5,6,7) no pueden estar juntas en una lista vlida, pues tienen bloques en comn, pero ninguna de las dos es subconjunto o subgrafo de la otra. Entre las dos listas, seleccionaremos la primera, que contiene cuatro regiones, mientras la segunda slo contiene tres.

Captulo 7. Optimizacin de cdigo

311

7.8.1. Algoritmo de planificacin de optimizaciones utilizando regiones


Para cada bloque se definen los siguientes vectores booleanos, que tienen tantos elementos como variables forman parte del programa que se est planificando: R[v]=1 si la variable v se utiliza dentro del bloque. Se le asignar 0 en caso contrario. R es la inicial de Referencia (Reference, en ingls). A[v]=1 si la variable v recibe alguna asignacin dentro del bloque. A es la inicial de Asignacin (Assignment, en ingls). B[v]=1 si la variable v se usa dentro del bloque antes de recibir alguna asignacin. Se incluye el caso en que v es utilizada, pero no recibe asignacin dentro del bloque. B es la inicial de Before (que significa antes, en ingls). La disyuncin lgica (O lgico) de R[v] para todos los bloques de una regin, nos da el valor que toma R[v] para la regin entera, e igual pasa con A[v], pero no necesariamente con B[v], pues en una regin que contenga varios bloques, una variable que en uno de ellos sea utilizada antes de asignarle nada, podra no cumplir esta propiedad para la regin entera. Las optimizaciones se aplican sucesivamente a las distintas regiones de la lista, de la primera a la ltima. Una vez optimizada una regin, pueden aparecer bloques nuevos, como consecuencia (por ejemplo) de la extraccin de instrucciones del interior de los bucles, que pasan a la zona de inicializacin, que se encuentra fuera del bucle (vase la Seccin 7.7). A continuacin se resume el algoritmo completo. 1. Se selecciona una lista de regiones. Se hace i=1 (i es un ndice sobre la lista de regiones seleccionada). 2. Se toma la regin Ri de la lista seleccionada. Se prepara un bloque I de iniciacin vaco (sin instrucciones). 3. Se eliminan redundancias dentro de cada bloque de la regin. 4. Se construyen los tres vectores booleanos para cada bloque de la regin. Estos vectores se utilizan para comprobar si se cumplen las condiciones de optimizacin de bucles indicadas en la Seccin 7.7. Por ejemplo, los invariantes del bucle son las variables para las que A[v]=0. 5. Se extraen fuera del bucle las instrucciones invariantes y se reduce la fuerza dentro de la regin, con lo que se va llenando el bloque I. Se eliminan las asignaciones muertas (asignaciones que nunca se usan, vase la Seccin 7.9). A medida que se realizan estas optimizaciones, se crean variables temporales y se van modificando los vectores booleanos, cuyos valores y cuyo tamao van cambiando. 6. Se obtienen los valores de los tres vectores para la regin entera. 7. Se insertan copias del bloque I entre cada bloque predecesor de la regin y el bloque de entrada correspondiente.

312

Compiladores e intrpretes: teora y prctica

8. Se colapsa la regin, es decir, se sustituyen todos sus bloques por un bloque nico, al que no hace falta aplicar ms optimizaciones. A dicho bloque se le aplican los vectores calculados en el paso 6. 9. Se modifica la lista de regiones para tener en cuenta los cambios efectuados en los pasos 7 y 8. 10. i++; si i<=n, ir al paso 2. 11. Se realiza la optimizacin de cudruplas y la eliminacin de redundancias en los bloques bsicos que no pertenecen a ninguna regin. Ejemplo En el ejemplo de la Figura 7.2, la primera regin a optimizar es la (6), que est formada por un 7.15 solo bloque, que evidentemente es un bucle. Esta regin tiene dos puntos de entrada externa (ambos dirigidos al bloque 6), cuyos nodos antecesores son 4 y 5. Al aplicar, en el punto 5, el algoritmo de la Seccin 7.7, algunas de las instrucciones saldrn del bucle y pasarn a formar parte del nuevo bloque de inicializacin. Habr que colocar dos copias de este bloque (I1 e I2) en todos los puntos de entrada de la regin. (En la figura, en medio de los arcos que vienen de los bloques antecesores, 4 y 5). El resultado aparece en la Figura 7.3.

I1

I2

Figura 7.3. Optimizacin de regiones: primer paso.

A continuacin, se colapsan todos los bloques de la regin recin tratada, para formar un solo bloque, al que ya no hay que aplicar ms optimizaciones. Al mismo tiempo, se calcular el valor de los vectores booleanos para el bloque colapsado. En el ejemplo, la regin formada por el bloque (6) se colapsa para formar el bloque R1, y desaparece el bucle del bloque 6 sobre s mismo (vase la Figura 7.4). Una vez colapsada la regin (6) e introducidos los bloques nuevos I1 e I2, la lista de regiones debe modificarse adecuadamente, quedando as: (R1), (3,5), (2,3,5,I1,R1,7), (2,3,4,5,I1,I2,R1,7). A continuacin, se pasa a la segunda regin de la lista (3,5). Esta regin, que evidentemente constituye otro bucle, tiene un solo arco de entrada, cuyo predecesor es el bloque 2. Se aplican sobre esta regin las optimizaciones indicadas en el algoritmo. Como consecuencia de este proceso, aparecer un bloque nuevo (I3), que contendr las instrucciones extradas desde el

Captulo 7. Optimizacin de cdigo

313

I1

R1

I2

Figura 7.4. Optimizacin de regiones: segundo paso.

interior del bucle hasta su zona de inicializacin. En este caso, habr que insertar el bloque I3 en un solo punto, ya que la regin (3,5) tiene un solo nodo de entrada. Tambin se colapsar la regin, formando un bloque nuevo (R2). La Figura 7.5 muestra el resultado final de esta parte del algoritmo.

I3

R2

I1

R1

I2

Figura 7.5. Optimizacin de regiones: tercer paso.

Despus de estos cambios, la lista de regiones queda as: (R1), (R2), (2,I3,R2,I1,R1,7), (2,I3,R2,4,I1,I2,R1,7). Las restantes iteraciones del algoritmo quedan como ejercicio para el lector.

Identificacin y eliminacin de las asignaciones 7.9 muertas


El desarrollo de las aplicaciones, que a medida que se depuran los errores se ven sometidas a modificaciones frecuentes del cdigo fuente, provoca a menudo la aparicin de asignaciones con-

314

Compiladores e intrpretes: teora y prctica

secutivas de valores distintos a la misma variable, sin que exista un uso intermedio de la primera asignacin, que no aporta nada al programa. Tambin pueden surgir asignaciones intiles como consecuencia de la reduccin de fuerza y de optimizaciones semejantes (como se vio en la Seccin 7.7). Esta situacin se formaliza en el concepto de asignacin muerta, que sera conveniente eliminar. A veces, las asignaciones muertas estn camufladas bajo la apariencia de instrucciones tiles. Esto ocurre, por ejemplo, en las asignaciones recursivas a variables que slo se utilizan en dichas definiciones recursivas. Vase, por ejemplo, la siguiente instruccin de C++: for (int i=0; i<n; i++) {} Obsrvese que, en esta instruccin, la variable i es local al bloque, cuyo cuerpo est vaco. Por lo tanto, esta variable recibe el valor inicial cero y se va incrementando hasta alcanzar el valor n, despus de lo cual queda eliminada, por haberse alcanzado el lmite de su rango de definicin, sin haberse utilizado para nada. Por esta razn, las dos asignaciones a la variable i en esta instruccin se deberan considerar asignaciones muertas. (Recurdese que la expresin i++ representa una notacin reducida de la instruccin i=i+1, y por tanto se trata de una asignacin). Para ver si una asignacin est muerta, se puede utilizar el siguiente algoritmo: 1. A partir de una asignacin sospechosa a la variable v, se sigue adelante con las instrucciones que forman parte del mismo bloque. Si aparece otra asignacin a la misma variable sin un uso intermedio, la asignacin est muerta. Si aparece un uso de esa variable, la asignacin no est muerta. Si no ocurre ni una cosa ni otra, ir al paso 2. 2. Se siguen todas las ramificaciones del programa, a partir del bloque en que se encuentra la asignacin sospechosa, y se comprueban los valores de los vectores R, A y B para dicha variable en cada bloque por el que se pase. Si se encuentra que B[v]=1 en un bloque, la asignacin no est muerta. Si B[i]=0, pero A[i]=1, se abandona ese camino. Si se acaban los caminos o se cierran todos los bucles de los bloques sucesivos, la asignacin est muerta.

7.10 Resumen
Dado que la optimizacin perfecta no es decidible, este captulo presenta un conjunto de tcnicas de optimizacin que se pueden aplicar juntas o por separado, tanto sobre la representacin intermedia en forma de cudruplas, como sobre el cdigo final generado, para incrementar la eficiencia del mismo en diferentes aspectos: reduccin del nmero de instrucciones, del nmero de variables auxiliares utilizadas, del tiempo de proceso de las instrucciones, etc. El criterio ms importante que se utiliza para agruparlas es su dependencia respecto a una mquina concreta.

Captulo 7. Optimizacin de cdigo

315

7.11 Ejercicios
1. Dado el programa del Ejercicio 14 del Captulo 6: int a = 2, b = 8, c = 4, d; for(i=0; i<5; i++){ a = a * (i* (b/c)); d = a * (i* (b/c)); } Aplicar sucesivamente las siguientes optimizaciones a sus cudruplas: Ejecucin en tiempo de compilacin. Eliminacin de redundancias. Reduccin de fuerza. 2. Dado el programa del Ejercicio 15 del Captulo 6: int a; float b; a = 4 + 3; a = 5; b = a + 0.7; Suponiendo que inicialmente la tabla de smbolos est vaca, especificar su contenido despus de aplicar el algoritmo de ejecucin en tiempo de compilacin a sus cudruplas. 3. Dado el programa del Ejercicio 5 del Captulo 6: int a,b,c,d,i; ... a = b+c; for (i=0; i<a; i++) d+=(b+c)*i; Aplicar el algoritmo de reduccin de redundancias y de reduccin de fuerza a sus cudruplas.

Captulo

Intrpretes

Como se vio en la Seccin 1.13.2, un intrprete es un procesador de lenguaje que analiza un programa escrito en un lenguaje de alto nivel y, si es correcto, lo ejecuta directamente en el lenguaje de la mquina en que se est ejecutando el intrprete. Cada vez que se desea ejecutar el programa, es preciso interpretar el programa de nuevo. En realidad, muchos de los intrpretes existentes son en realidad compiladores-intrpretes, cuyo funcionamiento tiene lugar en dos fases diferentes: La fase de compilacin, o de introduccin del programa: el programa de partida se compila y se traduce a un formato o lenguaje intermedio, que no suele coincidir con el lenguaje de ninguna mquina concreta, ni tampoco ser un lenguaje simblico o de alto nivel, pues se acostumbra a disear un formato propio para cada caso. Esta operacin se realiza usualmente una sola vez. El formato interno puede ser simplemente el resultado del anlisis morfolgico, o bien puede haberse realizado una parte del anlisis sintctico y semntico, como la traduccin a notacin sufija o a cudruplas. La fase de interpretacin, o de ejecucin del programa: el programa compilado al formato o lenguaje intermedio se interpreta y se ejecuta. Esta operacin se realiza tantas veces como se desee ejecutar el programa. Las dos fases no tienen por qu ser consecutivas. Es posible que el programa se introduzca y compile mucho antes de ser ejecutado por el intrprete. En general, el compilador y el intrprete estn integrados en un solo programa ejecutable. En cambio, en el lenguaje JAVA, las dos partes se han separado por completo y se tiene el compilador de JAVA, que traduce los programas escritos en JAVA a un formato especial llamado bytecode, y lo que se llama intrprete o mquina virtual de JAVA es, en realidad, un intrprete de bytecode.

318

Compiladores e intrpretes: teora y prctica

8.1 Lenguajes interpretativos


Entre los lenguajes que usualmente se interpretan se pueden mencionar LISP, APL, PROLOG, SMALLTALK, Rexx, SNOBOL y JAVA. De algn lenguaje, como BASIC, existen a la vez compiladores e intrpretes. Algunos de los lenguajes anteriores no pueden compilarse y exigen la utilizacin de un intrprete. Esto puede ocurrir por diversos motivos: 1. Porque el lenguaje contiene operadores muy difciles o imposibles de compilar. El ejemplo tpico de esto es una instruccin que ejecuta una cadena de caracteres como si se tratase de una instruccin ejecutable. LISP, por ejemplo, posee una instruccin EVALQUOTE que realiza precisamente esa funcin. El mtodo value, aplicado a objetos de la clase Block, realiza la misma funcin en el lenguaje SMALLTALK. APL y PROLOG tambin disponen de funciones u operadores semejantes. Esta operacin es imposible para un compilador, a menos que el cdigo que genere incorpore un intrprete completo del lenguaje fuente. En cambio, para un intrprete es perfectamente posible, pues el intrprete est siempre presente durante la ejecucin del programa, por lo que puede invocrsele para ejecutar una cadena de caracteres. De hecho, lo que usualmente se llama compilador de LISP, de PROLOG o de SMALLTALK no genera realmente cdigo mquina equivalente al programa fuente, sino un programa ejecutable que empaqueta dicho programa fuente, precompilado, junto con un intrprete del cdigo o lenguaje intermedio. 2. Porque se ha eliminado del lenguaje la declaracin de las variables, que pasa a ser implcita. En lenguajes como LISP, APL, PROLOG y SMALLTALK, una variable tiene siempre el ltimo valor que se le asign y no existe restriccin alguna para asignarle a continuacin otro valor de un tipo completamente diferente. En los lenguajes que se compilan totalmente, es difcil implementar esto, pues el compilador debe asignar espacio a las variables en el programa objeto (vase el Captulo 10), y para ello debe saber a qu tipo pertenece su valor, ya que el tamao de una variable depende de su tipo. 3. Porque se ha eliminado del lenguaje fuente la gestin dinmica de la memoria, confindosela al intrprete y quitando as trabajo al programador. Esto se aplica, por ejemplo, a los lenguajes LISP, APL, PROLOG, SMALLTALK y JAVA. 4. Porque la presencia del intrprete durante la ejecucin es necesaria por razones de seguridad o de independencia de la mquina. El ejemplo ms claro de esto es el lenguaje JAVA, que fue diseado para permitir incluir programas en las pginas de la red mundial (World Wide Web). Un programa JAVA debe poder ser ejecutado del mismo modo, cualquiera que sea la plataforma (mquina y sistema operativo) desde la que se invoque. No tendra sentido que las pginas de la red mundial slo pudiesen verse desde Windows o desde Linux, o slo desde mquinas dotadas de microprocesador INTEL, por ejemplo. La solucin consisti en colocar un intrprete de bytecode, o mquina virtual de JAVA, en cada navegador, con lo que la dependencia de la plataforma se traslada al navegador, mientras el bytecode que se interpreta es idntico en todos los entornos.

Captulo 8. Intrpretes

319

Por otra parte, haba otro requisito indispensable para que la red mundial pudiera llegar a imponerse: la seguridad de que, al entrar en una pgina cualquiera, no se corre el riesgo de quedar infectado por un virus. Este peligro existi desde el momento en que las pginas web pudieron llevar programas ejecutables anejos. La solucin, como en el caso anterior, consisti en el uso de un intrprete para ejecutar dichos programas. Los intrpretes tienen una gran ventaja respecto a los programas ejecutables generados por los compiladores: conservan el control durante la ejecucin. Cuando un compilador genera un programa objeto ejecutable, ste puede ponerse en marcha incluso en otra mquina distinta, o si se ejecuta en la misma mquina, est fuera del control del compilador, cuyo trabajo termin con la generacin del cdigo ejecutable. En cambio, puesto que los programas interpretados se ejecutan bajo control del intrprete, ste puede disearse de tal manera que asegure la seguridad frente a virus, gusanos, troyanos y otras pestes informticas. Para ello, basta con que el intrprete o mquina virtual asociado al navegador se asegure de que no se pueden realizar ciertas operaciones peligrosas, como escribir en el disco duro local, o incluso a veces leer del mismo (un gusano que se dispersa utilizando el correo electrnico y la agenda de direcciones de la computadora local no precisa escribir en el disco duro, pero s tiene que leer de l).

8.2 Comparacin entre compiladores e intrpretes


El hecho de que los compiladores y los intrpretes coexistan, a veces incluso para el mismo lenguaje, como en el caso del BASIC, indica que ambos tipos de procesadores de lenguaje tienen que presentar alguna ventaja sobre el otro en determinadas condiciones. Esta seccin describe algunas de las ventajas y desventajas de los intrpretes respecto a los compiladores.

8.2.1. Ventajas de los intrpretes


Flexibilidad: Los lenguajes interpretativos suelen ser ms flexibles y permiten realizar acciones ms complejas, a menudo imposibles o muy difciles de procesar para un compilador. En el apartado anterior se han visto algunas de ellas: Ejecucin de cadenas de caracteres mediante operadores como execute, interpret, evalquote o value. Esto permite escribir programas muy potentes, capaces de modificarse a s mismos. Cambiar sobre la marcha el significado de los smbolos, e incluso prescindir por completo de las declaraciones. Esto reduce la carga de trabajo del programador, que no tiene que preocuparse de declarar las variables, aunque tambin puede hacer ms difcil la depuracin de los programas. Simplificar la gestin dinmica de memoria en los programas fuente, ponindola por completo bajo el control del intrprete. Al contrario de la propiedad anterior, esto facilita enormemente la depuracin de los programas, pues una parte importante del tiempo de depuracin de los programas escritos en lenguajes compilables, como C y C++, se dedica a la deteccin y correccin de errores en el manejo de la memoria dinmica.

320

Compiladores e intrpretes: teora y prctica

Obtener un enlace dinmico completo en los sistemas orientados a objetos. En los lenguajes de este tipo, los mensajes (equivalentes a las instrucciones en los lenguajes no orientados a objetos) se dirigen a un objeto determinado para indicarle que debe ejecutar cierto mtodo (una funcin). En estos lenguajes existe una propiedad (el polimorfismo), que significa que el mtodo concreto que se ejecute depende del objeto que recibe el mensaje: objetos de clases diferentes pueden tener mtodos propios con el mismo nombre, que realicen acciones muy distintas. Pinsese, por ejemplo, en la posible existencia simultnea de un mtodo llamado abrir en los objetos pertenecientes a las clases Archivo y Ventana, respectivamente. Pues bien: en los lenguajes orientados a objetos procesados por un compilador, como C++, para conseguir cierto grado de enlace o ligamiento dinmico entre un mensaje y los mtodos que invoca, hay que recurrir a declaraciones de mtodos virtuales y otros procedimientos que complican la programacin. Esto se debe a que el compilador debe resolver en tiempo de compilacin todas las invocaciones de los mensajes, pues el programa ejecutable est escrito en el lenguaje de la mquina o en lenguaje simblico, y no dispone de una tabla de smbolos que le proporcione informacin sobre la resolucin de las llamadas a mtodos polimrficos. De hecho, las declaraciones virtuales se procesan introduciendo en el programa objeto una minitabla que aporta informacin sobre las funciones polimrficas y traduce las invocaciones correspondientes en llamadas indirectas a funciones. En cambio, en los lenguajes orientados a objetos procesados por un intrprete, como SMALLTALK y JAVA, este problema no se presenta, ya que el intrprete tiene acceso a la tabla de smbolos en el momento de la ejecucin, y es capaz de resolver los mensajes polimrficos sin otra ayuda especial. Esto simplifica los programas (JAVA es un lenguaje ms simple que C++, aunque deriva histricamente de l) y facilita su depuracin. Facilidad de depuracin de programas: Adems de los comentarios anteriores a este respecto, durante la ejecucin de un programa por un intrprete, dicha ejecucin puede interrumpirse en cualquier momento, para examinar o modificar los valores de las variables, realizar saltos en la ejecucin, abandonar la ejecucin de una subrutina o alterar el entorno. Durante estas acciones, el hecho de que la tabla de smbolos est disponible facilita mucho la depuracin. Es cierto que los lenguajes compilables vienen provistos de depuradores potentes, que tambin permiten observar y manipular el contenido de las variables y realizar acciones semejantes a las de los intrpretes, a costa de sobrecargar el programa objeto con tablas de smbolos y listas de direcciones de las instrucciones. Pero hay algo que ningn depurador de un lenguaje compilable permite hacer, y que resulta fcil con un intrprete: la habilidad de suspender la ejecucin del programa en cierto punto, modificar el programa fuente y continuar la ejecucin desde el mismo punto al que haba llegado sin necesidad de volver al punto de partida. Con un programa compilable, cada vez que el depurador nos permite detectar un error en el programa, hay que interrumpir la ejecucin para corregirlo, compilar el programa corregido, generar un nuevo programa ejecutable y empezar de nuevo. Con un intrprete, en cambio, si se detecta un error, ste puede corregirse, y al continuar la ejecucin

Captulo 8. Intrpretes

321

es posible detectar ms errores sin prdida de tiempo, lo que acelera considerablemente la depuracin. Rapidez en el desarrollo: Como consecuencia de lo anterior, los programadores que utilizan un lenguaje interpretable suelen conseguir mayor eficiencia de programacin que los que programan en lenguajes compilables. Por esta razn, a veces se utilizan los primeros durante el desarrollo inicial de una aplicacin muy grande, y una vez depurada se traduce a un lenguaje compilable. En principio, la aplicacin no debera contener muchos errores nuevos, y la mejora de eficiencia de la primera fase suele compensar ms que de sobra la prdida de eficiencia debida a la traduccin. De este modo se aprovecha lo mejor de ambos tipos de traductores, eludiendo las desventajas que se mencionan a continuacin.

8.2.2. Desventajas de los intrpretes


Velocidad de los programas ejecutables: A menudo son un orden de magnitud ms lentos que programas compilados equivalentes, por lo menos. Esto se debe a que un compilador realiza los anlisis morfolgico, sintctico y semntico una sola vez y genera cdigo ejecutable en el lenguaje de la mquina de una vez para siempre. Este cdigo puede despus ejecutarse tantas veces como se desee, sin necesidad de volver a realizar anlisis alguno. En cambio, con un intrprete, cada vez que se ejecuta un programa es preciso realizar con l algn tipo de anlisis. Dicho anlisis puede no ser tan complejo como el que realiza un compilador, si el intrprete est actuando sobre cdigo parcialmente traducido (como ocurre en JAVA con bytecode), pero siempre supone cierta carga de trabajo, por muy pequea que sta sea. Lo peor es que dicha carga puede acumularse hasta alcanzar cifras enormes. Pinsese, por ejemplo, en un bucle que ha de ejecutarse 10 000 veces. Con un compilador, el bucle se traducir una sola vez al cdigo objeto, y es ste el que se ejecuta 10 000 veces. Con el intrprete, sin embargo, la pequea prdida debida al anlisis que hay que realizar cada vez que se ejecute el bucle se multiplicar por 10 000. Tamao del programa objeto: Se ha visto que lo que normalmente se llama programa compilado en entornos interpretables, en realidad no es tal, sino que se construye aadiendo una parte del intrprete al programa fuente (que habr sido traducido a veces a algn formato intermedio). Esto significa que los programas ejecutables independientes escritos en lenguajes interpretativos suelen ser mucho ms grandes que los programas compilados. La diferencia disminuye a medida que aumenta el tamao de la aplicacin, pero se nota mucho en programas triviales, del tipo Hello, World. En los lenguajes compilables, estos programas suelen dar lugar a ejecutables de 1 kilobyte como mximo, mientras que no es raro que el tamao mnimo de un programa equivalente generado por un intrprete alcance un centenar de kilobytes, pues incluye dentro de s al intrprete. Esta desventaja tiene menos importancia hoy da, cuando el bajo precio de las memorias ha dado lugar a que casi nadie se preocupe por la cantidad de memoria requerida por una aplicacin.

322

Compiladores e intrpretes: teora y prctica

8.3 Aplicaciones de los intrpretes


Resumiendo las consideraciones anteriores, se puede decir que los intrpretes se usan principalmente: Cuando el lenguaje presenta caractersticas que exigen un intrprete (LISP, APL, REXX, SMALLTALK, PROLOG). Para el desarrollo de prototipos. Para la enseanza. Durante los aos setenta, Seymour Pappert [1] dise un lenguaje interpretativo llamado LOGO, que en los ochenta fue muy bien acogido por los educadores como herramienta apropiada para ensear los principios de la programacin a los nios pequeos. Una de sus ideas en este contexto, los grficos tortuga, ha encontrado aplicaciones inesperadas en otros campos, como la descripcin de curvas fractales. Cuando el lenguaje dispone de operadores muy potentes. En tal caso, la prdida de eficiencia debida al anlisis de las instrucciones deja de tener tanta importancia, pues la mayor parte del tiempo los programas estn ejecutando cdigo rpido prefabricado, incluido en el intrprete (el cdigo que implementa dichos operadores potentes), en lugar de analizar los programas fuente del programador. En APL, por ejemplo, existen funciones primitivas capaces de invertir o manipular matrices completas sin tener que escribir bucle alguno. SNOBOL es otro lenguaje de este tipo. Para obtener independencia de la mquina (vase la Seccin 8.1, en referencia a JAVA). Para aumentar la seguridad (vase la Seccin 8.1, en referencia a JAVA).

8.4 Estructura de un intrprete


Como se puede observar en la Figura 8.1, la estructura de un intrprete es muy semejante a la de un compilador, pues contiene los mismos analizadores (morfolgico, sintctico y semntico) y una tabla de smbolos o de identificadores. Hay tambin una seccin de gestin de memoria y otra de proceso de errores, aunque stas funcionan de una manera bastante diferente de las componentes respectivas de un compilador, como se ver en los dos captulos siguientes. La diferencia fundamental entre un compilador y un intrprete estriba en la ausencia de una etapa de generacin de cdigo (as como de un optimizador de cdigo, como es lgico), que se sustituye por una componente de ejecucin de cdigo. Esto se debe a que un intrprete no genera cdigo, sino que ejecuta directamente las instrucciones en cuanto detecta lo que tiene que hacer con ellas.

Captulo 8. Intrpretes

323

Analizador semntico

Analizador morfolgico

Ejecucin de cdigo

Programa fuente

Analizador sintctico

Tabla de identificadores

Gestin de memoria

Proceso de errores

Figura 8.1. Estructura de un intrprete.

8.4.1. Diferencias entre un ejecutor y un generador de cdigo


La gestin de registros durante la ejecucin es innecesaria. Un compilador debe recordar en cada momento qu variable ha colocado en cada registro de la mquina en la que se vaya a ejecutar el programa objeto (que incluso podra ser diferente de la mquina en que se est ejecutando el compilador). En cambio, un intrprete puede prescindir de este trabajo, pues tiene que cargar las variables en sus propios registros para realizar sobre ellas la operacin correspondiente. As, donde un compilador tuviese que generar el cdigo siguiente, que corresponde a la instruccin C:=A+B: MOV EAX,A ADD EAX,B MOV C,EAX un intrprete tendra que ejecutar esas mismas instrucciones, lo que implica que ya estarn escritas en el cdigo del intrprete (si no, no podra ejecutarlas), y la seleccin del registro a utilizar (en este caso EAX) ya habr sido realizada previamente por el programador que ha construido el intrprete. Es evidente, por tanto, que un intrprete debe tener incluidas en su seccin de ejecucin todas las combinaciones de cdigo que pudiese generar un compilador que traduzca programas escritos en el mismo lenguaje. Las conversiones de tipo pueden adelantarse o aplazarse hasta el ltimo momento. Por ejemplo, si hay que realizar la suma de dos vectores de la misma longitud, uno de ellos entero y el otro en punto flotante, una opcin sera convertir todo el vector entero al tipo flotante. Otra posibilidad sera esperar hasta el momento en que se realice la operacin: a medida que le llega el turno, cada elemento del vector entero puede convertirse al tipo flotante para sumarlo con el elemento correspondiente del vector cuyos valores son nmeros reales. Ambos procedimientos pueden tener ventajas e inconvenientes: normalmente habr que buscar un equilibrio entre la ocupacin de memoria y el tiempo de ejecucin.

324

Compiladores e intrpretes: teora y prctica

Durante el anlisis sintctico, la informacin semntica asociada a los operandos de las expresiones puede generarse sobre plataformas de operandos, es decir, vectores de estructuras que contienen toda la informacin asociada al operando izquierdo, el operando derecho y el resultado de la operacin. Esta informacin se pone al da cada vez que el intrprete realiza una operacin, lo que permite obtener mejoras de eficiencia significativas. Las plataformas de operandos estn relacionadas con la pila semntica, pues la informacin que contienen puede tener que almacenarse en dicha pila para utilizarla posteriormente, como ocurrira, por ejemplo, cuando el analizador detecte que se ha abierto un parntesis durante la ejecucin de una instruccin.

8.4.2. Distintos tipos de tabla de smbolos en un intrprete


Aunque la gestin de memoria en los intrpretes se ver con detalle en el Captulo 10, aqu se va a mencionar la parte de dicha gestin que afecta a la estructura de las tablas de smbolos, puesto que stas constituyen una parte independiente de los procesadores de lenguaje. Algunos intrpretes utilizan una tabla de smbolos de tamao fijo, cuyos elementos contienen punteros a la memoria asignada a las variables. En estos casos, la tabla de smbolos tendr normalmente estructura de array. Esto es especialmente til cuando las variables pueden tener valores que ocupan mucho espacio, como ocurre con vectores, matrices y otras estructuras de datos. Por otra parte, si el valor de una variable es pequeo (como ocurre cuando se trata de un nmero entero), a veces puede introducirse dicho valor en la propia tabla de smbolos para mejorar la eficiencia del acceso al valor a travs del nombre de la variable. Otras veces, los intrpretes disponen de tablas de smbolos cuyo tamao puede modificarse de forma dinmica, y la tabla de smbolos podra estructurarse como una lista encadenada. En estos casos tambin pueden utilizarse los tipos de tabla hash descritos en el Captulo 2, con los algoritmos adaptados para tener en cuenta la estructura dinmica de las tablas. En algunos intrpretes, la tabla de smbolos no apunta directamente a la memoria asignada a las variables, sino que lo hace a travs de una tabla intermedia de referencias, que lleva la cuenta del nmero de punteros que apuntan en un momento dado al objeto de que se trate. Esto simplifica la recoleccin de desechos y la gestin de la memoria, a costa de aumentar el tiempo de acceso a los valores de las variables, pues hay que atravesar un direccionamiento indirecto ms. En cambio, la memoria ocupada por los programas durante su ejecucin puede ser significativamente menor, pues una instruccin de asignacin tal como x:=y no supondra trasvase alguno de datos (vase la Figura 8.2): bastara apuntar desde el elemento de la tabla de smbolos correspondiente a la variable x a la misma referencia a la que apunta la variable y, incrementando al mismo tiempo el nmero de referencias que apuntan a dicho elemento. De este modo, si el valor de la variable x cambiase posteriormente, el de la variable y no se perder, pues dicho valor no se declarar basura hasta que el nmero de referencias que le apuntan se reduzca a cero. Naturalmente, si se modificase parcialmente el valor de la variable x, por ejemplo, con la instruccin x[3]:=5, sera pre-

Captulo 8. Intrpretes

325

Clave

Valor

NumRef Valor

Valor de x e y y 2

Figura 8.2. Uso de la tabla de referencias para minimizar el uso de memoria en las asignaciones.

ciso obtener una referencia independiente para esta variable, copiar su valor a una zona nueva de memoria, y slo entonces modificar el valor del elemento especificado (vase la Figura 8.3).
NumRef Valor

Clave

Valor

Valor de y y 1

Valor de x

Figura 8.3. La tabla de referencias despus de la ejecucin de la instruccin x[3]:=5.

326

Compiladores e intrpretes: teora y prctica

8.5 Resumen
Este captulo revisa las principales diferencias entre los compiladores y los intrpretes. Ambos tipos de procesadores de lenguaje tienen una gran parte en comn, pero se diferencian esencialmente en la generacin de cdigo, que en los intrpretes es sustituida por una fase de ejecucin directa del cdigo fuente interpretado. Se analizan los principales lenguajes que usualmente exigen la utilizacin de un intrprete y las razones por las que conviene disearlos as. Se comparan los compiladores y los intrpretes desde el punto de vista de las ventajas y los inconvenientes del uso de unos y otros. Finalmente, se estudia con ms detalle la estructura interna de un intrprete, con especial nfasis en la ejecucin de cdigo y en las diferencias que se pueden detectar en el uso de la tabla de smbolos.

8.6 Bibliografa
[1] Papert, S. (1980): Mindstorms: children, computers and powerful ideas, The Harvester Press, ISBN: 0855271639.

Captulo

Tratamiento de errores

Para un compilador o un intrprete, la deteccin de errores es indispensable, pues todos los programas errneos deben ser rechazados automticamente. Un compilador dispondra de un procesador de errores perfecto si realiza correctamente las tres acciones siguientes: Detectar todos los errores que contiene el programa que se est analizando. No generar ningn mensaje que seale errores donde no los hay. No generar mensajes de error innecesarios.

9.1 Deteccin de todos los errores verdaderos


El primer objetivo es casi imposible de conseguir para un compilador, pero no para un intrprete. Esto se debe a que un programa contiene ordinariamente errores de varios tipos: Errores morfolgicos, como identificadores mal formados, constantes mal construidas, smbolos no pertenecientes al lenguaje, comentarios incorrectos, etc. Errores sintcticos, es decir, cadenas de unidades sintcticas que no se adaptan a la sintaxis del lenguaje fuente. Errores semnticos, como operaciones realizadas sobre tipos incompatibles. Entre stos se incluyen los errores relacionados con el uso de la tabla de smbolos, como uso de identificadores no declarados o declaracin doble de un identificador en la misma regin de alcance. Errores detectables nicamente en tiempo de ejecucin, como punteros con valor nulo o cuyo valor se sale de los lmites permitidos, o indexacin de vectores con ndices inapropiados.

328

Compiladores e intrpretes: teora y prctica

Los tres primeros tipos de error deberan ser localizados por cualquier compilador. Por esta razn, se llaman errores de compilacin, en oposicin a los del ltimo tipo (los errores de ejecucin). Sin embargo, no todos los compiladores son capaces de detectar a la primera todos los errores de compilacin. Considrese, por ejemplo, el siguiente programa escrito en el lenguaje C: void main () { int i,j,k; i=0; /* Asigno 0 a i // j=; =k; } Este programa contiene tres errores: un comentario mal escrito (no est bien sealado el punto donde termina) y las dos instrucciones siguientes, cuya sintaxis es incorrecta. Pues bien, al no detectar el final del comentario en la primera lnea errnea, algunos compiladores supondrn que las tres lneas siguientes forman parte del comentario, no realizarn su anlisis sintctico y no detectarn los errores que contienen. En cambio, adems del primer error correcto (comentario mal terminado) seguramente se sealar un error inexistente (la funcin main no termina correctamente), que indica que el compilador echa de menos la presencia de la llave final de bloque (}), que sin embargo s est presente. En un caso como el que nos ocupa, el programador corregir el primer error sealado (cerrando bien el comentario, con los smbolos */), pero no habr recibido informacin que le permita detectar los dos errores sintcticos (pinsese que puede haber decenas de instrucciones entre el primer error y el segundo). En cambio, el segundo mensaje recibido le parecer absurdo, pues puede ver a simple vista que la llave que seala el final de la funcin main s est presente. En consecuencia, despus de corregir el primer error, volver a compilar el programa fuente, y slo entonces el compilador sealar los dos errores pendientes, al mismo tiempo que desaparece el error espurio referente a la llave final. En definitiva, ms pronto o ms tarde se detectan todos los errores, pero a costa de perder el tiempo, teniendo que realizar varias compilaciones innecesarias. Un compilador con un procesador de errores ms perfecto podra detectar que el comentario mal terminado se debe al cambio del carcter * por el carcter /, dara por terminado el comentario al final de la lnea y continuara el anlisis sintctico a partir de ah, detectando las dos instrucciones errneas y eliminando el mensaje de error espurio. Por otra parte, los errores en tiempo de ejecucin suelen estar siempre fuera del alcance de un compilador, que no tiene control alguno sobre la ejecucin de los programas que ha generado. Para un intrprete, en cambio, no existe ningn problema, pues mantiene control total sobre la ejecucin del programa objeto, que se lleva a cabo como parte del propio intrprete (vase el Captulo 8). Por ejemplo, en el momento de indexar un vector de datos con una variable, el intrprete puede comprobar si el valor de la variable se encuentra dentro del margen permitido por el tamao del vector, pues este dato le es accesible a travs de la tabla de smbolos.

Captulo 9. Tratamiento de errores

329

9.2 Deteccin incorrecta de errores falsos


El segundo objetivo puede parecer tan obvio que resulte innecesario mencionarlo. Sin embargo, a lo largo de la historia de la Informtica, ms de un compilador comercial de prestigio no ha cumplido este requisito. Esto ocurre cuando la presencia de un error desequilibra al compilador hasta tal punto que a partir de entonces detecta errores en instrucciones que no los tienen. A veces, el efecto se propaga hasta el final del anlisis, con lo que el compilador genera una serie de mensajes de error en cadena, que afectan a todas las instrucciones a partir de la que provoc el desequilibrio. Se ha visto un ejemplo sencillo en la seccin anterior, donde la deteccin de un error provoc la aparicin de otro espurio (sealando que la funcin main no terminaba con una llave }). Veamos otro ejemplo, an ms espectacular. Considrese el siguiente programa correcto en lenguaje C o C++: void main () { int i, j; i=1; while (i) { int j; } j=2; if (i<j) j++; } // Lnea 1 // Lnea 2 // Lnea 3 // Lnea 4 // Lnea 5 // Lnea 10 // Lnea 11 // Lnea 12 // Lnea 65

Supngase que el programador olvid escribir la llave situada en la lnea 4, que abre el bloque while. En tal caso, es muy probable que el compilador genere los siguientes mensajes de error: Lnea 5: doble definicin del identificador j. Lnea 11: instruccin situada fuera de una funcin. Lnea 12: instruccin situada fuera de una funcin. ... Lnea 65: instruccin situada fuera de una funcin. Al faltar la llave inicial del bloque while, el compilador interpreta que las instrucciones 5 a 9 pertenecen al bloque principal de la funcin main, por lo que la declaracin de la variable j en la lnea 6 sera incorrecta (ya haba sido declarada en dicho bloque en la lnea 2). Adems, la llave final de bloque de la lnea 10 ser interpretada como el cierre de la funcin main, con lo que todas las instrucciones sucesivas (de la 11 a la 65) sern marcadas como errneas, por encontrarse fuera de un bloque. Cualquier error adicional situado en alguna de estas instrucciones no sera detectado. El compilador habr generado as 56 mensajes de error, de los que slo el primero da alguna informacin al programador, despus de pensar un poco, permitindole detectar la ausen-

330

Compiladores e intrpretes: teora y prctica

cia de la llave en la lnea 4. Una vez corregido este error, en una nueva compilacin habrn desaparecido los 55 mensajes espurios, detectndose entonces otros posibles errores situados en las lneas posteriores. Cuando un compilador es capaz de evitar estos errores en cadena, se dice que posee un procesador de errores con buena capacidad de recuperacin. Es muy difcil que un compilador se recupere del problema mencionado en el ejemplo. Una forma de conseguirlo sera la siguiente: al detectar que se estn generando demasiados errores, el compilador podra intentar introducir alguna llave adicional en algn punto que parezca razonable (quiz haciendo uso de la informacin proporcionada por el indentado de las instrucciones del programa fuente) hasta conseguir que los errores espurios desaparezcan.

9.3 Generacin de mensajes de error innecesarios


Un mensaje de error es innecesario si ya ha sido sealado previamente (en la misma o en otra instruccin) o si no aade informacin nueva a la proporcionada por mensajes de error anteriores. La razn de que se desee prescindir de los mensajes innecesarios es parecida a la de la eliminacin de los mensajes espurios: facilitar la labor del programador, evitando que tenga que localizar los mensajes de error buenos en medio de un frrago inabordable de mensajes, que proporcionan informacin nula (los mensajes repetidos) o contraproducente (los mensajes espurios). A continuacin se presenta un anlisis por separado de los dos casos mencionados: Evitar que un solo error d lugar a la aparicin de ms de un mensaje. Por ejemplo, sea la expresin a[i1;i2;i3], donde a no ha sido declarado como array. Al abrir el corchete, el compilador sealar el siguiente error: a no es un array y no se le puede indexar Al cerrar el corchete, un compilador con un procesador de errores poco sofisticado podra sealar otro error: El nmero de ndices no coincide con el rango de a. Una vez sealado el primero, el segundo es innecesario. Para solucionar este problema, una vez detectado el primer error, podra sustituirse la referencia a la variable a en la instruccin que se est analizando por una referencia a un identificador interno, generado automticamente por el compilador e introducido en la tabla de smbolos. La rutina de tratamiento de errores podra interceptar, a partir de entonces, todos los mensajes de error que hagan referencia al identificador interno. Evitar que un error idntico repetido d lugar a varios mensajes. Por ejemplo, considrese el siguiente programa correcto en lenguaje C/C++: void main () { int i; i=1; // Lnea 1 // Lnea 2 // Lnea 3

Captulo 9. Tratamiento de errores

331

while (i) { // Lnea 4 int j; // Lnea 5 if (i<j) { // Lnea 10 } // Lnea 20 for (j=0; j<n; j++) { // Lnea 21 a=j-i+1; // Lnea 22 b=2*a+j; // Lnea 23 } // Lnea 24 ... // Lnea 65

Supngase que el programador olvida escribir la llave de la lnea 10. En tal caso, la llave de la lnea 20 cerrara el bloque que se abri en la lnea 4, con lo que se dara por terminado el alcance de la variable j. Pero dicha variable se utiliza cinco veces en las lneas 21, 22 y 23. Por lo tanto, un compilador con un procesador de errores poco sofisticado generara cinco mensajes de error idnticos en dichas lneas: La variable j no ha sido declarada Esta situacin puede solucionarse de varias maneras: Cuando se detecta que el identificador j no ha sido declarado, se puede introducir en la tabla de smbolos un identificador con ese nombre y los atributos que se pueda deducir que tiene, con el objeto de que los siguientes mensajes identificador no declarado que podran generarse no lleguen a ser detectados. Esta solucin tiene el peligro de que un error real subsiguiente no llegue a ser detectado, como consecuencia de una asignacin errnea de atributos a la variable fantasma. Cuando se detectan mensajes de este tipo, se puede aplazar su generacin hasta que se est seguro de que ya no pueden detectarse ms casos, generando entonces un solo mensaje como el siguiente: La variable j no ha sido declarada en las lneas 21, 22 y 23

9.4 Correccin automtica de errores


En un compilador, un procesador de errores ptimo sera capaz no slo de detectar los errores, sino tambin de corregirlos, con lo que el programa ejecutable podra generarse ya en la primera pasada de compilacin, lo que ahorrara mucho tiempo. Sin embargo, la correccin automtica presenta el peligro de introducir errores difciles de detectar, si las suposiciones realizadas por el compilador para corregir los errores fuesen incorrectas, por lo que los compiladores comerciales no suelen realizar correccin automtica de errores, excepto algunos de propsito muy especfico, que usualmente generan directamente programas ejecutables (.EXE), en los que este

332

Compiladores e intrpretes: teora y prctica

procedimiento puede ahorrar tiempo de desarrollo, pues la mayor parte de los errores de compilacin habrn sido correctamente corregidos, y los que no lo hayan sido podrn detectarse, con un depurador conveniente, a la vez que se buscan los errores de ejecucin. En todo caso, las decisiones que tome el compilador para corregir los errores detectados deben estar bien documentadas, para que el programador pueda comprobarlas a posteriori y no se pierda tratando de depurar un programa ejecutable que no corresponde exactamente a lo que haba programado. Existen varios tipos de correcciones automticas: Correccin ortogrfica. Corresponde al analizador morfolgico y a la tabla de smbolos, aunque el analizador sintctico y el semntico tambin pueden desempear algn papel. Su funcionamiento es muy similar al de los correctores ortogrficos de los procesadores de textos: cuando se detecta un identificador no declarado, el error podra ser simplemente ortogrfico. Se trata de considerar los errores ms tpicos que podran haberse producido y detectar el identificador correcto, utilizando como trmino de comparacin la lista de palabras reservadas del lenguaje y los identificadores correctamente definidos. Una forma de acelerar el proceso consistira en comprobar nicamente los errores ortogrficos tpicos, que son: Cambio de un carcter por otro (usualmente situado en el teclado cerca del carcter correcto). Eliminacin de un carcter. Introduccin de un carcter espurio. Intercambio de la posicin de dos caracteres consecutivos. Un identificador declarado, pero no utilizado, puede ser candidato para una correccin ortogrfica. Para detectar estos casos, podra ser til aadir en la tabla de smbolos, entre la informacin asociada a cada identificador, un contador de usos y referencias. Sin embargo, debe recordarse que este caso tambin puede darse cuando un identificador utilizado en versiones anteriores del programa ha dejado de usarse, sin que el programador se acordase de eliminar su declaracin. El analizador sintctico puede ayudar en este proceso. Por ejemplo, si se espera en este punto una palabra reservada, y lo que aparece es un identificador no declarado, se puede buscar la palabra reservada ms prxima al identificador errneo, teniendo en cuenta los errores ortogrficos tpicos mencionados. El analizador sintctico puede ser tambin til para detectar los errores de concatenacin (en realidad un caso particular de la eliminacin de un carcter, cuando ste es el espacio en blanco). Por ejemplo, cuando la cadena de entrada contiene begina y lo que se espera encontrar es begin a. En cuanto al analizador semntico, tambin puede ayudar en estas correcciones. Por ejemplo, si aparece un identificador declarado correctamente en un contexto incompatible con su tipo, el error podra consistir en que se ha utilizado un identificador parecido en lugar del deseado, que s tiene tipo compatible. Correccin sintctica. Cuando se detecta un error al analizar la cadena xUy, donde x,y* y U es el prximo smbolo (terminal o no terminal) que se va a analizar, se puede intentar una de las dos acciones siguientes:

Captulo 9. Tratamiento de errores

333

Borrar U e intentar de nuevo el anlisis. Esta opcin tiene en cuenta la posibilidad de que se haya introducido en el programa una unidad sintctica espuria, que conviene eliminar. Ejemplo: sea la instruccin else x=0; donde no existe ninguna instruccin if previa pendiente. El analizador sintctico detectara un error al tratar de analizar la palabra reservada else. Una posible accin podra ser eliminar dicha unidad sintctica y tratar esta instruccin como si se tratase de: x=0; Hay que insistir de nuevo en la necesidad de que todas estas correcciones automticas del compilador estn muy bien documentadas. Insertar una cadena de smbolos terminales z entre x y U y continuar el anlisis a partir de z. Ejemplo: sea la instruccin if (i<j) {x=1; y=2; else x=0; ste parece un caso muy semejante al ejemplo anterior, en el que tambin se podra intentar eliminar la palabra reservada else. Sin embargo, obsrvese que es ms probable que el error, en esta instruccin if-then-else, consista en la falta de una llave que cierre el bloque de instrucciones correspondiente a la parte then. En este caso, el analizador sintctico, al detectar un error en la unidad sintctica else, hara mejor en tratar de insertar la llave } delante de dicha unidad sintctica y volver a intentar el anlisis, que probablemente resultara correcto. Una tercera opcin posible (borrar smbolos del final de x) no es aconsejable, pues supondra echar marcha atrs en el anlisis y deshara la informacin semntica asociada.

9.5 Recuperacin de errores en un intrprete


En el caso de un intrprete, como se mencion en el Captulo 8, el tratamiento de errores es un poco diferente al de un compilador. En primer lugar, una cosa es la ejecucin de un programa en condiciones normales, bajo control total del intrprete, y otra el programa ejecutable generado por algunos intrpretes, que empaqueta el propio intrprete junto con un programa fuente, o bien con dicho programa precompilado y traducido a un formato intermedio. En el segundo caso se supone que, antes de generar el programa empaquetado, la aplicacin ya habr sido depurada previamente, bajo control total del intrprete y en la forma que vamos a ver a continuacin, por lo que no es probable que d lugar a nuevos errores, aunque ya se sabe que toda aplicacin informtica grande contiene errores ocultos. Sin embargo, y dado que es imposible asegurar que una aplicacin informtica est totalmente libre de errores (tal demostracin es un problema NP-completo), el programa empaquetado de-

334

Compiladores e intrpretes: teora y prctica

bera tener previsto algn tipo de actuacin en el caso de que se detecte un error. Normalmente, lo mejor es generar un mensaje de error apropiado y dar por terminada la ejecucin del programa, que en todo caso terminara de una forma ms ordenada y elegante que lo que suele ocurrir cuando un programa generado por un compilador topa con un error de ejecucin que no ha sido previsto y detectado por el programador. Esto significa que el intrprete que se empaqueta con el programa para generar una aplicacin independiente llevar usualmente un procesador de errores ms simple y restringido que el que corresponde al intrprete completo. Si un intrprete detecta un error durante el anlisis y ejecucin de un programa fuente bajo el control total del intrprete (recurdese que ambas fases estn entrelazadas, pues cada instruccin se analiza y se ejecuta sucesivamente), lo mejor es detener la ejecucin, sealar el error detectado con un mensaje apropiado, y permitir al programador que tome alguna accin adecuada, eligiendo entre las siguientes: Revisar los valores de las variables. Revisar el cdigo que se est ejecutando. Modificar el cdigo para corregir el error sobre la marcha. En cuanto sea posible, hacerlo sin afectar a la ejecucin interrumpida. Reanudar la ejecucin, continuando con el programa corregido a partir del punto al que se haba llegado. Tngase en cuenta que, si el cambio realizado es muy drstico (como una reorganizacin total del programa), quiz no sea posible reanudar la ejecucin. Reanudar la ejecucin en un punto diferente del programa que se ha interrumpido, situado en la misma funcin o procedimiento que estaba activo en el momento de la deteccin del error. Esto puede significar saltarse algunas lneas, reejecutar otras (echar marcha atrs) o pasar a una zona totalmente diferente de la funcin o procedimiento. Reanudar la ejecucin en un punto diferente del programa que se ha interrumpido, pero abandonando la funcin o procedimiento donde se detect el error. Normalmente esto significa que el intrprete tendr que manipular las pilas sintctica y semntica para asegurar que el proceso se reanuda de una forma ordenada. Abandonar totalmente la ejecucin para empezar de nuevo, al estilo de lo que se hace durante la depuracin de un programa compilado, aunque en este caso no hace falta distinguir entre las dos fases de compilacin y depuracin separadas, que corresponden al uso de un compilador. Dicho de otro modo, no es preciso alternar entre dos aplicaciones informticas diferentes (el compilador y el depurador), siendo posible volver a comenzar la ejecucin desde el principio, sin que el intrprete tenga que ceder control a una aplicacin diferente.

9.6 Resumen
En este captulo se revisa el tema de la deteccin y correccin de errores, con especial nfasis en los siguientes puntos: Cmo detectar todos los errores de compilacin que contiene un programa.

Captulo 9. Tratamiento de errores

335

Cmo evitar que el procesador seale errores que no lo son. Cmo evitar que la deteccin de un error provoque que el compilador genere una cascada de errores espurios. Cmo interceptar los mensajes de error innecesarios. Debe el compilador corregir un error automticamente? En qu circunstancias? Qu diferencias separan la deteccin de errores en un compilador y en un intrprete.

Captulo

10

Gestin de memoria

El mdulo de gestin de la memoria es muy diferente en los compiladores y en los intrpretes: en los primeros tiene por objeto gestionar la memoria del programa objeto generado por el compilador, mientras que en los segundos debe gestionar su propia memoria, parte de la cul va a ser asignada a las variables propias del programa objeto, que se ejecuta bajo control directo del intrprete.

10.1 Gestin de la memoria en un compilador


Al generar el programa objeto, un compilador debe tener en cuenta que ste est formado por dos partes, claramente distintas en las computadoras que siguen el modelo de John von Neumann (actualmente, todos): El cdigo ejecutable o instrucciones (que es generado, como indica su nombre, por el generador de cdigo). Los datos. En los primeros microprocesadores de INTEL de los aos ochenta (8086 y 8088), la distincin entre el cdigo ejecutable y los datos estaba muy clara: aquellas mquinas utilizaban direccionamiento de 16 bits, que daba acceso nicamente a 64 kBytes de memoria, un tamao demasiado pequeo para muchas aplicaciones informticas. Por ello, dichos procesadores utilizaban un procedimiento especial para acceder a memorias ms grandes, para lo cul su unidad aritmtico-lgica vena provista de ciertos registros especiales (registros de segmentacin) que permitan acceder a una memoria de 12 bits, es decir, un mximo de 1 MByte. Los registros de segmentacin son cuatro: CS, DS, ES y SS. El primero (Code Segment) permite acceder a una zona reservada para el cdigo ejecutable; los dos siguientes (Data Segment y Extended data Segment), a dos zonas reservadas para datos; el cuarto (Stack Segment) a la pila de almacenamiento temporal, utilizada, por ejemplo, en las llamadas a subrutinas. Una direccin

338

Compiladores e intrpretes: teora y prctica

concreta de cualquiera de las cuatro zonas se obtiene multiplicando por 16 el valor del registro de segmentacin correspondiente y sumando un desplazamiento de 16 bits (que en el caso del cdigo ejecutable est guardado en el registro IP). As se consigue acceder a 16 65536 = 1 MByte de memoria. Los compiladores que generaban cdigo para mquinas INTEL de este tipo tenan que tener en cuenta la existencia de los registros de segmentacin y permitan al usuario elegir entre varios modelos de gestin de memoria completamente diferentes: Modelo tiny (diminuto). En este modelo CS=DS=ES=SS, constante durante la ejecucin del programa objeto. Es decir, tanto el cdigo, como los datos, como la pila, comparten una sola zona de memoria de 64 kBytes. Modelo small (pequeo). En este modelo DS=ES=SS y distinto de CS, ambos constantes durante la ejecucin del programa objeto. Es decir, los datos y la pila comparten una zona de 64 kBytes, pero el cdigo ejecutable se almacena en otra. El tamao mximo de un programa ms los datos que utiliza es, por tanto, igual a 128 kBytes. Modelo medium (intermedio). En este modelo CS es constante durante la ejecucin del programa, pero DS, ES y SS pueden variar a lo largo de la misma. Es decir, el cdigo ejecutable cabe en 64 kBytes, pero los datos pueden distribuirse entre varios segmentos, sin rebasar el mximo terico total de 1 MByte. El generador de cdigo debe tener esto en cuenta para asignar a los registros de segmentacin de datos los valores apropiados a lo largo de la ejecucin. Modelo compact (compacto). En este modelo DS=ES=SS, constantes durante la ejecucin del programa, pero CS puede variar a lo largo de ella. Es decir, los datos caben en 64 kBytes, pero el cdigo ejecutable puede distribuirse entre varios segmentos, sin rebasar el mximo terico total de 1 MByte. El generador de cdigo debe utilizar instrucciones con cdigo de operacin diferente para llamar a una subrutina y volver al punto en que se la llam, ya que la direccin de retorno debe recuperar no slo el valor del registro de desplazamiento IP, sino tambin el registro de segmentacin CS. Modelo large (grande). En este modelo CS, DS, ES y SS pueden variar independientemente a lo largo de la ejecucin del programa objeto. Es decir, la memoria total mxima de 1 MByte puede distribuirse arbitrariamente en zonas de datos y zonas de cdigo ejecutable. Este modelo viene a ser una combinacin de los dos anteriores. Modelo huge (gigantesco). En todos los modelos anteriores, una sola variable no puede rebasar el tamao mximo de 64 kBytes. En el modelo huge este lmite puede rebasarse. Al generar cdigo con este modelo para indexar esas variables, el compilador debe modificar adecuadamente el valor del registro de segmentacin durante la propia operacin de indexacin. En los modelos medium, large y huge, la memoria de datos suele dividirse en dos zonas diferentes, llamadas near heap (montn prximo, con un tamao de 64 kBytes) y far heap (montn lejano, con el resto de la memoria disponible). Inicialmente, los compiladores hacen que los registros de segmentacin de los datos y la pila del programa objeto apunten al near heap, donde se colocan la pila de llamadas de subrutina (normalmente al final de la regin) y las variables

Captulo 10. Gestin de memoria

339

estticas del programa. El far heap se reserva para la memoria dinmica (vase ms adelante). La mayor parte del tiempo, los registros de segmentacin de datos permanecen invariables. El compilador slo tiene que cambiar su contenido cuando es preciso apuntar a la memoria dinmica, y recupera el valor usual en cuanto deja de ser necesario hacerlo. Para guardar y recuperar dicho valor, se utiliza la pila de llamadas a subrutina, mediante las instrucciones PUSH y POP. Uno de los errores de ejecucin tpicos en los programas que usaban este modelo de memoria se produce cuando la memoria asignada a la pila de llamadas de subrutina (que, a medida que se requiere memoria, se extiende desde las direcciones ms altas del near heap hacia abajo) se superpone con la memoria asignada a las variables estticas, destruyendo los valores de stas. Esta situacin se llama rebosamiento de pila (stack overflow, en ingls) y, aunque los compiladores podan generar cdigo que comprobara la presencia de esta situacin, era posible inhabilitarlo mediante una opcin de compilacin que casi todos los programadores utilizaban para ahorrar memoria y tiempo de ejecucin en los programas objetos generados. A mediados de los aos ochenta, INTEL desarroll un nuevo procesador, 80286, que, aunque mantena el funcionamiento anterior por compatibilidad con los programas desarrollados para mquinas ms antiguas, posea un nuevo modo de funcionamiento que aumentaba la memoria disponible a un direccionamiento de 24 bits, es decir, 16 MBytes. A finales de los ochenta apareci un tercer procesador de INTEL, 80386, que mantena la compatibilidad con los dos modos anteriores, pero aada un tercer modo de funcionamiento con direcciones de 32 bits, lo que permita alcanzar memorias de 4 Gbytes. ste es el modo ms utilizado hoy, pues ha seguido siendo aplicable a todos los procesadores posteriores de INTEL: 80486, Pentium, Pentium II, Pentium III y Pentium IV. A continuacin se va a estudiar con ms detalle el tratamiento que debe dar un compilador a los distintos tipos de variables de datos que suele haber en un programa objeto. Variables estticas: En algunos lenguajes (como FORTRAN) todas las variables pertenecen a este grupo. En otros lenguajes (como C o C++) slo son estticas las variables declaradas como tales (con la palabra reservada static) o como externas (con la palabra reservada extern), adems de las constantes que precisen que se les asigne memoria (como las cadenas de caracteres y algunas de las constantes que aparecen como parmetro en las llamadas a subrutinas). Adems, las variables que se han declarado fuera del alcance de una funcin son estticas, por omisin. La gestin de las variables estticas por el compilador es relativamente fcil. Como se ha indicado ms arriba, suelen colocarse en el near heap o zona de memoria equivalente, situada dentro del campo de accin ms usual de los registros de segmentacin de datos (DS y ES). Usualmente, la direccin de memoria asignada a estas variables en el programa objeto forma parte de la informacin asociada a la variable en la tabla de smbolos, aunque, si el compilador genera cdigo simblico o ensamblador, dicha informacin puede omitirse, sustituyndola por etiquetas, que el ensamblador se encargar de convertir en las direcciones adecuadas. La direccin asignada a una variable puede ser absoluta o relativa. Las direcciones relativas se expresan mediante un offset (desplazamiento) respecto a una direccin base, que sue-

340

Compiladores e intrpretes: teora y prctica

le coincidir con el principio de la zona donde se almacenan todas las variables estticas. Dado que muchos programas deben ejecutarse aunque se instalen en direcciones de memoria diferentes (se dice entonces que son reubicables), en general es mejor trabajar con direcciones relativas que con direcciones absolutas. Cuando se utilizan registros de segmentacin de datos, todas las direcciones son, por definicin, relativas al contenido del registro de segmentacin. Otra informacin til asociada a las variables en la tabla de smbolos es su longitud, que es funcin del tipo de la variable y del nmero de elementos que contiene si se trata de un array (vector, matriz, etc.), o de su composicin, si se trata de una estructura (struct en C, class o struct en C++, record en Pascal, etc.). La Tabla 10.1 muestra el tamao usual de los distintos tipos atmicos que pueden tener las variables en el lenguaje C.

Tabla 10.1. Tamaos de algunos tipos atmicos de datos. Tipo de dato Boolean char short long int float double Tamao de cada elemento 1 bit 1 Byte 2 Bytes 4 Bytes 2 o 4 Bytes 4 Bytes 8 Bytes

A veces, un compilador puede compactar las variables estticas para optimizar el uso de la memoria. Sean, por ejemplo, las siguientes variables estticas en el lenguaje C: const int meses[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; const int base = 31; El compilador podra asignarle a la variable base la misma direccin que a la variable meses[0], pues ambas tienen el mismo valor. Lo mismo podra hacerse con cadenas de caracteres: supongamos que a lo largo de un programa se utilizan las cadenas de caracteres constantes abcd y d; para ahorrar memoria, el compilador podra asignar a la segunda la direccin del cuarto elemento de la primera, como indica la Figura 10.1. Obsrvese que esto slo se puede hacer cuando la segunda cadena de caracteres es subcadena final de la primera, pues el smbolo que indica el final de la cadena en el lenguaje C (\0) debe estar incluido en ambas cadenas. Tambin hay que tener mucho cuidado de que este tipo de optimizaciones se haga nicamente cuando las dos variables que se super-

Captulo 10. Gestin de memoria

341

Clave

Valor

abcd a b c d \0

Figura 10.1. Asignacin optimizada de memoria a dos cadenas de caracteres.

ponen sean constantes (es decir, que su valor no pueda cambiar durante la ejecucin del programa objeto), ya que, en caso contrario, al cambiar una de ellas cambiara la otra, lo que no sera correcto. Variables automticas: Son las variables locales de las subrutinas o de los bloques de datos, as como los parmetros que se pasan a las subrutinas (a menos que sean muy grandes, en cuyo caso se les puede asignar memoria esttica, pasando su direccin como variable automtica). Usualmente, a estas variables se les asigna espacio en la pila de llamadas de subrutinas. El conjunto de todas las variables automticas asociadas a una subrutina se llama activation record (bloque o registro de activacin) y a veces se maneja en bloque, reservando la memoria asociada a todas las variables en una sola operacin, cuando se invoca la subrutina, y liberndola asimismo toda a la vez al terminar su ejecucin. Vase como ejemplo el cdigo que podra generarse en la invocacin de una subrutina, tal como la expresin rutina(a, b), donde a y b son variables enteras: PUSH b PUSH a CALL rutina ADD SP,4 Las dos primeras instrucciones introducen en la pila los dos argumentos de la subrutina (en orden inverso, como exigen las reglas del lenguaje C). A continuacin se produce la llamada a la subrutina. La ltima instruccin libera de un golpe el espacio ocupado en la pila por los dos argumentos, sin necesidad de ejecutar dos instrucciones POP.

342

Compiladores e intrpretes: teora y prctica

b a SP Direccin de retorno Espacio libre

Figura 10.2. La pila de llamadas a subrutinas despus de llamar a rutina(a,b).

Inmediatamente despus de la ejecucin de la instruccin CALL rutina por el programa objeto, el aspecto de la pila sera semejante al indicado por la Figura 10.2, donde se ha supuesto que la instruccin CALL guarda en la pila la direccin de retorno (la direccin de la instruccin ADD SP,4) y que sta ocupa 4 Bytes (cada espacio pequeo sealado en la figura ocupa 2 Bytes). Vase ahora el cdigo generado para la definicin de la subrutina, suponiendo que el cdigo fuente correspondiente fuese el siguiente: int rutina (int a, char *b) { int i, j, k; double r; ... return k; } El cdigo generado por el compilador para esta subrutina podra ser: rutina: PUSH BP MOV BP,SP SUB SP,14 ... MOV SP,BP POP BP RET El registro BP desempea el papel de registro apuntador del bloque de activacin de la subrutina. Por eso, la segunda instruccin generada (MOV BP,SP) le asigna el valor actual del

Captulo 10. Gestin de memoria

343

b a Direccin de retorno BP BP antiguo i j k

[BP+8] [BP+6] [BP+2] [BP] [BP-2] [BP-4] [BP-6]

SP

r Espacio libre

[BP-14] [BP-16]

Figura 10.3. La pila de llamadas a subrutinas despus de ejecutar el principio de rutina(a,b).

registro SP, que apunta a la cabecera de la pila, tal como est en ese momento. Antes de hacerlo, sin embargo, el valor antiguo de BP se guarda en la propia pila, para poder recuperarlo al final de esta subrutina, ya que dicho valor antiguo de BP apuntaba al bloque de activacin de la subrutina que ha llamado a sta. A continuacin, la tercera instruccin generada (SUB SP,14) reserva memoria en la pila para el resto del bloque de activacin de la subrutina, que est formado por las variables locales declaradas dentro de sta (i, j, k y r), cuyo tamao total es de 14 Bytes. Al llegar la ejecucin del programa objeto a los puntos suspensivos, el estado de la pila ser el que indica la Figura 10.3. Obsrvese que el registro BP (apuntador del bloque de activacin de la subrutina) apunta a la direccin en que se ha guardado en la pila el valor anterior del propio registro BP. Por encima de esa direccin (recurdese que la pila se expande hacia abajo) estn la direccin de retorno a la subrutina que invoc a sta, y los valores de los argumentos que se le han pasado, situados en las posiciones BP+6 (a) y BP+8 (b). En cuanto al resto del bloque de activacin (las variables locales), el espacio reservado para ellas se encuentra situado justo por debajo del valor actual de BP. El compilador ha asignado a dichas variables las direcciones indicadas en la Tabla 10.2. Al final de la ejecucin de la subrutina, en el momento de realizar el retorno al punto desde el que se la invoc, es preciso deshacer todo este trabajo inicial, para devolver la pila a las condiciones en que se encontraba antes de la invocacin de la subrutina, con objeto de que el programa anterior pueda seguir funcionando correctamente (en particular, el registro BP debe volver a apuntar al bloque de activacin de dicho programa). Para ello, basta con que el compilador genere las tres instrucciones siguientes:

344

Compiladores e intrpretes: teora y prctica

Tabla 10.2. Direcciones asignadas a las variables en el bloque de activacin del programa rutina. Variable b a direccin de retorno BP antiguo i j k r Direccin asignada [BP+8] [BP+6] [BP+2] [BP] [BP-2] [BP-4] [BP-6] [BP-14]

MOV SP,BP, que deshace la reserva de espacio para las variables locales de la subrutina. Obsrvese que se habra conseguido lo mismo ejecutando la instruccin ADD SP,14, pero sta es menos eficiente, por incluir una operacin de suma, mientras que la instruccin elegida slo tiene que copiar el contenido de un registro en otro. POP BP, que devuelve al registro BP el valor que tena en la rutina que invoc a la subrutina actual (con lo que pasa a apuntar al bloque de activacin de aqulla). Despus de ejecutar esta instruccin, el registro SP pasa a apuntar a la direccin de retorno. RET, que pasa el control a la direccin apuntada por SP (es decir, devuelve la ejecucin a la instruccin siguiente a CALL rutina) en el cdigo generado para el programa que invoc a la subrutina. En ese punto, la instruccin siguiente que se debe ejecutar es ADD SP,4, que libera el espacio ocupado por los argumentos de la subrutina, devolviendo la pila (y el registro apuntador SP) al estado en que se encontraba antes de la invocacin de la subrutina. La Figura 10.4 resume la ejecucin de la llamada a la subrutina y de la ejecucin de sta. Las variables automticas que aparecen definidas en bloques internos se tratan exactamente igual, pero excluyendo la llamada de subrutina, el tratamiento del registro apuntador BP y el retorno. Vase un ejemplo en la instruccin siguiente: for (i=0; i<n; i++) { int j, k; ... } El cdigo generado por el compilador para este bloque ser: SUB SP,4 ... ADD SP,4

Captulo 10. Gestin de memoria

345

Programa Fuente: ... rutina (a,b) ...

int rutina (int a, char *b) { int i, j, k; double r; ... return k; }

Programa Objeto:

PUSH b PUSH a CALL rutina ADD SP,4

rutina: PUSH BP MOV BP, SP SUB SP, 14 ... MOV SP, BP POP BP RET

Figura 10.4. Resumen del cdigo generado para la ejecucin de una llamada de subrutina.

La primera instruccin reserva espacio para las variables locales del bloque, j y k, al final del bloque de activacin de la subrutina que contiene a esta instruccin. Si se tratase de la subrutina del ejemplo anterior, esto significa que a las dos variables locales al bloque se les asignaran las direcciones [BP-16] y [BP-18], respectivamente. Obsrvese que, en este caso, el compilador no puede utilizar la instruccin MOV SP,BP, en lugar de ADD SP,4, pues en tal caso liberara todo el bloque de activacin de la subrutina, en lugar del espacio local a este bloque. Variables dinmicas: El propio programa objeto pide espacio para ellas durante su ejecucin. Usualmente esto se hace mediante una llamada a alguna funcin u operador adecuados (como malloc en el lenguaje C, o new en C++). El cdigo objeto suele venir prefabricado, dentro de alguna biblioteca (library) de subrutinas asociada al compilador, que se proporciona con ste para que el programador o el compilador puedan utilizarlas, cuyas llamadas sern resueltas ms tarde por el programa enlazador (linker). La nica responsabilidad del compilador, en este caso, es generar la llamada a dicha subrutina de biblioteca. Es importante que las subrutinas asociadas a la gestin de la memoria dinmica estn bien diseadas. A este respecto, se puede mencionar un caso concreto. En un compilador para el lenguaje C comercializado por Microsoft a principios de los aos noventa, la subrutina predefinida malloc utilizaba el siguiente criterio para la reasignacin de la memoria dinmica. Buscaba espacio en primer lugar en el bloque inicial (que al principio abarcaba toda la memoria dinmica), y slo si no encontraba espacio en l buscaba en los bloques reutilizables (liberados anteriormente por el programa objeto). Esto tena la consecuencia de que la memoria dinmica se fragmentaba progresivamente, aunque tambin se obtena a cambio alguna mejora en el tiempo de ejecucin, pues era ms probable (al principio, al menos) en-

346

Compiladores e intrpretes: teora y prctica

contrar espacio en dicho bloque, cuyo tamao sola ser el ms grande de todos, hasta que su fragmentacin progresiva lo iba reduciendo. El problema se presentaba en aplicaciones de funcionamiento permanente (como, por ejemplo, programas de gestin del negocio en una tienda) que estn continuamente requiriendo espacio dinmico y liberndolo y que deben funcionar 24 horas al da y siete das por semana. Al cabo de algn tiempo, el programa fallaba porque la memoria se haba fragmentado hasta tal punto que la siguiente peticin de espacio ya no encontraba un bloque de memoria del tamao suficiente. Para evitarlo, hubo que sustituir la versin prefabricada de malloc contenida en la biblioteca asociada al compilador por una versin propia, que inverta el criterio de asignacin de espacio, reutilizando los bloques liberados antes de intentar buscarlo en el bloque inicial. Con este cambio se resolvi totalmente el problema, y la aplicacin pudo funcionar indefinidamente sin que se le acabase la memoria. Aunque las variables dinmicas presentan menos dificultades para un compilador, en realidad la carga de trabajo asociada a su utilizacin correcta se traslada al programador. En particular, es importante que el programador tenga cuidado de liberar de forma ordenada toda la memoria dinmica que ha ido reservando a lo largo del programa. Si no lo hace, pueden producirse errores de ejecucin muy difciles de detectar, pues no suelen hacer efecto hasta mucho despus de haberse producido, lo que dificulta su depuracin. Ante el tratamiento de la liberacin de la memoria dinmica, existen varias posibilidades: No liberar nada. La memoria dinmica que va reservando el programa no puede reutilizarse, aunque el programa haya dejado de necesitarla. Cuando se acabe totalmente la memoria disponible, el programa fallar. Antes de que esto ocurra, si el sistema operativo utiliza el disco duro como extensin de la memoria principal (paginacin o swap, en ingls), la eficiencia del programa puede deteriorarse. Liberacin explcita, que se realiza mediante llamadas a subrutinas u operadores especiales (free en el lenguaje C, delete en C++, dispose en PASCAL). En este caso hay que tener un cuidado especial, pues un mal uso de la liberacin puede dar lugar a errores catastrficos, como los siguientes: Liberacin de una memoria que no haba sido asignada. Liberacin incorrecta de una variable, antes de que el programa objeto haya terminado de utilizarla. Si dicha memoria es reasignada a otra variable por la subrutina u operador de asignacin de memoria dinmica, el programa no funcionar bien, pues una de sus variables habr recibido un valor incorrecto, que podra ser incluso incompatible con su tipo. Este error se detectar cuando se vuelva a usar dicha variable, lo que puede ocurrir mucho despus de la liberacin incorrecta, a veces despus de transcurrir horas de ejecucin del programa. Doble liberacin de la misma memoria dinmica asignada a una variable: se reduce al caso anterior. Liberacin implcita. En los compiladores, slo suele aplicarse a las variables automticas, como se explic anteriormente. En los intrpretes desempea un papel muy importante, como se ver en el apartado siguiente.

Captulo 10. Gestin de memoria

347

10.2 Gestin de la memoria en un intrprete


En los intrpretes, la gestin de la memoria es muy distinta a la de un compilador. En primer lugar, toda la memoria asignada al programa objeto suele tratarse como memoria dinmica (excepto las variables que ocupan poco espacio, cuyo valor puede incluirse en la propia tabla de smbolos), recabndose espacio para cada variable cuando comienza a ser necesaria, y liberndolo automticamente en cuanto deja de serlo. En este caso, la gestin de la memoria dinmica la lleva el propio intrprete, lo que quita mucho trabajo al programador y permite simplificar considerablemente el lenguaje fuente, que gracias a ello puede prescindir de declaraciones de variables y de todos los problemas asociados a la gestin y liberacin de la memoria dinmica, que se acaban de comentar en el apartado anterior. La liberacin automtica de memoria dinmica por el intrprete puede provocar la fragmentacin de dicha memoria, por lo que ms pronto o ms tarde es preciso realizar su compactacin, para poder reutilizarla correctamente. La gestin de la memoria dinmica se denomina recoleccin de basuras o de desechos (garbage collection, en ingls). Recordando lo que se dijo en la Seccin 8.4, al hablar de la estructura de la tabla de smbolos en un intrprete, existen cuatro mtodos principales para liberar la memoria asociada a una variable: Liberacin explcita o definicin esttica de qu es basura: El programador decide cundo debe liberarse una variable, con un mtodo semejante al que se usa en un compilador. Sin declaracin explcita de basura: La liberacin de espacio innecesario no se produce cuando la memoria asignada a una variable deja de ser necesaria, sino que se retarda hasta el momento en que la memoria dinmica se agota y es indispensable realizar una recoleccin de desechos. Este mtodo parece ms rpido a primera vista, pero esa ventaja es engaosa, pues, cuando se produce el agotamiento de la memoria, la recoleccin de desechos suele ser difcil y lleva mucho tiempo, con lo que interrumpe durante perodos considerables la ejecucin de los programas. En algunos intrpretes que se comercializaron durante los aos ochenta, a menudo ocurra que apareca un mensaje en la pantalla advirtiendo que la recoleccin de basuras estaba en proceso, y a veces se tardaba media hora en reanudar la ejecucin del programa objeto. En ciertos entornos (como en el control de instrumentos de laboratorio o en sistemas en tiempo real) este funcionamiento es inadmisible, por lo que este mtodo de asignacin dinmica de memoria ha dejado prcticamente de utilizarse, excepto en casos muy concretos y especializados. Referencia nica: Cuando a un puntero se le asigna un nuevo valor, la zona de memoria a la que apuntaba previamente se declara basura. Esto tiene la restriccin de que todo espacio reservado en la memoria dinmica slo puede estar apuntado por un nico puntero: asignar un puntero a otro estara prohibido, pues en caso contrario, al declarar basura el primero, el segundo quedara apuntando a una regin inutilizada, lo que podra dar lugar a errores peligrosos. Este procedimiento, que se emplea cuando la tabla de smbolos del intrprete no utiliza una tabla auxiliar de referencias, obliga a duplicar la asignacin de la memoria cuando se desea disponer de dos punteros que apunten a los mismos valores, como

348

Compiladores e intrpretes: teora y prctica

ocurre, por ejemplo, cuando se asigna el valor de una variable a otra, o cuando se introduce una variable como argumento de una subrutina, con la modalidad de paso por valor. Aunque este procedimiento ocupa mucha memoria, es algo ms rpido que el que vamos a ver a continuacin, por lo que, en la prctica, se utilizan ambos en diversos intrpretes. Referencia mltiple: Cuando los punteros de la tabla de smbolos no apuntan directamente a la memoria dinmica asignada a las variables, sino a travs de una tabla de referencias, como se explic en la Seccin 8.4. En este caso, pueden existir varios punteros apuntando a la misma zona de datos dinmicos. La tabla de referencias contiene informacin sobre el nmero de punteros que apuntan al mismo espacio, que no se declarar basura hasta que dicho nmero se reduzca a cero, lo que indica que el espacio ha dejado de ser necesario.

10.2.1. Algoritmos de recoleccin automtica de basura


Existen diversos algoritmos para la recoleccin de desechos, que se utilizan en diversos intrpretes. A veces pueden combinarse con xito varios de ellos en un solo intrprete. Mencionaremos los siguientes: Asignacin de memoria por un extremo del bloque libre inicial y recoleccin global de basuras cuando no queda espacio en dicho bloque: Este procedimiento tiene la ventaja de que los bloques ms usados acabarn ocupando espacio en direcciones bajas de memoria y ya no se movern ms. En cambio, lo que va quedando del bloque libre inicial a lo largo de la ejecucin se encuentra siempre en las direcciones ms altas, y su localizacin ser factible con un solo puntero. La desventaja principal de este mtodo es que, cuando un bloque antiguo queda libre, para reutilizar ese espacio hay que mover casi todo lo dems. Su problema principal es que el tiempo necesario para llevar a cabo la ejecucin de un programa concreto no es predecible con exactitud, pues en la mitad de dicha ejecucin puede interponerse una recoleccin de basuras, de duracin impredecible. En una de estas operaciones, para un espacio total de 20 MBytes, puede ser necesario cambiar de sitio de 2 a 4 MBytes. Si el sistema est paginado (swap), la cosa puede ser peor si el sistema operativo disminuye automticamente la prioridad de las zonas de memoria afectadas. Cada vez que se mueve la posicin de un bloque, es preciso modificar al mismo tiempo todos los apuntadores que contenan su direccin. Con los sistemas de referencia nica y de tabla de referencias vistos anteriormente, esto significa cambiar el valor de un solo puntero. A veces se incluye en cada bloque un puntero inverso que apunta a su propio apuntador, lo que reduce el tiempo necesario para la recoleccin de basuras. Para reducir el nmero de veces que se realiza un desplazamiento completo, retrasando la aparicin de la necesidad de hacerlo, lo que ocurre cuando una peticin de memoria dinmica no puede satisfacerse, porque no existe ningn bloque del tamao adecuado, suelen utilizarse diversos mecanismos paliativos, como los siguientes: Fusin automtica del ltimo bloque: Si el bloque liberado es el ltimo, y a continuacin comienza lo que queda de la zona libre inicial, en lugar de someter dicho blo-

Captulo 10. Gestin de memoria

349

que al sistema general de recoleccin de basuras, bastara fusionarlo con la zona libre inicial, modificando el valor del puntero que apunta a sta. Fusin de bloques libres: Siempre que dos bloques contiguos quedan libres, se fusionan para formar un solo bloque ms grande. Desplazamiento de bloques ocupados para permitir la fusin de bloques libres no contiguos: Es un paso adicional que facilita la realizacin del paso anterior y retrasa an ms la necesidad de la recoleccin de basuras global. Asignacin de memoria alternativa por los dos extremos del bloque libre inicial y recoleccin global de basuras cuando no queda espacio en dicho bloque (algoritmo pingpong): En este caso, lo que va quedando del bloque libre inicial a lo largo de la ejecucin se encuentra ms o menos centrado en la memoria dinmica disponible, por lo que ser preciso localizar su posicin mediante dos punteros. Este procedimiento puede reducir considerablemente el nmero de recolecciones de basuras que ha de realizarse durante la ejecucin de un programa. Para ver cmo puede ocurrir esto, y cmo difiere este mtodo del anterior, considrese el siguiente programa escrito en el lenguaje APL: A 1 3 2.5 A2+3A La primera instruccin asigna a la variable A el vector formado por los tres valores reales (1 3 2.5). La segunda instruccin calcula el resultado de sumar 2 al triple de la parte entera de A. La expresin A significa la parte entera de A. En APL, las operaciones aritmticas se aplican automticamente a vectores, matrices y estructuras completas ms complejas, de modo que las operaciones indicadas en la segunda instruccin necesitaran de la asignacin de memoria dinmica para almacenar los distintos resultados parciales, de los que slo el ltimo ser asignado como nuevo valor de la variable A. Las Figuras 10.5 y 10.6 muestran cmo se ejecutaran estas dos instrucciones con el procedimiento de la asignacin de memoria por un solo extremo y por los dos extremos del bloque inicial, respectivamente. Caso de la asignacin de memoria por un solo extremo: a) b) En la situacin inicial, la memoria est vaca. El puntero apunta al principio de dicha memoria. Durante la ejecucin de la instruccin A 1 3 2.5, se obtiene de la memoria libre el espacio dinmico necesario para introducir un vector de tres elementos. Dicho espacio queda asignado a la variable A, mientras el puntero a la memoria libre se ha desplazado adecuadamente. En la segunda instruccin, la primera operacin que hay que realizar es el clculo de la parte entera de (A), cuyo valor resultar ser el vector de tres elementos (1 3 2). Para calcularlo, se necesita otro bloque de memoria dinmica, que se extrae del principio de la zona libre. El puntero se desplaza adecuadamente.

c)

350

Compiladores e intrpretes: teora y prctica

a)

b) A

c) A A

d) A A 3 x A

e) A A 3 x A 2 + 3 x A

f) A A 3 x A A

Puntero al bloque inicial

Figura 10.5. Ejecucin de dos instrucciones APL con asignacin dinmica de memoria por un solo extremo del bloque inicial.

d)

La segunda operacin que hay que realizar es el producto de la constante 3 por el resultado de la operacin anterior. Para ello, hay que pedir espacio para otro vector de tres elementos, al que se asignarn los valores (3 9 6). Mientras no se haya calculado ese producto, no se puede prescindir del resultado de A. Pero una vez calculado 3 A ya se puede declarar basura el espacio asignado a A. A continuacin, hay que sumar la constante 2 al resultado de la operacin anterior. Para ello, hay que pedir nuevo espacio. Con el algoritmo que estamos considerando, dicho espacio se obtiene del bloque libre inicial, sin reutilizar el que fue declarado basura en el paso anterior, que no estar directamente accesible hasta que se realice una recoleccin de desechos. En el bloque nuevo se calcula la operacin 2 + 3 A, cuyo resultado es (5 11 8). Slo en este punto se puede declarar basura el bloque correspondiente al resultado de la operacin anterior (3 A). Finalmente, se ejecuta la asignacin del ltimo resultado a la variable A. El valor antiguo se declara basura. Obsrvese que el bloque libre inicial ha quedado fragmentado en dos zonas libres, separadas por una zona utilizada por el valor actual de la variable A.

e)

f)

Captulo 10. Gestin de memoria

351

a)

b) A

c) A

d) A 3 x A

e) A 3 x A

f) A

2 + 3 x A

Puntero al bloque inicial

Figura 10.6. Ejecucin de dos instrucciones APL con asignacin dinmica de memoria por los dos extremos del bloque inicial.

Caso de la asignacin de memoria alternativamente por los dos extremos: a) b) En la situacin inicial, la memoria est vaca. Los dos punteros apuntan al principio y al fin de dicha memoria. Durante la ejecucin de la instruccin A 1 3 2.5, se obtiene de la parte superior de la memoria libre el espacio dinmico necesario para introducir un vector de tres elementos. Dicho espacio queda asignado a la variable A, mientras el primer puntero a la memoria libre se ha desplazado adecuadamente. En la segunda instruccin, la primera operacin que hay que realizar es el clculo de la parte entera de (A), cuyo valor resultar ser el vector de tres elementos (1 3 2). Para calcularlo, se necesita otro bloque de memoria dinmica, que se extrae del final de la zona libre. El segundo puntero se desplaza adecuadamente. La segunda operacin que hay que realizar es el producto de la constante 3 por el resultado de la operacin anterior. Para ello, hay que pedir espacio para otro vector de tres elementos, que se obtendr del extremo superior de la memoria libre, y al que se asignarn los valores (3 9 6). Una vez calculado el producto 3 A ya se puede declarar basura el espacio asignado a A. Como este espacio es frontero con el final

c)

d)

352

Compiladores e intrpretes: teora y prctica

actual de la memoria libre, puede fusionarse directamente con sta, modificando el valor de dicho puntero. e) A continuacin, hay que sumar la constante 2 al resultado de la operacin anterior. Para ello, hay que pedir nuevo espacio, que se obtendr del extremo inferior del bloque libre inicial, con lo que se reutilizar automticamente el bloque que qued libre en el paso anterior. En el bloque nuevo se calcula la operacin 2 + 3 A, cuyo resultado es (5 11 8). En este punto se puede declarar basura el bloque correspondiente al resultado de la operacin anterior (3 A). Como dicho bloque es fronterizo con el extremo superior de la memoria libre, el algoritmo que estamos utilizando lo fusionar automticamente con sta, modificando el valor del puntero correspondiente. Finalmente, se ejecuta la asignacin del ltimo resultado a la variable A. El valor antiguo se declara basura. Dado que dicho valor es fronterizo con el bloque libre, puede fusionarse con l inmediatamente. Obsrvese que el bloque libre inicial no ha quedado fragmentado, y que el valor asignado a la variable A se encuentra ahora en el otro extremo de la memoria. Es evidente que, con este algoritmo, el nmero de recolecciones de basura necesarias disminuye considerablemente.

f)

Algoritmo basado en la utilizacin de clulas colega (buddy cells): Este algoritmo, que se debe a Knuth, Markowitz y Knowlton, reparte el espacio disponible en celdas cuyo tamao es una potencia de 2. Si se necesita un bloque de un tamao determinado, dicho tamao se extiende a la potencia de 2 inmediata superior. Existen tantas listas de zonas libres como tamaos posibles. Si una lista est vaca y se precisa un bloque de ese tamao, se extrae un bloque de la lista de tamao superior y se divide en dos. Uno queda asignado como respuesta a la peticin, y el otro pasa a la lista de bloques libres. El procedimiento es recursivo. Estos dos bloques estn ligados entre s permanentemente (se los llama colegas). Cuando se declara basura un bloque, se examina el estado en que se encuentra su compaero. Si est libre tambin, ambos bloques se fusionan para formar un solo bloque de tamao doble. Este procedimiento tambin es recursivo. Veamos un ejemplo. Supongamos que la memoria libre est formada por un solo bloque de 4 kBytes y que ste es el tamao mximo posible. Los tamaos menores que vamos a considerar son 128, 256 y 512 Bytes, 1 kByte y 2 kBytes. Existirn, por tanto, seis listas de memoria libre, cinco de las cuales estarn inicialmente vacas [vase la Figura 10.7.a)]. Se pide espacio para 80 Bytes. La peticin se redondea a la potencia de 2 inmediata superior (128 Bytes). Dado que la lista correspondiente est vaca, se pasa a la lista siguiente (256), que tambin est vaca, y as sucesivamente hasta llegar a la nica lista con elementos libres (la de 4 kBytes). Se divide el bloque de 4 kBytes en dos zonas iguales de 2 kBytes. Se pone una en la lista de bloques correspondiente, mientras la otra se divide en dos zonas de 1 kByte. Se pone una en la lista de bloques de 1 kByte y se divide la otra en dos de 512 Bytes, y as sucesivamente hasta que obtenemos dos zonas de 128 Bytes, una de las cuales

Captulo 10. Gestin de memoria

353

a)

b) Espacio asignado

Lista de 128 Bytes

Lista de 128 Bytes

Lista de 256 Bytes

Lista de 256 Bytes

Lista de 512 Bytes

Lista de 512 Bytes

Lista de 1 kByte

Lista de 1 kByte

Lista de 2 kBytes

Lista de 2 kBytes

Lista de 4 kBytes

Lista de 4 kBytes

Figura 10.7. Ejemplo de recoleccin de basura utilizando clulas colega.

ir a la memoria libre y la otra ser devuelta como respuesta a la peticin inicial [vase la Figura 10.7.b)]. Si la memoria libre de 4 kBytes empieza originalmente en una direccin mltiplo de 4 kBytes, cada bloque parcial de 2n Bytes tendr una direccin cuyos n bits inferiores son iguales a cero. Esto significa que la direccin de la celda colega de una celda dada se puede obtener fcilmente haciendo una operacin O exclusivo de su direccin con su tamao. Cada zona debe estar asociada con dos datos: un bit que indique si est libre y un campo que indique su tamao. Con eso, se puede localizar la celda colega. Cuado se libera un bloque, se mira a ver si su pareja tiene el mismo tamao y si est libre. En tal caso, ambas pueden fusionarse en un bloque de tamao doble. Para evitar fusiones y divisiones consecutivas, se puede dejar un nmero mnimo de bloques en cada cadena o retardar la fusin hasta que se requiera un bloque grande. Se calcula que la utilizacin media de cada bloque no pasa del 75% (en la prctica es algo menor, pues los bloques menores son ms frecuentes que los mayores).

10.3 Resumen
Este captulo aborda el tema de la gestin de memoria en los procesadores de lenguaje. Se trata de una componente que funciona de modo completamente diferente para los compiladores y para los intrpretes, por lo que el captulo se divide de forma natural en las dos secciones correspondientes.

354

Compiladores e intrpretes: teora y prctica

En la primera seccin, se analiza el modo en que un compilador debe gestionar la utilizacin de la memoria por el programa objeto que est generando. Se analizan distintos modos del uso de la memoria por los programas objetos en las arquitecturas INTEL, y se distingue entre el tratamiento que se puede aplicar a las variables estticas, automticas y dinmicas. En la segunda seccin, se revisan distintos mtodos para la gestin de la memoria de un intrprete (memoria que utilizan en comn el propio intrprete y el programa objeto). El problema principal que hay que resolver en este contexto es la eliminacin de la memoria innecesaria, que tuvo utilidad en algn momento pero posteriormente ha dejado de tenerla. Se comparan distintos procedimientos para la compactacin de la memoria utilizable y la eliminacin de basuras o desechos.

Bibliografa
[1] Aho, A. V.; Sthi, R. y Ullman, J. D. (1986): Compiler: Principles, techniques and Tools,

Reading, MA, Addison-Wesley Publishing Company.


[2] Alfonseca, N.; Sancho, J. y Martnez, M. (1990): Teora de lenguajes, gramticas y

Autmatas, Madrid, Ed. Universidad y Cultura.


[3] Fischer, C. N. y LeBlanc Jr., R. J. (1991): Crafting a compiler with C. Redwood City, Ben-

jamin/Cummings.
[4] Gries, D. (1971): Compiler construction for Digital Computers, New York, John Wiley and

Sons, Inc. Existe traduccin espaola de F. J. Sanchs Llorca, 1975.


[5] Grune, D. (2000): Modern Compiler Design, Wiley. [6] Hopcroft, J. E.; Motwani, R. y Ullman, J. D. (2001): Introduction to Automata Thory,

[7] [8] [9] [10] [11]

Languages, and Computation, Addison Wesley. Existe versin espaola: Hopcroff, J. E., Motwani, R. y Ullman, J. D. (2002): Introduccin a la Teora de Autmatas, Lenguajes y Computacin, Madrid, Addison Wesley. Ed. Koskimies, K. (1998), Compiler construction, Proc. 7th Int. Conf. CC98, Springer. Linz, P. (1990): An introduction to Formal Languages and Automata, Lexington, D. C., Heath and Co. Louden, K. C. (2004): Construccin de compiladores. Principios y prctica, Thomson. Marcotty, M.; Ledgard, H. F. y Bochmann, G. V. (1976): A sampler of Formal Definitions, Computing Surveys, 8:2, pp. 181-276. Wirth, N. (1996): Compiler Construction, Addison-Wesley.

ndice analtico

A accin semntica, 200-203, 210-211, 219, 224, 228, 234, 238-239, 255-258, 268, 274-275, 280 acumulador, 244, 246, 247, 248, 281 ADA, lenguaje de programacin, 26, 27 Adelson-Velskii, vase bsqueda con rboles AVL algoritmo, 1, 23, 30, 177-180, 181-182 de recoleccin automtica de basuras, 348353 por llenado de tabla, 73 al-Jowritzm, Abu Jafar Mohammed ibn Musa, 1 ambigedad, 21-22, 23, 75, 176-177, 279 mbito, 56, 59, 61 anlisis ascendente, 89, 114, 127, 219, 223, 227, 260, 268 LALR(1), 89, 116, 159, 234 LR, 127 LR(0), 89, 116, 127, 129, 138, 140, 143, 145-147, 155, 159-160, 167 LR(1), 89, 116, 148, 152, 155-156, 159-160, 167 LR(k), 127, 151, 158-159 SLR(1), 89, 116, 138, 140, 145-147, 159-160 de precedencia simple, 89, 177-180 descendente, 89, 93, 114, 218-219, 223, 227, 260-261, 268

con vuelta atrs, 93 con vuelta atrs rpida, 97 LL(1), 99, 111 selectivo, sin vuelta atrs o descenso recursivo, 93, 99, 102, 111, 227 analizador ascendente, vase anlisis ascendente descendente, vase anlisis descendente LALR(1), vase anlisis LALR(1) LR(0), vase anlisis LR(0) LR(1), vase anlisis LR(1) morfolgico o lxico, 28-29, 65, 192, 196, 200, 217, 231-232, 234, 237, 240, 322, 332 semntico, 29, 192, 194, 211, 217-218, 221, 223, 228, 230, 232-235, 322, 332 sintctico, 29, 89, 142-143, 196, 200, 217218, 220-223, 237, 322, 332, 333 anotacin semntica, 192, 194, 208 APL, lenguaje de programacin, 26, 28, 253, 318, 322 apuntador, 247, 253, 254, 324, 327, 342-344, 347-352 del anlisis, 128, 130-131, 139, 151, 153 tipo de dato, 157, 217, 228-230 rbol de derivacin, 19-21, 97-99, 114, 141, 148149, 151, 175, 192-194, 196, 202, 205, 208, 221, 224 con anotaciones semnticas, 192193, 196, 200-201 AVL, vase bsqueda

356

Compiladores e intrpretes: teora y prctica

binario ordenado, vase bsqueda arco, 68, 181, 182 argumento, de un procedimiento, 58, 192, 230, 233, 258, 341-344, 348 array, 192, 229, 301, 324, 330, 340 ASCII, 286 asidero, 14-15, 21, 89, 117, 132, 218, 223, 260 asignacin, 157, 206-208, 231, 254, 266, 287, 294, 314, 324, 352 muerta, 311-314 atributo, heredado, 205, 216-217, 221, 223, 230 semntico, 199, 202-203, 205, 210, 212, 214, 216-217, 235-236 sintetizado, 205, 214, 216, 221, 223 atributos evaluacin de, 208 propagacin de, 205, 231 sistema de, 203 autmata a pila, 4, 29, 30, 90, 116-117 aceptador, 3 concepto de, 2,3, 68 de anlisis, 117, 127, 129, 131-133, 135139, 151, 154-156, 158, 160-161, 166167 finito, 2, 4, 117 determinista, 29, 65-66, 69, 71-76 no determinista, 66, 68-71 autmatas, teora de, 2 axioma, 11, 14, 18, 19, 22, 23, 24, 29, 89-94, 110, 113-114, 116, 129, 168-169, 235 B Backus, forma normal de, vase B.N.F. BASIC, lenguaje de programacin, 26, 28, 318, 319 basura, vase recoleccin de basuras binoide, 9 Bison, 234 bloque, vase tb. estructura de bloques, lenguaje con , 59-62, 279, 310, 314 abierto, 57-58 actual, 57-58, 60

cerrado, 57, 60 de entrada, 309-313 predecesor, 309-312 B.N.F., 12, 237-238 Borrar, operacin de diccionario, 38-39, 41, 44 bucle, vase instruccin iterativa buddy cells, vase clulas colega Buscar, operacin de diccionario, 38 bsqueda, algoritmo de binaria, 35, 39 con rboles AVL, 37, 44 con rboles binarios ordenados, 35-36, 40 lineal, 34, 56 listas ordenadas, 56 tabla hash o de smbolos, 51, 55, 59, 60 Bytecode, 28, 317, 318, 321 C C, lenguaje de programacin, 26, 27, 56, 7981, 104, 157, 228-229, 234-239, 253, 258-259, 287, 290, 292, 304, 307, 319, 328, 329, 330, 339, 340, 345, 346 C++, lenguaje de programacin, 26, 27, 56, 258, 259, 314, 319, 320, 329, 330, 339, 340, 345, 346 cardinal, 8, 177 clulas colega, 352, 353 Chomsky, Avram Noam , 3, 4, 15, 16, 25, 30, 90 cierre, vase clausura de un conjunto de configuraciones, 129130, 132-136, 152-155, 157 cierre l, 72 clausura, 10, 67-68, 78 clave, 196 COBOL, lenguaje de programacin, 26, 27 cdigo intermedio, 194, 258-282 colisin, 46-47, 50, 55 comentario, 29, 65, 76, 84-85 compilador, 3, 27-29, 30, 243, 318, 319-321 compilador-intrprete, 28, 317 complejidad, 33 complementacin, 11

ndice analtico

357

computabilidad, 2 concatenacin, 5-6, 7, 8-9, 67-68, 78, 245, 332 condicin de inicio, 80, 83-85 exclusiva, 84 configuracin de anlisis, 129-132, 134-136, 138, 144, 151-153, 156-157, 160-161, 166 de anlisis LR(0), 127-128 de desplazamiento, 128, 139 de reduccin, 57, 128, 138-139, 147, 149, 158 conflicto, 139, 145, 147, 158, 167 conjunto de generadores, 6, 9 de smbolos de adelanto, 151-153, 155158, 160-161, 166 first y last, 168, 171-174 primero, 90-93, 102-103, 111-113, 155156 siguiente, 90-93, 102-103, 111-113, 145, 147-149, 160 cudrupla, 258-259, 267-282, 289-300, 302303, 306-308, 311-312, 317 D declaracin, 201, 233 de una funcin o procedimiento, 30 de un identificador, 194, 230-231, 318, 328, 329, 332 definicin dirigida por la sintaxis, 223 delimitador, 76 dependencia circular, 227 constante, 45 en eliminacin de redundancias, 293-300 entre atributos , 219, 222, 224, 226 lineal, 33, 40 logartmica, 35, 44-45 depuracin de programas, 285, 319, 320-321, 334, 346 derivacin, 13, 14, 16, 17, 19-22, 89, 91, 9395, 97, 147, 175 desplazamiento, 229, 247, 260, 268, 339

accin de anlisis, 115-117, 120-122, 131, 137, 141-144, 149, 154, 158, 218, 223 diccionario, 30, 38-39 direccin , 229-231 de un identificador, 229, 244-248, 253, 339-340, 341, 344 de retorno, 246, 338, 342, 343, 344 direccionamiento, vase hash, tabla directiva, 79, 234-235 dispersin, vase hash, tabla do, vase instruccin iterativa E encadenamiento, vase hash, tabla ensamblador, 27, 195, 243-258, 289, 339 entrada calculada, vase hash, tabla equivalencia de gramticas, 17, 18, 22, 168, 169, 176 error de compilacin, 328 de ejecucin, 327 morfolgico, 29, 77, 327 semntico, 192, 327 sintctico, 125-127, 131, 142, 158, 178, 182, 327 errores correccin de, 319, 331-333 recuperacin de, 29, 329-331, 333-334 tratamiento de, 77, 320-321, 322, 327-335 estado , 120-121, 123 de aceptacin, 132, 144, 158 de reduccin, 132 final, 68, 72-76, 132, 147, 158 inicial, 68, 72-76, 154 estructura de bloques, lenguaje con, 56, 60 etiqueta, 57, 62, 230, 232, 233, 243, 245-246, 255-258, 266, 275-279, 339 expresin aritmtica, 29, 119, 126, 192, 199, 206, 231, 249-253, 288-289, 292, 300-302 con subndices, 266, 267, 330 regular, 4, 66-71, 75-84

358

Compiladores e intrpretes: teora y prctica

F factor de carga, 50, 52, 54-56 first, vase conjunto first flag, vase indicador for, instruccin, vase instruccin iterativa forma sentencial, 14-15, 20, 21, 90-91, 112, 175-176 FORTRAN, lenguaje de programacin, 26, 27, 339 frase, 3-4, 11, 14-15, 21 funcin de precedencia, 180-183 de transicin, 68, 72, 74, 75 de transicin extendida, 72 hash, vase hash, funcin G garbage collection, vase recoleccin de basuras generador de cdigo, 29, 232, 243-285, 323324, 337-338 Gdel, Kurt, 1-2 GOTO, instruccin, vase instruccin GOTO grafo de estados y transiciones del autmata, 68, 135 de dependendencias, 224-225 de regiones de un programa, 309-310 de una expresin, 303 gramtica , 129 ambigua, 21-22, 23, 176-177 aumentada, 133-134, 138, 147, 152, 155 bien formada, 23-25 de atributos, 30, 192, 199, 203, 210-212, 214, 216-217, 221, 223, 227-228, 234 de estructura de frases, 16 de precedencia simple, 168-182 formal, 11 independiente del contexto, 17-18, 23, 24, 30, 65, 91, 100, 104, 116, 147, 155, 157, 191, 203, 210, 212, 214 limpia, 23-24, 90 lineal, 18

LL(1), 89, 93, 99-100, 103, 104, 107, 112 LALR(1), 89 LR(0), 89, 140 LR(1), 89, 158-159 reducida, 23-24 SLR(1), 89, 147, 158 tipo 0, 3-4, 16, 17, 18, 30, 191 tipo 1, 3-4, 17, 19 tipo 2, 4, 17-18, 19, 25, 30, 90 tipo 3, 4, 18, 19 transformacional, 3, 25 Greibach, forma normal de, 99-104, 106 H hash funcin , 45-46, 48-49, 55-56 tabla , 45-46, 50, 55, 58-61, 324 hoja, 20 I identificador activo, 57, 58 global, 58 local, 58, 314 if-then-else, instruccin, vase instruccin condicional indicador , 245 ndice, 192, 248, 267, 327, 330 informacin semntica, 76-77, 232, 255, 260, 324, 333 insercin, 56, 77 en tabla hash o de smbolos, 51, 59 Insertar, operacin de diccionario, 38-41, 44 instruccin condicional, 230, 232, 254-256, 266, 268, 272-275, 310, 333 iterativa, 66, 233, 248, 256-258, 280, 314, 321, 322, 329, 344 GOTO, 252, 275-279 intrprete, 27-29, 30, 89, 317-326 interseccin, 11, 102, 103, 113 invariante, 7, 304, 306-307, 311-312 iteracin, vase clausura

ndice analtico

359

J
JAVA, lenguaje de programacin, 26, 28, 56, 317, 318, 320, 321, 322 L LALR(1), vase gramtica o anlisis Landis , vase bsqueda con rboles AVL last, vase conjunto last lenguaje asociado a una gramtica, 14 de los parntesis, 211 de programacin, 2, 17, 25, 26, 30, 76, 191, 195, 201, 206, 211, 223, 227-230 dependiente del contexto, vase lenguaje sensible al contexto fuente, 27-28, 65, 73, 75, 77, 230, 248, 254, 285, 287, 318, 327, 347 independiente del contexto, 17-18, 26, 89, 116 intermedio, 28, 196, 199, 317, 318 objeto, 27, 29, 232, 248, 251, 285, 287, 302 regular, 18, 65 sensible al contexto, 4, 17 simblico, 26, 27, 195, 230, 232, 243, 285, 287, 317, 320 universal, 5, 7, 10, 11, 12 vaco, 7, 8, 9 lex, 78-85, 240 linker, vase programa enlazador LISP, lenguaje de programacin, 26, 28, 56, 318, 322 lista, 55, 56, 60, 324, 352-353 LL(1), vase gramtica o anlisis LOGO, lenguaje de programacin, 322 Look-Ahead-Left-to-Right , vase gramtica LR, vase gramtica o anlisis LR(0), vase gramtica o anlisis LR(1), vase gramtica o anlisis LR(k), vase gramtica o anlisis M main, vase programa main mquina de Turing, vase Turing, mquina de

matriz Booleana, 170-171, 173-174, 182 memoria dinmica, 56, 229, 319, 339, 345-353 esttica , 229, 231, 258, 302, 339-341 gestin de, 29, 196, 258, 259, 322, 324, 337-354 meta-carcter, 67, 78 monoide, 6-7, 8, 9 N nodo, 19-21, 68, 181-182 notacin de pares numricos, para configuraciones de anlisis, 128 explcita, para configuraciones de anlisis, 128 infija, 258 prefija, 258, 259 sufija, 195, 196, 258, 259-266, 317 nmeros, teora de, 1 O offset, vase desplazamiento operador , 67, 252, 259, 268, 318, 319, 322 de comparacin, 255 didico, 254, 258, 265 mondico , 252, 259, 265, 302 operando, 229, 243-245, 247-254, 258-259, 266-268, 324 optimizacin, 195, 286 de bucles, 304 de regiones, 309-312 planificacin, 311 optimizacin de cdigo, 29 optimizador de cdigo, 29, 322 P palabra, 3, 4, 5-7, 68, 93, 94, 97, 104, 110, 111 doble, 244 longitud de una, 5, 6, 7 refleja o inversa, 7 reservada, 65, 77, 80, 83, 332, 333, 339

360

Compiladores e intrpretes: teora y prctica

vaca, 5-10, 15, 17, 24, 67, 168-169 palndromo, 19 Pappert, Seymour, 322 parser, vase analizador sintctico PASCAL, lenguaje de programacin, 26, 27, 56, 340, 346 paso del anlisis, 57, 99, 113-114, 116, 118, 123, 127 de un compilador, 29, 58-60, 194, 196, 199, 221, 227-228, 258, 285 pila, 58-60, 113-114, 116-118, 120-123, 126, 141, 144, 178-180, 195, 217, 228, 231, 244-247, 258, 260-261, 268, 272, 274275, 302, 324, 337-339, 341-344, vase tambin autmata a pila plataforma, 247, 248, 253, 324 pop, 60, 119, 122-123, 228, 245, 265, 266, 273, 274, 339, 341 potencia, 7, 9-10, 169, 171, 173 primero_LR(1), conjunto, 155-156 problemas no computables, 4 recursivamente enumerables, 4 procedimiento, 62, 192, 229, 233, 334 produccin, vase regla de produccin programa enlazador, 243, 345 programa main, 80, 81, 82, 239-240, 328, 329, 330 PROLOG, lenguaje de programacin, 26, 28, 56, 318, 322 puntero, vase apuntador puntero a la pila, 244, 246 push, 60, 119, 228, 244, 261, 264-266, 269274, 339 R raz, 19, 21 recoleccin de basuras, 347-353 recorrido en profundidad por la izquierda con vuelta atrs, 218 recuperacin de errores, 29, 77, 329-331 recursividad, 15, 176, 352 a izquierdas, 100

redimensionamiento, 55 reduccin, 29, 93 accin de anlisis, 115-117, 122-124, 128129, 131, 137, 141-142, 144-145, 147150, 154, 158, 218, 223, 227 de fuerza, 304-306, 311-313 redundancia, eliminacin de, 292-293 reflexin, 7, 10 regin, 309-313, 327 fuertemente conexa, 309 registro, 230-231, 286-287, 302 de activacin, 341-343 de segmentacin, 337-340 de trabajo, 246, 323 gestin de, 246-249 regla de produccin, 11-12, 15, 16-18, 19, 30, 114-117, 120-122, 124, 128-129, 141, 148-150, 203, 209, 211, 214-215, 234, 237 de redenominacin, 23, 24, 211 innecesaria, 23 no generativa, 23, 24, 168, 169 recursiva, 15, 168, 169, 176, 212, 215 superflua, 23 regla-l, 101-102, 104-106, 113, 114, 210-211, 238 rehash, vase sondeo relaciones de precedencia, 175-176 teora de, 168, 169-174 reordenacin de cdigo, 287 de operaciones, 300 S salto, 230, 232 condicional, 246, 266 incondicional, 245, 255, 257, 258, 266, 276, 277 scanner, vase analizador morfolgico semntica, 3, 29-30, 191 semigrupo, 6-7

ndice analtico

361

sentencia, 14, 16, 17, 20, 21, 22, 81 Shannon, Claude Elwood, 2 smbolo de adelanto, 155 inaccesible, 23 no generativo, 23, 24 no terminal, 11-12, 14, 16, 20, 23-24, 90, 94, 99, 100, 102, 103, 104, 105, 111, 117, 128-130, 132, 141-142, 144, 145, 147, 152, 157, 160, 168, 194, 210-211, 215, 231-232, 235, 254, 255, 260, 332 terminal, 11, 14, 20, 21, 23, 28, 94, 99, 102, 103, 111, 114, 117, 130-131, 142, 144, 145, 152, 160, 168, 211, 235, 237, 253, 260, 268, 332, 333 sintaxis, 3, 17, 25-26, 29-30, 89, 191, 243, 253, 259, 327, 328 Smalltalk, lenguaje de programacin, 26, 28, 259, 318, 320, 322 SNOBOL, lenguaje de programacin, 318, 322 sondeo, 50, 51, 52, 55 aleatorio, 54 cuadrtico, 54 lineal , 52 multipicativo, 53 subrbol, 21 submeta, 94-97 subrutina, 29, 56, 58, 229, 233, 243, 246, 254, 320, 337, 338, 339, 341-345, 346, 348 SLR(1), vase gramtica o anlisis T tabla de anlisis, 227 de anlisis ascendente, 118-119, 122-123, 125, 137-138, 140, 145-149, 158, 167 de referencias, 324-325, 348 de smbolos o identificadores, 28, 30, 33, 56, 58-59, 61-62, 202, 196, 206-208, 217, 230-233, 289, 293, 320, 322, 324-

325, 327, 328, 330, 331, 332, 339, 340, 347, 348 hash, vase hash, tabla terminal, vase smbolo terminal tipo, de dato, 62, 192, 194, 196, 199-203, 206207, 209, 223, 230-233, 288 Thue, relacin de , 14 transicin, 68, 72, 73, 75, 76 diagrama de, 68, 69, 141, 143-145, 147149, 156-157, 160-161 extendida, funcin de, vase funcin de transicin extendida funcin de, vase funcin de transicin triplete, 259, 267-268 indirecto, 267-268 tupla, 195, 259 Turing, Alan Mathison, 1-2, 4 Turing, mquina de, 1, 2, 4, 30 U unidad sintctica, 28, 65, 75, 76, 77, 80, 81, 82, 83, 196, 217, 240, 253, 255, 327, 333 unin, 8, 9, 67, 68, 78, 170, 173 V variable 57 automtica, 341-345 esttica, 338, 339-341 intermedia, 292-293, 311-312 de bucle, 305-308 vector, 55, 247, 323, 324, 327, 328, 340, 349352 W while, instruccin, vase instruccin iterativa Y yacc, 168, 234, 235, 238, 239, 240