Compiladores e Interpretes Teoria y Practica
Compiladores e Interpretes Teoria y Practica
Compiladores e Interpretes Teoria y Practica
teora y prctica
www.librosite.net/pulido
Manuel Alfonseca Moreno Marina de la Cruz Echeanda Alfonso Ortega de la Puente Estrella Pulido Caabate
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
1.10.
1.11.
1.12.
1.13.
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
4.3.
4.4.
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.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
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.
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
ndice analtico
Captulo
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.
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.
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
Expresiones regulares
Figura 1.1. Relacin jerrquica de las mquinas abstractas y los lenguajes formales.
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|
es el semigrupo libre engendrado por . Aadiendo ahora la palabra vaca, diremos que W() es el monoide libre generado por .
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
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.
Es decir: todas las palabras del lenguaje concatenacin se forman concatenando una palabra del primer lenguaje con otra del segundo.
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.
10
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()-{}
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 *.
11
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 *.
12
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 }
13
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
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).
16
17
18
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.
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?
20
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
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
22
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
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::=+|-|*|/}).
23
24
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.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 |
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})
26
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.
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
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
28
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
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.
29
Analizador semntico
Analizador sintctico
Generador de cdigo
Tabla de identificadores
Gestin de memoria
Proceso de errores
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.
30
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].
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.
34
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].
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.
35
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.
36
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.
37
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
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.
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.
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
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.
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.
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
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).
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
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).
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.
45
46
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.
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.
48
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
49
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
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
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
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.
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
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.
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.
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
55
La bsqueda de claves que s estn en la tabla se puede aproximar mediante esta otra: 1 1 log 1
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
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
57
{ int
a,
b,
c,
d;
{ 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
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.
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.
59
b, c, d; e, f; ...
i, h; { int a;
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
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.
61
b, c, d; e, f; ...
i, h; { int a;
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
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.
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
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.
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.
68
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.
69
0 p 1 r 0,1 0 1 q
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)
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
p1
q1
p2
q2
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
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
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
72
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
73
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.
74
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
75
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
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.
otro 1 / 2 * 3 * otro
* 4 / 5
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.
78
79
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
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.
81
%{ #include <stdio.h> /* para utilizar printf en la seccin de reglas */ %} digito [0-9] constante {digito}+ %option noyywrap %% begin end {constante}
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
%{ #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);
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.
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.
84
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
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
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);}
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
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.
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
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.
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
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)={+,$,)}
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)={*,+,$,)}
94
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
95
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
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
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
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
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
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
99
100
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
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
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
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
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).
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
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)
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
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
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.
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
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.
111
E (i+i*i, 0) = = = = = = =
V E V X X X 5
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) = = = = =
1) = 2) = 3) = T (i+i*, 4)) =
112
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::= ) $
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
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.
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
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
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.
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
ENTRADA a1 a2 E s0 T1
TABLA ANLISIS T Tn N1 N Nm
...
au $
...
sk
...
...
...
X m-1
...
Accin Ir_a s0
SALIDA
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.
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.
120
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.
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
i d1
( d2
r3 d1 r2 d6 d6 d2 d2
7 r1 r4 r1 r4
Ir_a
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
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
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 .
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
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.
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
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
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
127
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.
128
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.
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
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:
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
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
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.
133
s0
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.
134
( s5
( i i s4 i
T::=i T::= i
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).
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
(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).
137
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
138
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.
139
s3
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
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
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
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.
141
<Bloque>
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>
begin
dec
ejec
ejec
end
142
sacc
B::=B$ B::=B$
s5
D::=D;d d e
D::=D;d D::=D;d
B::=bD;Ef B::=bD;Ef
s3
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-
143
<Bloque> Desplazamiento
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.
sacc
B::=B$ B::=B$
s5 ;
e
D::=D;d D::=D;d
s3
B::=bD;Ef B::=bD;Ef
E ::= ::= e E
s9 S10
s8 s7
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
sacc
B::=B$ B::=B$
B::=bD;Ef B::=bD;Ef
E::=e E::=e ;
E::=e E::= e;E
s8 s7
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.
145
sacc
B::=B$ B::=B$
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
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.
146
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.
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).
148
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
A s3
sacc
S::=S$ S::=S$
x s5 S::=x b S::=xb
B::=x B::=x
s6 b
S::=xb S::=xb
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.
149
N x d5 acc r1 d8 r4 r5 r2 7 4 $
T S
a d3
A 2
B 4
Ir_a
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
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
A s3
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
A s3
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.
151
152
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 ::= ).
153
S::=S {$}
S::=S
{$}
S::=S
{$}
A c) S::=S $ A b)
x d)
S::=S
{$}
{$}
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
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.
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.
156
s1 S::=S{$} s2
s3 A::=B{$} S::=A{$} A s5
s10
A::=aAb{$} b
1primero 2primero
S s0
A::=aAb{$} A
s7
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.
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
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 =
R * S::=L=R {$} s9
L::=id{$,=} R::=L{$}
L::= *R{$,=} s7
L::=*R{$} L::=id{$}
L::=* R{$} R::= L{$}
Figura 4.56. Ejemplo de diagrama de estados de autmata de anlisis LR(1) con conjuntos de smbolos de adelanto no unitarios.
158
la parte derecha de la regla y no se modifican los smbolos de adelanto ({$}). El anlisis para la configuracin L::=id{=,$} es similar.
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
159
N A 2 B 3
11
Ir_a
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
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.
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
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
s1
S::=S{$} s2
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
s1 S::=S{$} s2
s10
A::=aAb{$} b
S s0
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.
163
s1 S::=S{$} s2
s10
A::=aAb{$} b
S s0
A::=aAb{$} A s7
x B::=x{b}
s1 S::= S{$} s2
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
b)
Figura 4.60. Unin de los estados s10 y s12 en el nuevo estado s10_12.
164
s1 S::=S{$} s2
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
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
b)
165
S::=S{$} s2
s10_12 A::=aAb{$,b} b
S s0
s1 S::=S{$} s2
s10_12 A::=aAb{$,b} b
S s0
A::=aAb{$,b} A
B::=x{b}
166
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}}
167
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
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
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.
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).
170
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.
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
172
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
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
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}.
175
176
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.
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.
178
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
179
Con lo que la matriz de precedencia queda: a b c S =. >. a =. <. <. >. b >. >. c >. >. <. <. <. <. S Se analizar ahora la cadena aacbb:
Pila
Asidero
Regla a aplicar
S::=c
aSb
S::=aSb
aSb
S::=aSb
Pila
Asidero
Regla a aplicar
<.a <.a<.a
180
Pila
Asidero
Regla a aplicar
S::=c
aSb
S::=aSb
Sb Sb No hay
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)
182
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.
183
Pila
f(x) 0 2 2 3
g(x) 3 3 2 2
Asidero
Regla a aplicar
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
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.
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
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
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
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.
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
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
192
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.
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>
100
<expr> <id>
+ <expr> <id>
a)
<Programa> begin <declrcns> int A ; <sntnc> <asignacion> int int int <id> := <expr> A <sntncs> ; <sntncs> <sntnc> ; int <asignacion> <sntncs> end
<sntnc>
<const.int>
<id> := <expr> <salida> int int + <expr> int <id> int A output <expr> int <id> int A
100
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
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.
195
Fuente
Objeto
Fuente
Cdigo intermedio
Objeto
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
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.
<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
begin
<declrcns>
int
; <sntnc> ; <sntncs> <sntncs>
<sntnc>
<declrcn>
<sntnc>
<salida>
<tipo>
<asignacion>
int
<id>
<id> := <expr>
100 A
<const.int>
A. Sint. y Sem.
Cdigo
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
<sntncs>
begin
<declrcns>
int
<sntnc> ; <sntnc> ; <sntncs> <sntncs>
int <asignacion>
<sntnc>
<declrcn>
<salida>
<tipo>
<asignacion>
int
<id>
<id> := <expr>
100
<const.int>
A. Sem.
Cdigo
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
199
200
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.
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
t:i L
t:i L
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.
203
204
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=}
205
206
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;
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
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
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
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.
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
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
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.
212
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).
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
<lista>
p:3
<lista_interna>
p:2
<lista_interna> p:1
<lista_interna> p:1
<lista_interna> p:0
<lista_interna> p:0
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).
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
<lista>
t:4
<lista_interna>
a:1 d:4
<lista_interna>
a:1 d:2
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.
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.
218
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.
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
v:0 i t:i x ,
i y
v:0 t:i
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.
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.
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
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 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.
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.
224
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.
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
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: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.
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
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.
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
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
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.
232
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.
233
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.
234
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.
235
236
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-
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
238
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)
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: { };
240
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.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.
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
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).
244
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.
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
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.
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
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.
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
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
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-
251
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
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
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
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.
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
; ; ; exp PILA
<exp>
jz fin_then#
; ; ;
<instruccion>
fin_then#:
1 <instruccion1> else
<instruccion2> 3
; ; ; exp PILA
<exp>
jz fin_then#
; ; ;
<instruccion1>
<instruccion2>
fin_ifelse#:
(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)
257
<exp > do
<instruccion> end 3
inicio_while#:
; ; ; exp PILA
<exp>
jz fin_while#
; ; ;
<instruccion>
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)
; ; ; ; ;
258
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.
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.
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
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.
261
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
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.
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
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.
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.
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
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.
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
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.
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
269
Pila auxiliar
(+,a,b,t1)
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
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.
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
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.
273
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)
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
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;
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
275
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
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-
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 ...
FUENTE GOTO et1 ... GOTO et1 ... GOTO et1 ...
CUDRUPLAS (p) (TR,,,) ... (q) (TR,p,,) ... (r) (TR,q,,) ...
278
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;
279
(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,,)
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
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)
Figura 6.30. Generacin de cudruplas para la instruccin for x=n1,n2,n3 do a:=a+1; end.
281
282
Cudrupla (*,a,b,t1)
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)
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;
283
2.
3.
4.
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.
7.
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.
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.
284
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
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:
288
;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)
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.
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
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)}
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)
(=,t3, , f)
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
La comparacin de la secuencia original de cudruplas con el resultado del algoritmo muestra claramente la optimizacin obtenida.
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.
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.
294
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
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
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-
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
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.
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
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
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.
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.
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.
302
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.
303
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
(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.
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.
306
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)
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
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.
309
310
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.
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.
311
312
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
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
313
I1
R1
I2
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
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.
314
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.
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
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).
320
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.
322
Captulo 8. Intrpretes
323
Analizador semntico
Analizador morfolgico
Ejecucin de cdigo
Programa fuente
Analizador sintctico
Tabla de identificadores
Gestin de memoria
Proceso de errores
324
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.
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
326
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.
328
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.
329
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
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.
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
332
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:
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.
334
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.
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.
338
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
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
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-
341
Clave
Valor
abcd a b c d \0
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
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
343
SP
r Espacio libre
[BP-14] [BP-16]
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
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
345
Programa Objeto:
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
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.
347
348
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.
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
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
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)
351
a)
b) A
c) A
d) A 3 x A
e) A 3 x A
f) A
2 + 3 x A
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
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
353
a)
b) Espacio asignado
Lista de 1 kByte
Lista de 1 kByte
Lista de 2 kBytes
Lista de 2 kBytes
Lista de 4 kBytes
Lista de 4 kBytes
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
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,
jamin/Cummings.
[4] Gries, D. (1971): Compiler construction for Digital Computers, New York, John Wiley and
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
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
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
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