11.07.2015 Views

tesis_uam/Guia para hacer compiladores _UAM7403.pdf - cedip

tesis_uam/Guia para hacer compiladores _UAM7403.pdf - cedip

tesis_uam/Guia para hacer compiladores _UAM7403.pdf - cedip

SHOW MORE
SHOW LESS

Create successful ePaper yourself

Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.

CASAABIERTA AL TIEMPODIVISION DE CIENCIAS BASICAS E INGENIERIALIC E N CIA1 U RA E N C OM P UTACIO NPROYECTO DE INVESTIGACION144172AUIA PARA REALKAR COMPILADORESALUMNO : AGUILAR CORNEJO MANUEL.7ASESOR : M. EN C. SERGiO PREZ RODEA.ABRIL DE 1993.


INDICEINDICE ..................................................... 1CAPITULO I . BOSQUEJO GENERAL ............................... 21.1 introducción ........................................... 21.2 Las fases de un compilador ............................. 41.2.1 Análisis léxico .................................. 51.2.2 Análisis sintáctico .............................. 51.2.3 Análisis semántica ............................... 61.2.4 Administración de la tabla de símbolos ........... 71.2.5 Detección e información de errores ............... 71.2.6 Generación de código intermedio .................. 81.2.7 Optimización de código ........................... 91.2.8 Generación de código ............................. 9CAPITULO I1 . ANALISIS LEXICO ............................... 122.1 Expresiones regulares .................................. 142.2 Definiciones regulares ................................. 182.3 Autómatas .............................................. 192.4 LEX (Generador de analizadores léxicos) ................ 282.4.1 Algunos ejemplos de especificación <strong>para</strong> LEX ...... 312.4.2 Ejemplo de un analizador léxico .................. 36CAPITULO I11 . ANALISIS SINTACTICO ......................... 383.1 Gramáticas independientes del contexto ................. 383.2 Análisis sintáctico descendente ........................ 423.2.1 Gramáticas LL(1) ................................. 483.2.1.1 Cálculo del FIRST ....................... -503.2.1.2 Otras características de lasgramáticas LL(1) ......................... 543.2.1.3 Cálculo del FOLLOW ....................... 553.3 Análisis sintáctico ascendente ......................... 573.3.1 Implementación por medio de una pila de análisissintáctico por desplazamiento y reducción ........ 613.3.2 Conflictos durante el análisis sintáctico ........ 623.3.3 Algoritmo de análisis sintáctico LR(1) ........... 643.3.4 Construcción de tablas de análisis sintáctico .... 653.3.4.1 La operación cerradura ................... 663.3.4.2 La operación ir-a ........................ 683.3.4.3 Tablas de análisis sintáctico LR ......... 703.4 YACC (Generador de analizadores sintácticos) ........... 723.4.1 Especificaciones básicas ......................... 733.4.2 Acciones ......................................... 753.4.3 Ejemplos de una gramática completa ............... 781- *. -,ll-.llllll. L__- ............... . . .........


I#TRODüCCIO#A grandes rasgos, un compilador es un programa que lee unprograma escrito en un lenguaje fuente (lenguaje expresivo dealto nivel), y lo traduce a un programa equivalente en otrolenguaje, el lenguaje objeto (vease figura 1.1) . Como parteimportante de este proceso de traducción, el compilador informa asu usuario de la presencia de errores en el programa fuente.Programa Fuente(Lenguaje expresivo ---de alto nivel).Programa Objeto(Lenguaje de bajonivel) .Fig. 1.1Función de un compilador.Los <strong>compiladores</strong> a menudo se clasifican como de una pasada, demultiples pasadas, de carga y ejecución, de depuración o deoptimización, dependiendo de como háyan sido construidos o de quefunción depende que realizan. A pesar de esta aparentecomplejidad, las tareas básicas que debe realizar cualquiercompilador son escencialmente las mismas. Al comprender talestareas, se pueden construir <strong>compiladores</strong> de gran diversidad delenguaje fuente y máquina objeto útilizando las mismas técnicasbásicas.Nuestro conocimiento sobre como organizar y escribir<strong>compiladores</strong> ha aumentado mucho desde que comenzaron a aparecerlos primeros <strong>compiladores</strong> a pricipios de los años cincuenta. Esdificil dar una fecha exacta de la aparición de el primercompilador, Por que en un principio gran parte de el trabajo deexperimentación y aplicación se realizó de manera independientepor varios grupos. Gran parte de los primeros trabajos decompilación estaba relacionada con la traducción de formulasaritméticas a código máquina.En la decada de 1950, se consideró a los <strong>compiladores</strong> comoprogramas notablemente difíciles de escribir. El primercompilador de FORTRAN, por ejemplo, necesitó <strong>para</strong> suimplementación 18 años de trabajo en grupo (Backus y otros[1975]). Desde entonces, se han descubierto técnicas sistemáticas<strong>para</strong> manejar muchas de las importantes tareas que surgen en lacompilación. También se han desarrollado buenos lenguajes deimplementación, entornos de programación y herramientas desoftware (como generadores de analizadores léxicos ysintácticos). Con estos avances, puede <strong>hacer</strong>se un compilador realincluso como proyecto de estudio, en un curso sobre diseño de<strong>compiladores</strong>.3


En la compilación hay .dos partes: análisis y sín<strong>tesis</strong>. Laparte del análisis divide al programa fuente en sus elementoscomponentes y crea una representación intermedia del programafuente. La parte de la sín<strong>tesis</strong> construye el programa objetodeseado a partir de la representación intermedia. De las dospartes, la sín<strong>tesis</strong> es la que requiere las tecnicas másespecializadas. Se examinará el análisis y se esbozará la formade sintetizar el código objeto en un compilador estándar.1.2 LAS FASES DE UN COMPILADOR.Conceptualmente un compilador opera en fases, cada una de lascuales transforma el programa fuente de una representación enotra. En la figura 1.2 se muestra una descomposición típica deun compilador. En la práctica, se pueden agrupar algunas fases, ylas representaciones intermedias entre las fases agrupadas nonecesitan ser construidas explícitamente.Programa FuenteIVAnalizador Léxico 1IV!Administrador L Analizador Semantic0 Manej adorde la tabladede símbolosIErroresIV \t-------j\IOptimizador de . /Código Intermedior4


proposiciónde asignaciónI:=1 iidentificadorexpresiónIpos 1 ción +1 \expresiónexpresiónIIidentificadorinicialI*7expresiónIInúmeroIexpresiónidentificadorvelocidadIvelocidadFig 1.3 Arbol de análisis sintáctico <strong>para</strong> una expresiónLa estructura jerárquica de un programa normalmente se expresautilizando reglas recursivas. Por ejemplo se pueden dar lassiguientes reglas como parte de la definición de expresiones:1.- Cualquier identificador es una expresión.2.- Cualquier número es una expresión.3.- Si expresión1 y expresión2 son expresiones entonces tambiénlo son:expresión1 + expresión2expresión1 * expresión2( expresión ).La división entre análisis léxico y análisis sintáctico esalgo arbitraria. Generalmente se elige una división quesimplifique la tarea completa de el análisis.1.2.3 Análisis Semántico.La fase de análisis semántico revisa el programa fuente <strong>para</strong>tratar de encontrar errores semánticos y reúne la informaciónsobre los tipos <strong>para</strong> la fase posterior de generación de código.En ella se utiliza la estructura jerárquica determinada por lafase de análisis sintáctico <strong>para</strong> identificar los operadores yoperandos de expresiones y proposiciones.Un componente importante de el análisis semántico es laverificación de tipos. Aquí el compilador verifica si cada6


operador tiene operandos permitidos por la especificación de ellenguaje fuente. Por ejemplo, las definiciones de muchoslenguajes de programación requieren que el compilador indique unerror cada vez que se use un número real como indice de unamatriz .1.2.4 Administración de la Tabla de Símbolos.Una función escencial de un compilador es registrar losidentificadores utilizados en el programa fuente y reunirinformación sobre los distintos atributos de cada identificador.Estos atributos pueden proporcionar información sobre la memoriaasignada a un identificador, su tipo, su dunbito (la parte delprograma donde tiene validez) y, en el caso de nombres deprocedimientos, cosas como el número y tipos de sus argumentos,el método de pasar cada argumento (por ejemplo, por referencia) yel tipo que devuelve, si los hay.Una tabla de símbolos es una estructura de datos que contieneun registro por cada identificador, con los campos <strong>para</strong> losatributos del identificador. La estructura de datos permiteencontrar rápidamente el registro de cada identificador yalmacenar o consultar rápidamente datos de ese registro.Cuando el analizador léxico detecta un identificador en elprograma fuente, el identificador se introduce en la tabla desímbolos. Sin embargo, normalmente los atributos de unidentificador no se pueden determinar durante el análisis léxico.Por ejemplo, en una declaración en Pascal comovar posición, inicial, velocidad : real:el tipo real no se conoce cuando el analizador léxico encuentraposición, inicial, y velocidad.Las fases restantes introducen información sobre losidentificadores en la tabla de símbolos y después la utilizan devarias formas. Por ejemplo, cuando se está haciendo el análisissemántico y la generación de código intermedio, se necesita sabercuáles son los tipos de los identificadores, <strong>para</strong> poder comprobarsi el programa fuente los usa de una forma válida y así podergenerar las operaciones apropiadas con ellos. El generador decódigo, por lo general, introduce y utiliza informacióndetallada sobre la memoria asignada a los identificadores.1.2.5 Detección e Información de Errores.Cada fase puede encontrar errores. Sin embargo, después dedetectar un error, cada fase debe de tratar de alguna forma eseerror, <strong>para</strong> poder continuar la compilación, permitiendo ladetección de más errores en el programa fuente. Un compilador que7I . . . . . ..-I.__" --- .- ... . . . ... ~-


se detiene cuando encuentra el primer error, no resulta tan Útilcomo debiera.Las fases de análisis sintáctico y semántico por lo generalmanejan una gran porción de los errores detectables por elcompilador. La fase léxica puede detectar errores donde loscaracteres restantes de la entrada no forman ningtln componenteléxico del lenguaje. Los errores donde la cadena de componentesléxicos violan las reglas de estructura (sintaxis) del lenguajeson determinados por la fase de análisis sintáctico. üuarante elanálisis semántico el compilador intenta detectar construccionesque tengan la estructura sintáctica correcta, pero que no tengansignifcado <strong>para</strong> la operación implicada, por ejemplo, si seintenta sumar dos identificadores, uno de los cuales es el nombrede una matriz, y el otro, el nombre de un procedimiento.1.2.6 Generación de Código intermedioDespués de los análisis sintáctico y semántico, algunos<strong>compiladores</strong> generan alguna representación intermedia explícitadel programa fuente. Se puede considerar esta representaciónintermedia como un programa <strong>para</strong> una máquina abstracta. Estarepresentación intermedia debe tener dos propiedades importantes;debe ser fácil de producir y fácil de traducir al programaobjeto .La representación intermedia puede tener diversas formas. Encápitulos posteriores se trata una forma intermedia llamdalecódigo de tres direcciones", que es como el lenguaje ensamblador<strong>para</strong> una máquina. El código tres direcciones consiste en unasecuencia de instrucciones, cada una de las cuales tiene comomáximo tres operandos. Veamos un ejemplo.templ : = EntAReal(60)temp2 := id3 * templtemp3 := id2 + temp2id1 := temp3Esta representación intermedia tiene varias propiedades.Primera cada instrucción de tres direcciones tiene a lo sumo unoperador, además de la asignación. Por tanto, cuando se generanesas instrucciones, el compilador tiene que decidir el orden enque deben efectuarse las operaciones; la multiplicación precede ala adición. Segunda, el cornpilador debe generar un nombretemporal <strong>para</strong> guardar los valores calculados por cadainstrucción. Tercera, algunas instrucciones de "tres direcciones"tienen menos de tres operandos, por ejemplo, la primera y laúltima instrucción.Posteriormente se tratarán las principales representacionesintermedias empleadas en los <strong>compiladores</strong>. En general, estasrepresentaciones deben <strong>hacer</strong> algo más que calcular expresiones;también deben manejar construcciones de flujo de control yllamadas a procedimientos.8


1.2.7 Optimización de Código.La fase de optimización de código trata de mejorar el códigointermedio, de modo que resulte un código de Mquina más rápidode ejecutar. Algunas optimizaciones sontriviales. Por ejemplo, unalgoritmo natural genera el código intermedio (1.1) utilizandouna instrucción <strong>para</strong> cada operador de la representación de árboldespues del análisis semántico, aunque hay una forma mejor derealizar los mismos cálculos utilizando las dos instruccionestempl := id3 * 60.0id1 := id2 + templEste sencillo algoritmo no tiene nada de malo, puesto que elproblema se puede solucionar en la fase de optimización decódigo. Esto es, el compilador puede deducir que la conversión de60 de entero a real se puede <strong>hacer</strong> de una vez por todas en elmomento de la compilación, de modo que la operación EntAReal sepuede eliminar. Además, temp3 se usa sólo una vez, <strong>para</strong>transmitir su valor a idl. Entonces resulta seguro sustituir id1por temp3, a partir de lo cual la última proposición de (1.1) nose necesita y se obtiene el código de (1.2).Hay mucha variación en la cantidad de optimización de códigoque ejecutan los distintos <strong>compiladores</strong>, una parte significativadel tiempo del compilador se ocupa en esta fase. Sin embargo, hayoptimizaciones sencillas que mejoran senciblemente el tiempo deejecución del programa objeto sin retardar demasiado lacompilación.1.2.8 Generación de Código.La fase final de un compilador es la generación de códigoobjeto, que por lo general conciste en código máquinarelocalizable o código ensamblador. Las posiciones de memoria seseleccionan <strong>para</strong> cada una de las variables usadas por elprograma. Después cada una de las instrucciones intermedias setraduce a una secuencia de instrucciones de máquina que ejecutanla misma tarea. Un aspecto desicivo es la asignación de variablesa registros.Como ejemplo, si utilizamos los registros AX y BX en latraducción del código (1.2) a lenguaje ensamblador <strong>para</strong> lafamilia de procesadores 8086-8088 tendríamos:MOV AX,id3MUL 60MOV BX, id2ADD AX,BXMOV id1,AX


Este código traslada el contenido de id3 a AX en la primerainstrucción, en la segunda multiplica Ax por 60, en la terceramueve el contenido de id2 a BX, en la cuarta suma Ax y BX (id2 +tempi), y por Último en la quinta deposita el resultado de lasuma en la variable idl. Posteriormente se tratará con másdetalle la generación de código. Como ejemplo final del capitulointroductorio ilustraremos como una linea de código fuente estratada a través de cada fase de compilación


posición := inicial + velocidad * 60IAnalizador léxicoIid1 := ida'+ id3 * 60:=/------tid1 +-id2 *:=rc------4-id1 +id2 */ \id3 EntARealII601Gen. de código Inter, ftempl : = EntAReal(60)temp2 := id3 * templtemp3 := id2 + temp2id1 := temp3IIposicion 0. .inicial . * *velocidad ...IItempl := id3 * 60.0id1 := id2 + templIGenerador de códigoIMOV AX, id3MüL 60MOV BX, id2ADD AX, BXMOV id1,AXFig. 1.4 Traducción de una proposición.11


CAPITUW 11.ñblAL1818 LEXICO.El analizador léxico es la primera fase de un cornpilador. Suspricipales funciones son:1.- Leer los caracteres de entrada del texto analizado y elaborarcomo salida una secuencia de componentes léxicos (tokens).2.- Eliminar los delimitadores del texto analizado, tal como,espacios en blanco, caracteres de tabulador, saltos de lineay comentarios de entre otros.3.- Puesto que esta es la primera fase que lee el c6digo fuente aanalizar, deberá de llevar una contabilidad sobre el númerode veces que se encontró el caracter de salto de linea, estocon el fin de que cuando se presente una secuencia invalidade caracteres sepamos en que linea ocurrió.4.- Detectar secuencias invalidas de caracteres, generando unmensaje de error en la linea donde se encontró.La función más relevante de las mencionadas, es la primera,consistente de leer los caracteres de entrada y elaborar comosalida una secuencia de componentes léxicos que utiliza elanalizador sintáctico <strong>para</strong> <strong>hacer</strong> el análisis. Esta interacción,esquematizada en la figura 2.1, suele aplicarse convirtiendo alanalizador léxico en una subrutina del analizador sintáctico.Recibida la orden "obten token" del analizador sintáctico, elanalizador léxico lee los caracteres de entrada hasta que puedaidentificar el siguiente componente léxico, o bien, se encuentrecon una secuencia de caracteres invalida <strong>para</strong> lo cual mandará unmensaje de error.ProgramafuenteTokenobten el sig.AnalizadorsintácticoTabla deSímbolosFig. 2.1 Interacción de un analizador léxico con un analizadorsintáctico.12


Componentes léxicos, patrones y lexemas.Cuando se menciona el análisis, ,los terminos, "componenteléxico" (token), npatrón", y "lexeman se emplea con significadosespecificos. En la fig. 2.2 aparecen ejemplos de dichos usos. Engeneral, hay un conjunto de cadenas en la entrada <strong>para</strong> el cual seproduce como salida el mismo componente lbxico. Este conjunto decadenas se describe mediante una regla llamada patrón asiciado alcomponente léxico. Se dice que el patrón concuerda con cadacadena del conjunto. Un lexema es una secuencia de caracteres enel programa fuente con la que concuerda el patrón <strong>para</strong> uncomponente léxico. Por ejemplo, en la proposición de pascalconst pi = 3.1416;la subcadena pi es un lexema <strong>para</strong> el componente léxicoU ident if icador" .COMPONENTELEXICOconstifrelaciónidnúmliteralLEXEMAS DEEJEMPLOconstifpi, cuenta, D23.1416, O, 6.02lWacía memoria"DESCRIPCION INFORMAL DELPATRONconstif< o = o >letra seguida de letras y dígitoscualquier constante numéricacualquier cosa entre Hn.Fig. 2.2 Ejemplos de componentes léxicos.Los componentes léxicos se tratan como símbolos terminales delas gramáticas del lenguaje fuente, con noares en negritas <strong>para</strong>representarlos. Los lexemas <strong>para</strong> el componente léxico queconcuerdan con el patrón representan cadenas de carcateres en elprograma fuente que se pueden tratar juntos como una unidadléxica .Un patrón es una regla que describe el conjunto de lexemas quepueden representar a un determinado componente léxico en losprogramas fuentes. El patrón <strong>para</strong> el componente léxico const dela figura 2.2 es simplemente la cadena sencilla const quedeletrea la palabra clave. El patrón <strong>para</strong> el componente léxicorelación es el conjuntom de los seis operadores relacionales dePascal. Para describir con presición los patrones <strong>para</strong>componentes léxicos más complejos, como id (<strong>para</strong> identificador) ynúm (<strong>para</strong> número), se utilizará la notación de expresionesregulares desarrolladas en parrafos posteriores.13


I " ...I I ~ . ~El analizador léxico recoge información sobre los componentesléxicos así como de sus atributos asociados. En la prdctica, loscomponentes léxicos suelen tener un solo atributo -un apuntadorala entrada de la tabla de símbolos donde se guarda lainformación sobre el componente léxico; el apuntador se convierteen el atributo del componente léxico. A efectos de diagnóstico,puede considerarse tanto el lexema <strong>para</strong> un identificador como elnúmero de linea en el que éste se encontró por primera vez. Estosdos elementos de información se pueden almacenar en la entrada dela tabla de símbolos <strong>para</strong> el identificador.Errores Léxicos.Son pocos los errores que se pueden detectar simplemente en elnivel léxico porque un analizador léxico tiene una visión muyrestringida de un programa fuente. Si aparece por primera vez lacadena fi en un programa en C en el contextofi ( a == f(x) ) ...Un analizador léxico no puede distinguir si fi es un error deescritura de la palabra clave if o si es un identificador defunción no declarado. Como fi es un identificador válido, elanalizador léxico debe devolver el componente léxico de unidentificador, y dejar que alguna otra fase del compilador seencarge de estos errores.2.1 EXPRESIONES REGULARES.Definición: Un alfabeto es un conjunto finito de símbolos.Ejemplo:s = { o, 1 }S = { a, b, c )ejemplos posibles de símbolos: el conjunto de letras, el conjuntode números, el conjunto de letras y de números. Los códigos ASCIIy EBCDIC son dos eejemplos de alfabetos de computador.Definición: Una cadena es cualquier secuencia finita desímbolos de algún alfabeto.Ejemplo: Sea el alfabeto S = { O, 1 ), entonces posiblescadenas de este alfabeto serían:x = 011; y = 0000; z = 1010; t = 0101;x = o; y = 1; z = 1111111100000000, etc.14.- .-I.- . _. ... .".- - - . . . -. - .-


La longuitud de una cadena "sn representada por Is¡, es el númerode símbolos que contiene del alfabeto del cual fuen formada. Porejemplo la cadena O11 tiene longuitud 3. La cadena vacía,representada por E, es una cadena especial de longuitud cero.Sea s una cadena, entonces definamos:Prefijo de s: Una cadena que se obtiene eliminando cero o mássímbolos desde la derecha de la cadena si porejemplo ban es un prefijo de la cadena bandera.subfijo de s : Una cadena que se forma suprimiendo cero o mássímbolos desde la izquierda de una cadena si porejemplo, era es subfijo de bandera.subcadena de s: Una cadena que se obtiene suprimiendo un prefijoy un subfijo de si por ejemplo, ande es unasubcadena de bandera. Todo prefijo y subfijo de ses una cadena de s, pero no toda subcadena de ses un prefijo o subfijo de s. Para toda cadena s,tanto s como E son prefijos, subfijos ysubcadenas de s.prefijo, subfijo o subcadenas propios de s : Cualquier cadena novacía x que sea, respectivamente, un prefijo,subfijo o subcadena de s tal que s x.subsecuencia de s : Cualquier cadena formada mediante laeliminación de cero o más símbolos nonecesariamente contiguos a s : por ejemplo, bandaes una subsecuencia de bandera.Si x e y son cadenas, entonces la concatenación de x e y, quese denota por x y, es la cadena que resulta de agregar y a x. Porejemplo, si x = caza e y = fortunas, entonces la concatenaciónde xy = cazafortunas y yx = fortunascaza. La cadena vacía es elelemento identidad <strong>para</strong> la operación de concatenación, esto es,SE = ES = S.Considerando la concatenación como producto, cabría definir laexponenciación de cadenas como: SO sería E y <strong>para</strong> i > Otendríamos si = si-1s. Dado que Es = 8, sl = s. Entonces s2 =ss, s3 = sss, etc.Definición: Un lenguaje es cualquier conjunto de cadenasformadas con los símbolos de algún alfabeto.Ejemplo:L= ( O , 1 } M = ( 00, 11 } N = ( E }15


Sean L, D y M lenguajes como se definieron anteriormente.Existen varias operaciones importantes definidas sobre loslenguajes como :1.-2.-3.-4.-Unión:L U M = ( x l x E L & x E M )L U M = { O, 1, 00, 11 }Concatenación:LUM=(xiylxEL& y E M }L U M = ( 000, 001, 100, 111 }L2 = ( 00, 01, 10, 11 }L3 = L L L = ( 000, 001, 010, 011, 0 . . }Cerradura de Kleene (concatenación de cero o más cadenas dealgún lenguaje L) .9 iL*=U Li=OCerradura positivaalgún lenguaje)L+=9 iu Li=iL+ = L1 u L2 u L3 u...01, 10, 11, ...}(concatenación de una o más cadenas de....L+ = ( o, 1, 00, 01, 10, 11, ...}Definición: Sea E un alfabeto. La expresión regular sobre E y losconjuntos que ella denotan son definidos recursivamente comosigue:1.- 0 es una expresión regular y denota al lenguaje compuesto delconjunto vacio.2.- E es una expresión regular y denota al lenguaje compuestopor el conjunto ( E }.3.- Para cada a E E, a es una expresión regular que denota allenguaje compuesto por el conjünto { a }. (Aunque se usa lamisma notación <strong>para</strong> la tres , técnicamente ¡a expresiónregular a es distinta de la cadena a o del símbolo a. Elcontexto aclarará a que término nos referimos).4.- Si R y S son expresiones regulares denotando los lenguajesL(R) y L(S) respectivamente, entonces, (rl s) , (rs) y r* sonexpresiones regulares que denotan los lenguajes L(R) U L(S),L(R)-L(S) y L(R) * respectivamente.Se dice que un lenguaje designado por una expresión regular esun conjunto regular. En la escritura de expresiones regulares sepueden omitir algunos paren<strong>tesis</strong> si tomamos en cuenta la16


precedencia de los operadores. El * tiene mayor precedencia quela concatenación y que el I, y la concatenación tienen mayorprecedencia que . Por ejemplo ( ( O (l*) ) + O ) puede serescrito como: 01*Ej emplos :Sea E = { O, 1 ),el alfabeto.Todos los números divisible entre 10.Los números divisibles entre 10 son:(10, 100, 110, 1000, lolo,..}Por lo tanto tenemos la siguiente expresión1 (O 11) *oTodos los números que comienzan con un 1.El conjunto de los números que comienzan con 1 es{I, 10, 11, 101, 100, 110, ... }Por lo tanto tenemos la siguiente expresiónTodos los números imparesEl conjunto de los números impares es{ 1, 11, 101, 111, 1001, 1011, . . o )Por lo tanto tenemos la siguiente expresiónTodas las cadenas de ceros o unos que tengan al menos un cero.(01 1) *O(Ol1) *Todas las cadenas de ceros y unos cuyo segundo símbolo deizquierda a derecha sea ceroTodas las cadenas de ceros y unos que no incluyan la subcadena01.Las cadenas que no incluye la subcadena O1 son11111.. 00000.. - 11100.. .Esto es,1*0*


I_ ---Un reconocedor <strong>para</strong> un lenguaje L recibe como entrada unacadena x y decide si ésta pertence o no a L.Si dos expresiones r y s representan al mismo lenguaje, se diceque r y s son equivalentes y se escribe r = s. Por ejemplo, (alb)= (bla).2.2 DEFINICIONES REGULARES.Si E es un alfabeto de símbolos básicos, entonces unadefinición regulares una secuencia de la forma.ejemplo: ------ > donde cada di es un nombre distinto, y cada ri es una expresiónregular sobre los símbolos de E U {dl,d2,..,dn)Ejemplo: El conjunto de los identificadores de Pascal, es elconjunto de cadenas de letras y dígitos que comienzan con unaletra. A continuación se da una definición regular <strong>para</strong> eseconjunto .letra ----->AlBlCl ... lZlalbl ... Izdígito ----> 011121 ... 10id ------- > letra (letra I dígito) *Ejemplo: los números sin signo en Pascal son cadenas, como6657, 93.79, 33.96737, Ó 9.813-4. La siguiente definición regularproporciona una especificación presisa <strong>para</strong> esta clase decadenas .dígito -----> 0111 ... 10dígitos ---- > dígito.dígito*fracción-optativa ---- > .dígitosl Eexponente-optativo----> ( E (+ I - I E) dígitos) I Enumero ---->dígitos fracción-optativa exponente-optativo18


2.3 AUTOMATAS.Un reconocedor de un leguaje es un programa que toma comoentrada una cadena x y responde %erdaderoN si x es una frase (otoken)del lenguaje, y llfalson si no lo es. Tal como se ilustra enla siguiente figuraEste reconocedor de lenguajes será utilizado por el analizadorsintáctico <strong>para</strong> analizar la segunda fase de compilación. Esto es,el analizador sintáctico pedirá un token al reconocedor delenguajes (analizador léxico). Este leerá el código fuente deentrada y si logra reconocer un token lo enviará al analizadorsintáctico <strong>para</strong> que lo procese. En otro caso, mandará un mensajede error y procesará éste. El proceso continua de la misma manerahasta que el código es analizado totalmente. Tal como se ilustraen la siguiente figuraProgramatabla dedonde: Rt es el reconocedor de tokens.Ed es el reconocedor de delimitadores.Se puede traducir una expresión regular en un reconocedor delenguajes construyendo un diagrama de transiciones generalizadollamado "Autómata finito". Un autómata finito puede serB1deterministall o Itno deterrninistat1, donde no deterministasignifica que en un estado se puede dar el caso de tener más deuna transición <strong>para</strong> el mismo símbolo de entrada.Tanto los autómatas finitos deterministas como los nodeterministas pueden reconocer con precisión a los conjuntosregulares. Por tanto, ambos pueden reconocer con con precisión loque denotan las expresiones regulares. Sin embargo, hay unconflicto entre tiempo y espacio: mientras que un autómata finitodeterminista puede dar reconocedores más rápidos que un nodeterminista, un autómata finito determinista puede ser muchomayor que un autómata no determinista equivalente.


DEFINICION. Un autómata finito determinista (DFA) es un modelomatemático formado por la quintupla (Q, E, s, qo, F), donde:1) Q es un conjunto finito de estados.2) E es el alfabeto de entrada.3) s es una función de transición Q X E ---> Q4) qo E Q, es el estado inicial.5) F C Q es el conjunto de estados finales.NOTA: Los estados terminales se representan con doble circulo, ylos no terminales con circulo sencillo.Ejemplo 1:Por lo tanto tenemos el siguiente autómatarbDEFINICION. Se dice que un autómata acepta ó reconoce una cadenax, si comenzando con el estado inicial, después de seguir lastransiciones indicadas por los símbolos de la cadena x se llega aun estado final.Ejemplo 2: Tomemos como autómata, el mostrado en el ejemploanterior, entonces:a) La cadena x=O1 es aceptada, ya que, comenzando en q0, alleerel símbolo O tomamos la función de transición s(q0,O) ypasamos al estado O; a continuación leemos un 1 y tomamos lafunción de transición s(q0,l) pasando al estado 1; puesto que yano hay más símbolos en la cadena x, revizamos si en elestadodonde estamos hubicados es terminal o no terminal; como elestado es terminal, la cadena x=O1 es aceptada.b) La cadena x=100 no es aceptada, pues, partiendo del estadoinicial q0 y siguiendo las funciones de transición visitamos losestados qlqOq0 terminando en q0; puesto que q0 no es un estadofinal, entonces la cadena no es aceptada por este autómata (o no20


pertenece al lenguaje reconocido por el autómata).DEFINICION. El lenguaje reconocido por un autómataes el con juntode cadenas que acepta.Ejemplo 3: En el ejemplo 1 tenemos un autómata que aceptacualquier cadena que termine con un 1, pues, si estamos en elestado O y nos llega un 1 pasamos al estado uno, y si estamos enel estado uno y nos llega un 1 nos quedamos en ese estado, elcual es un estado terminal. En este caso particular el lenguajeaceptado por éste autómata es el expresado por la expresiónregular (O I 1) *IEjemplo 4: De elvemos que el lenguaje reconocido por éste, es el expresado porx=l* o 1*Ejemplo 5: Del siguiente autómatavemos que genera el lenguaje x = o(oo)*~EJERCICIOS.Considerese el alfabeto E = { O, 1 }1) Dibujar el DFA que reconozca el lenguaje { E }21


2) Dibujar el DFA que reconozca el lenguaje ( O )3) Dibujar el DFA que reconozca el lenguaje { O1 }4) Dibujar el DFA que reconozca el lenguaje 1*, esto es, { E, 1,11, 111, ... )5) Dibujar el DFA que reconozca las cadenas de la forma xz dondex consta de un número par de ceros y z de un número impar deunos.22


6) Dibujar el DFA que reconozca las cadenas de ceros y unos queno tienen 2 ceros concecutivos.Sea el alfabeto de entrada E = ( ASCII ) y las siguientesdefiniciones regulares las que denotaran nuestro pequeñolenguaje.1 ----> albl ... lzlAlBl ... l Zd ----> 1121 ... 1910t ---->delimitadores --->Ii(lJd)*siI =(blCRI(l*))*donde: I denota la expresión ORb denota caracteres en blanco.CR denota el caracter de retorno de carro (enter)1 denota cualquier letrad denota cualquier dígitot denota los tokens válidos23


El autómata generado por estas definiciones regulares y quedenota el mismo lenguaje es el siguiente


La forma en que el autómata es implementado y reconoce ellenguaje, es la siguiente: se crea un buffer donde se almacenaráuna linea de código a analizar, se verificará si cada una de lascadenas de ésta linea es aceptada por el autómata; si es así seregresará un token, tal como lo muestra la figuraProgramafuenteAnalizadorEjemplo:Programa Fuentesi (comentario)a=#bSe lee la primer linea del código fuente y se almacena en elbufferaPsaPsVVAapi1) Se comienza con los apuntadores api y pas apuntando al iniciodel buffer. Se comienza por incrementar aps y che canos a queestado pasamos al encontrar una 'sr (estados), puesto que esentrada válida incrementamos aps; leemos el siguiente caracter'it, vemos si es entrada válida, puesto que es así pasamos alestado correspondiente (estado 3). Incrementamos aps leemos elcaracter apuntado por aps '(', puesto que no hay transición <strong>para</strong>ese caracter y puesto que estamos en un estado terminal, estacadena ya forma un token, el cual regresamos (al analizadorsintáctico), pero antes incrementamos el apuntador api a dondeapunta aps, <strong>para</strong> tratar de encontrar el siguiente token y ademasretornamos al estado inicial (estado O).2) mesto que api y aps apuntan a la localidad donde se encuentra'(, pasamos al estado 5 y avanzamos aps. Leemos la palabra'comentario' avanzando sucesivamente aps sin realizar cambio deestado; hasta que aps apunta a ')' lo cual provoca que pasemos alestado O. Esto es, ignoramos el comentario; después de leer elretorno de carro el cual nos deja en el mismo estado, cargamos25


el buffer con la siguiente linea a analizar y se inicializan apiy aps a 1.3) El buffer se cargo conaPsbuffer [*)Aapileemos el caracter apuntado por aps 'a', el cual provoca uncambio de estado (estado 1) 8 avanzamos aps y leemos el caracterapuntado por aps I='- # puesto que no hay transición <strong>para</strong> esesímbolo y puesto que estamos en un estado terminal (estado l),enviamos un token (puesto que encontramos un identificador) alanalizador sintáctico, incrementamos api a aps y pasamos alestado O <strong>para</strong> comenzar el análisis nuevamente.Leemos el caracter apuntado por aps '#'# el cual nos cambia alestado 6, incrementamos aps; leemos el símbolo apuntado por aps'b8; puesto que no hay transición <strong>para</strong> ese símbolo y no estamosen un estado final, entonces ocurrió un error (símbolo o cadenano válida). La forma de manejar el error es: pasamos al estadoinicial (estado O), nos posicionamos en el siguiente caracter ymandamos un mensaje de error.La implementación del autómata en un algoritmo lo mostraremosa continuación, sólo que ademas de lo arriba señalado haremos unmanejo adicional con las cadenas reconocidas por el autómata. Acada cadena válida (no tomando en cuenta los comentarios) leasignaremos un código, y además a los identificadores losintroduciremos en una tabla de símbolos con su código y unatributo adicional (su posición en el código fuente).token l(l[d)*Código1Atributoposición23declaramos api y aps globalesFUNCION LEXComienzaapi


token=l ;ap-id


2.4 LEX (Generador de Analizadores léxicos).Lex es un generador de programas capaces de realizar el procesamientoléxico de archivos de texto, es decir programas quepueden conocer ciertos patrones dentro del conjunto de caracteresque forman parte de un archivo de texto y realizar manipulacionessobre tales cadenas. Los analizadores l6xiws son programas quecaen dentro de ésta categoría, pues su función consiste en descubrirlas secuencias de caracteres que forman tokens o delimitadoresválidos de algún lenguaje fuente y realizan ciertas tareassobre tales cadenas: eliminan los delimitadores de la entrada yasocian un código númerico a cada token.Lex recibe como entrada un conjunto de expresiones regulares yacciones proporcionadas por el usuario (que en adelante llamaremosEspecificación de entrada) y produce como salida un programaescrito en lenguaje C llamado "yylex.cN. Cada expresión regulartiene asociado un conjunto de acciones que deben ser realizadascada vez que una cadena con la forma indicada por la expresiónregular se a encontrada en la entrada (ver figura)Especificación ----de entradaLéxico> YYLEX.CEntrada ---- ----- > SalidaFig. 2.4 Generación de un analizador léxico utilizando elcompilador LEX.El formato de la especificación de entrada <strong>para</strong> lex consta detres partes:declaraciones%%reglas de traducción%%procedimientos auxiliaresdonde las definiciones de funciones y funciones del usuario sonopcionales. El segundo %% es opcional, pero el primero se require<strong>para</strong> marcar el inicio de las reglas. El programa más pequeño quepuede escribirse en Lex, es:0%


(Ni definiciones ni reglas), el cual genera un programa quecopia la entrada a la salida (pantalla por default), sin modificaciónalguna, ya que de no indicarse ninguna otra, la acción poromisión del programa generado será copiar los caracteres deentrada a pantalla.Una regla individual patra la especificación de entrada podríaser:entero printf("Encontre la palabra ENTEROn);cuya finalidad es buscar la palabra entero en la entreada yescribir el mensaje "Encontre la palabra ENTEROn cada vez queésta aparezca.Si la acción asiciada a la expresión regular es una sólainstrucción de C, basta escribirla del lado derecho de la linea;si se trata de una instrucción compuesta o emplea más de unalínea, la acción deberá encerrarse entre llaves.EXPRESIONES REGUIIARES.Una expresión regular en la especificación de Lex puedecontener dos tipos de caracateres: caracteres textuales (letras odígitos) y caracteres que denotan operadores ( "\[IA-? .*+I OS/O%


coincide con todos los dígitos y ambos signos.- El operador indica complementación. Si se desea incluirdentro de alguna clase, éste deberá ser el primer caracter dentrode los corchetes, esto es[ "abc]representa todos los caracteres exepto a, b, y c, incluyendocaracteres de control.- Operador de caracter arbitrariodenota cualquier caracter, exepto el de nueva linea- Operador opcional. El operador opcional 3 indica un elementoopcional en la expresión. Por ejemplo:ab?cdenota tanto ab como abc, ya que la b es opcional.- Operadores de repetición. La repetición se indica mediante losoperadores +,*. Por ejemplo:a*representa las cadenas con cualquier número (cero o más) dea8s, incluyendo la cadena vacía.a+indica las cadenas con una o más a's.expresa todas las cadenas de letras minusculas; y[A-Za-z][A-Za-zO-9]*expresa todos los posibles identificadores de Pascal o C.- Operadores de alternativas. El operador I indica alternativas.Por e j emplo :representa tanto la cadena ab como cd.Los demas operadores se utilizan sólo en ocaciones esporádicaspor lo cual no serán tratados en el presente texto (si el lectordesea consultarlos puede <strong>hacer</strong>lo en el Manuel de Referencia deLex).30


144172Cabe señalar que Lex proporciona algunas variables que nospueden ser de mucha utilidad como:- YYTEXT. Contiene la cadena que coincidió con alguna expresiónregular.- YYLENG. contiene el número de caracteres de la cadena que seacaba de reconocer.NOTA: el último caracter de la cadena es:YYTEXT[YYLENG-l].2.4.1 AIAüNOS EJEMPLOS DE ESPECIFICACION PARA LEXEjemplo 1: Como ejempolo trivial, conciderese el problemaconcistente en contar todas las ocurrencias de la letra a en unarchivo de entrada y ademas producir como salida la impresión delarchivo de entrada en pantalla, sólo que por cada a que aparezcaimprimiremos una A.La espeificiación de entrada <strong>para</strong> que Lex genere una funciónque resuelva el problema anterior se muestra a continuación.int num-a=O;%%a ( ++num-a;printf ("A") ;return;1Se utiliza una variable de tipo entero llamada num-a la cuales declarada e inicializada en cero antes de iniciar la secciónde reglas de la especificación de entrada de Lex. El delimitador%% indica el inicio de la sección de reglas, en el cual seespecifica que la variable nun-a debe ser incrementada cada vezque en el archivo de entrada se encuentre una letra a y además seimprimirá en pantalla la letra A. Las acciones por defaultrealizadas por Lex <strong>para</strong> las entradas no contempladas en laespecificación de entrada es de la de impresión en pantalla. Estoes, <strong>para</strong> cualquier caracter de entrada exepto a será impreso enpantalla y cuando llege el caracter a se imprimirá A y seincrementará num a, cuando termine de procesarse el archivo, elvalor de la varxable indicará el número de ocurrencias de laletra a.Ejemplo 2: Escribir la especificación de entrada <strong>para</strong> que Lexcuente el número de lineas de un archivo y no imprima nada enpantalla.31


%)int nun-lineas=O;%%\n {++nun-lineas;)tEn la especificación de entrada declaramos e inicializamos lavariable num-lineas a cero; en la sección de reglas declaramos laexpresión regular que concuerde con el salt0 de linea, cuando enel archivo de entrada encontramos este caracter, la acción arealizar es incrementar la variable num-lineas exclusivamente. Siencontramos cualquier otro caracter se ejecutará la acción de lasegunda regla de la especificación de entrada, la cual es unaacción nula, por lo cual no realizamos nada, ni imprimimos nada apantalla.Ejemplo 3: Escribir la especificación de entrada <strong>para</strong> que Lexgenere un programa que imprima en la salida una lista con losidentificadores del archivo de entrada. Los identificadores sedeben escribir con mayusculas.int i;%I1 [a-zA-Z]d [O-91%%\n I{ for (i=O;iqyleng;i++)yytext[ i] = toupper (yyetxt[ i] );printf ("%s",yytext);I8los corchetes en la expresión regular indican que el símbolo fuedeclarada en la sección anterior.En la especificacion de entrada declaramos una variable enterai; y declaramos la clase de las letras y los dígitos; en lasección de reglas tenemos una expresión regular que buscaidentificadores (esto es cualquier cadena que comience con unaletra y sea seguida de letras o digitos), cuando lo encuentraconvierte caracter a caracter el contenido de yytext a mayusculase imprime el identificador en pantalla. La segunda expresiónregular concuerda con cualquier entrada que no sea unidentificador ni el caracter \n, su acción a ejecutar es nula. Latercera expresión regular concuerda con el caracter \n y suacción es nula.32


Ejemplo 4: Escribir la especificación de entrada <strong>para</strong> que Lexgenere un programa que sume todos los números enteros de unarchivo de texto.int suma=O;$1d [O-91%%{a)+ { suma = suma + atoi(yytext) t )I\n tEn la especificacion de entrada declaramos e inicializamos lavariable entera suma, declaramos la clase de los dígitos; en lasección de reglas tenemos una expresión regular que busca númerosenteros (esto es cualquier cadena que contenga sólo dígitos),cuando lo encuentra convierte el contenido de yytext a un enteroy ala variable suma le asignamos su contenido más la conversiónde yytext a entero. La segunda expresión regular concuerda concualquier entrada que no sea un identificador ni el caracter \n,su acción a ejecutar es nula. La tercera expresión regularconcuerda con el caracter \n y su acción es nula.Ejemplo 5: De acuerdo a las siguientes definiciones regularestoken asociadot ----> l(lld)* 1d+ 2I = 3dd ---->(blCR)*donde: t indica el token a buscar.d+ expresión regular que denota un entero= expresión regular que denota el símbolo =dd indica los delimitadores existentesb indica espacios en blancoCR indica el caracter enter.Construir la especificación de entrada <strong>para</strong> Lex, considerando queel programa generado será llamado por el analizador sintáctico.Puesto que el programa generado será llamado por el analizadorsintáctico, necesitaremos algunas variables globales que nossirvan de interfaz entre las dos fases:- ap-id. Cuando nuestro reconocedor léxico se encuentre conalgún identificador, lo que debemos de <strong>hacer</strong> es introducir elidentificador a una tabla de símbolos y regresar al analizadorsintáctico el número de token encontrado y además en ap-id33


egresamos la localidad de la tabla de símbolos donde insertamosel identificador.- valor-nun. Cuando el analizador léxico encuentra un enterodebemos de regresar el número de token asociado a esa expresiónregular y además en una variable entera (valor-nun) el enteroencontrado.%{int nun-linea; /*por si ocurre error sabemos el # línea */%I1 [a-zA-Z]%%i valor-num = atoi(yytext) treturn (2) t1return (3) tt{++nun-linea;}{ Error(yytext,nun-linea) i }En la sección de declaración de la especificacción de entradatenemos definida la variable num-linea, la cual contendrá elnúmero de línea que se está procesando en cada instante, esto conel fin de que si ocurre un error, además de mandar un mensaje nosposicionamos en la línea donde ocurrió dicho error. Ademástenemos declarado la clase de letras. En la sección de reglas,tenemos la primer expresión regular que denotan losidentificadores, las acciones a realizar <strong>para</strong> esta regla son:introducir el identificador a la tabla de símbolos (mediante lafunción maneja-id) y regresar el número de token asociado alanalizador sintáctico. La segunda expresión regular denota losenteros encontrados en el archivo de entrada, cuando es activadaesta expresión, la acción a relizar es: a valor-num le asignamosel entero encontrado y se lo regresamos al analizador sintácticoademás del número de token asociado. La tercer expresión busca elsímbolo = cuando lo haya lo Único que hace es regresar el númerode token asociado a ésta expresión al analizador sintáctico. Lacuarta expresión no realiza nada sólo salta los espacios enblanco. La quinta reconoce los saltos de línea y su acciónasociada es incrementar la variable num-linea. Y la Últimaconcuerda con los tokens no contemplados en las anterioresexpresiones y su acción es mandar un mensaje de error indicandola línea donde ocurrió.Para generar nuestro analizador léxico debemos introducir laespecificación de entrada <strong>para</strong> Lex donde debemos considerar:34


1.- Los tokens a reconocer, así como su número de token asociado.2.- Los delimitadores y comentarios.3.- Cuando encontremos un identificador lo debemos de introducira una tabla de símbolos (pues se ha encontrado una variable) y sedebe informar de esto a la siguiente fase de compilación, esto serealiza útilizando una variable global ap-id, la cual contiene ladirección de la localidad de la tabla de símbolos donde seencuentra éste identificador.4.- Cuando encontramos una cadena de caracteres entre comillas,es que se ha definido una cadea (si la especificación lo marcaasí), por lo tanto la debemos de introducir a una tabla decadenas y debemos de informar de esto a la siguiente fase decompilación; esto lo realizamos definiendo una variable globalposcad, la cual contiene la dirección donde se hubica la cadena,dentro de la tabla de cadenas.5.-En caso de encontrar caracteres inválidos, mandar un mensajede error.La tabla de símbolos se recomienda que se implemente medianteuna tabla de hash donde cada entrada apunte a una lista ligada ydonde la longuitud de la tabla de hash se un número primo y sufunción de inserción sea lo suficientemente eficiente <strong>para</strong>dispersar los símbolos en toda la tabla de manera uniforme. Porel momento los campos que debe de llevar cada nodo de la tablason:- Un apuntador a una cadena (identificador). Esto con el fin deno tener que declarar el tamaño del identificador a insertar ycon esto desperdiciar memoria.- Un apuntador al siguiente nodo.- Campos adicionales requeridos por las demás fases decompilaciónLa tabla de cadenas se recomienda que sea un arreglo deapuntadores a cadenas. Y poscad tendrá el indice de la Últimacadena que se insertó.


2.4.2 EJEMPLO DE UN ANALIZADOR LEXICO COMPLETO.#include nutiles.cnint num-linea-1;8)blancoletradigit0idnumeroZcornCcadCadenaotro[ \tl[A-Za-z]0-9 1{letra)({letra)l {digito))*(digito)+IA\nl("S" { Z 1 *\n)lAN\nl{C)*%%enterofuncionprincipalentoncessiotromientrashazregresa{blanco)\n{id){numero){ Cadena ){ return (ENTERO) ; ){ return (F"C10N) ; )(return(PRINC1PAL);){ return (ENTONCES) ; ){return (SI) ; ){ return (OTRO) ; ){return (MIENTRAS) t ){return(HAZ) ; ){return (REGRESA) ; )I{num-linea++;){ap-id = maneja-id(yytext,yyleng); /* INTRODUCIMOSEL IDENTIFICADOR ALA TABLA DE S1MBOLX)S */return (ID);){val-num = atoi(yytext) ; /* REGRESAMOS EL NüM. */return (NüM) ;){pos-cad = mete-cad(yytext); /* INTRODUCIMOSLA CADENA A LA TABLA DE CADENAS */return (CAD);){ num-linea++ ;){return(';');}{return(',');){return('(');){return(')');){return( { ,) ;){return(')') ;)36


{return (MAI){return(MAY)(return(ME1){ return (MEN){ return ( =I ){ return (NOI){ return (SUM){ return (SUB){return (MUL){ return (DIV){mane j a-errc%%37


CAPITULO I11AIi1ALISADOR SIYTACTICO3.1 GRAMATICAS INDEPENDIENTES DE CONTEXTO.DEFINICION. Una gramática independiente del contexto consta de 4partes :1.- Vn: Conjunto de símbolos no terminales.2.- Vt: Conjunto de Símbolos terminales.3.- P : Conjunto de producciones de la forma:a ---> B (a "deriva" 0)donde: a E Vn y lal-1 (longuitud de a-1)y B E (Vn U Vt)*4.- S : Símbolo inicial, donde S E VnAdemás Vn U Vt = E y Vn Vt =EJEMPLO:Vn={ S }Vt = { a,b }P = { S --> ab,S --> aSb }El lenguaje que podemos reconocer con esta gramática es elsiguiente:S --> aSb --> aaSbb --> aaabbben general podemos reconocer el lenguaje:a+b+DEFINICION. Una cadena w deriva directamente a una cadena 2, si zpuede obtenerse a partir de w mediante la aplicación de una delas producciones de la gramática ( w --> z).** * *Si a-->ai.-->a2-->a3-->. ..-->an, entonces a--->anSi a--->B y B ---> g, entonces por transitividad a ---> gDEFINICION. Sea una gramática G con símbolo inicial S.a) w es una forma sentencia1 de de G si S --- > w y w estáformada por símbolos terminales y no terminalesb) z es una sentencia de G si S ---> z y z está formada porsímbolos terminales.DEFINICION. El lenguaje generado por una gramática G es elconjunto de sentencias de la gramática*L(G) = { w I S --- > w siwEVt}**38


EJEMPLO 1: Decriba el lenguaje generado por la siguinetegramática:s ---> 1s ---> osDerivaciones posibles:s --> 1IIIos --> o100s --> O01000s --> 0001Analizando las derivaciones posibles observamos que ellenguaje generado por la gramática es el siguinte:L(G) = { l,Ol,OOl,OOOl, ...) = 0*1EJEMPLO 2:gramática:Describa el lenguaje generado por la siguiente--> 1s --> s1s --> soDerivaciones posibles:10 1 11I I Iso0 I Is10o1s11Analizando las derivaciones posibles observamos que ellenguaje generado por la gramática es el siguinte:L(G) = { l,ll,lO,lll,llO,lOl,lOO,...} = 1(0)1)*EJEMPLO 3: Escriba la gramática que genere el siguiente lenguaje:aAn bA2nt n>= 1.El lenguaje generado por esta expresión regular es elsiguiente :39


L(G) = { abb, aabbbb, aaabbbbbb, ... )Del lenguaje generado por la expresión regular podemosreagrupar cada sentencia de la siguiente forma:L(G) = { abb, a(abb)bb, aa(abb)bbbb, ...)De lo cual resulta claro que la grhtica que genera dicholenguaje es el siguiente:S --> abbS --> aSbbEJEMPLO 3: Escriba la gramática que genere el siguiente lenguaje:aAn bAm cAr : n par mayor que 1, m impar >=1, r>= O.El lenguaje generado por esta expresión regular es elsiguiente:L(G) = { aa, aabc, aabbb, aabbbc, . . . )Del lenguaje generado por la expresión regular podemosobservar que la gramática debe de ser de la formaDonde:El primer símbolo no terminal puede generar:A = {aa, aaaa, aaaaaa, ...)de aquí es claro que, A genera las siguientes producciones:> aaA ---> AaaA ---El segundo símbolo no terminal puede generar:A = {b, bbb, bbbbb, ...)de aquí es claro que, B genera las siguientes producciones:B --->B --->bbbBEl tercer símbolo no terminal puede generar:C = { E , c, CC, CCC, ...}de aquí es claro que, C genera las siguientes producciones:C --->c --->Ecc


144172Del análisis anterior podemos concluir que la gramática generadapor la expresión regular es la siguiente:S ---> A B C> aaA ---> AaaB ---> bB ---> bbBC ---> Ec ---> ccA ---Considere la gramática siguiente:S - EE --> IE --> I+EI --> aI --> bLa expresión "a+b+a" pertenece a la gramática generada por estelenguaje?En cada paso de la derivación debemos:1.- Decidir cual símbolo de la producción vamos a reemplazar.2.- Decidir cual producción sustituir del símbolo eleguido.Esto nos llevará a dos posibles caminos de derivación:a) E --> I + E --> a + E --> a + I + E --> a + b + E -->--> a + b + I --> a + b + aa) E --> I + E --> I + I + E --> I + 1 + I --> I + I + a -->--> I + b + a --> a + b + aEn el camino de derivación del inciso a consideramos derivacionesen donde sólo el no terminal de más a la izquierda fuesustituido en cada paso, formando así una "sentencia izquierda" o"forma de frase izquierda" de la gramática en cuestión.Análogamente, <strong>para</strong> el inciso b, consideramos derivaciones dondesólo el no terminal de más a la derecha fue sustituido en cadapaso formando así una Itsentencia derecha" de la gramática encuestión. Las derivaciones derechas a menudo se denominanderivaciones canónicas.Un árbol de análisis sintáctico se puede considerar como unarepresentación gráfica de una derivación que no muetre laelección relativa del orden de sustitución. El árbol creado por41


las derivaciones anteriores es:EIa I + Edonde: Cada nodo interior de un árbol de análisis sintáctico seetiqueta con algún no terminal E y que los hijos de ese nodo seetiquetan, de izquierda a derecha, con los símbolos del ladoderecho de la producción por el cual se sustituyó ésta E en laderivación. Las hojas del árbol de análisis sintáctico seetiquetan con no terminales y terminales y leidas de izquierda aderecha constituyen una sentencia llamada el producto o fronteradel árbol.Cabe señalar que un analizador sintáctico no constituyefísicamente el árbol de parze, sólo verifica sise puedeconstruir o no. Si se puede construir el árbol, implica que eltexto de entrada está escrito sintácticamente correcto.IbIIIa3.2 ANALISIS SINTACTICO DESCENDENTE.Existen básicamente dos tipos de analizadores sintácticos: elanalizador sintáctico descendente (Top Down), el cual intentaencontrar una derivación por la izquierda <strong>para</strong> una cadena deentrada. También se puede considerar como un intento de construirun árbol de análisis sintáctico <strong>para</strong> la entrada comenzando desdela raíz del árbol y creando los nodos del árbol en orden previo,y. El analizador sintáctico ascendente (Botton up), el cual serátratado posteriormente.Dentro de los analizadores sintácticos descendentesencontramos aquellos que útilizan la técnica de Backtrack y sonaquellos que enzayan producción por producción: cuando unaproducción no funciona, "deshace" todo lo hecho en estaproducción y "deslee" tokens <strong>para</strong> poder enzayar con la siguienteproducción tal y como se ilustra en el siguiente ejemplo.42


Cosidérese la gramáticaS --> c A dA-->ab I ay la cadena de entrada w = cad. Para construir un árbol deanálisis sintáctico descendente <strong>para</strong> esta cadena, primero se creaun árbol formado por un solo nodo etiquetado con S. Un apuntadora la entrada apunta a c, el primer sinrbolo de w. Después seútiliza la primera producción de S <strong>para</strong> expandir el árbol yobtener el árbol de la figura sig.a b aPasos en el análisis sintáctico descendente.Se empareja la hoja situada más a la izquierda, etiquetada con c,con el primer símbolo de w, y acontinuación se aproxima elapuntador de entrada a rar, el segundo símbolo de w, y seconsidera la siguiente hoja etiquetada con A. Entonces se puedeexpandir A utilizando la primera alternativa de A <strong>para</strong> obtener elárbol de la figura (b). Como ya se tiene una concordancia <strong>para</strong>el segundo símbolo de la entrada se lleva el apuntador de entradaa 'd8, el tercer símbolo de la entrada, y se com<strong>para</strong> 'd' con lahoja siguiente, etiquetada con 8b8. Como 'b8 no concuerda con'd', se indica fallo y se regresa a A <strong>para</strong> saber si existe otraalternativa de A que no se haya intentado, pero que pueda darlugar a un emparejamiento.Al regresar a A, se debe reestablecer el apuntador de entradaa la posición 2, aquella que tenía al ir a A por primera vez, locual significa que el procedimiento <strong>para</strong> A (análogo alprocedimiento <strong>para</strong> no terminales de la figura 2.17) debealmacenar el apuntador a la entrada en una variable local. Seintenta a continuación la segunda alternativa de A <strong>para</strong> obtenerel árbol de la figura (c). Se empareja la hoja 'a8 con el segundosímbolo de w, y la hoja 'd8, con el tercer símbolo. Como ya se haproducido un árbol de análisis sintáctico <strong>para</strong> w, se <strong>para</strong> y seanuncia el éxito de la realización completa del análisissintático.Una gramática recursiva por la izquierda puede <strong>hacer</strong> que unanalizador sintáctico por descenso recursivo, incluso uno conretroceso, entre en un lazo infinito. Es decir, cuando se intentaexpandir A, puede que de nuevo se esté intentando expandir A sin43


haber consumido ningún símbolo de entrada.Los analizadores sintácticos con backtrack tienen ladesventaja de que son muy costosos en tieapo y espacio, además deque pueden entrar en ciclos recursivos. En muchos casos,escribiendo con cuidado la gramática, eliminando su recursión porla izquierda y factorizando por la izquierda la gramáticaresultante, se puede obtener una gramática analizable con unanalizador sintáctico predictivo.Un analizador sintáctico predictivo o sin backtrack es aquelque sabe de antemano que producción va a ser utilizada; esto es,debe llevar un token adelante <strong>para</strong> poder saber que regla deproducción va a ocupar.Pa ra poder construir el diagrama de transiciones de unanalizador sintáctico predictivo a partir de una gramática,primero se debe eliminar la recursión por la izquierda de lagramática y después factorizar dicha gramática por la izquierda,luego, <strong>para</strong> cada no terminal A se hace lo siguiente:1.- Créese un estado inicial y un estado final (de retorno)2.- Para cada producción A --> Xl,X2,...,Xn créese un caminodesde el estado inicial al estado final, con aristasetiquetadas con Xl,X2,...,Xn.El analizador sintáctico predictivo que se desprende de losdiagramas de transiciones se comporta como sigue: comienza en elestado de inicio del símbolo inicial. Si después de algunosmovimientos se encuentra en el estado S, y si ese estado tieneuna arista etiquetada con el terminal a de estado S al estado T,y si el siguiente símbolo de entrada es a, entonces el analizadorsistáctico cambia el cursor de la entrada una posición aladerecha y se va al estado T. Si, por otra parte, la arista estáetiquetada con un no terminal A. El analizador sintáctico, va alestado de inicio de A, sin mover el cursor de la entrada. Sillega a alcanzar el estado final de A, inmediatamente va alestado T, habiendo en efecto wleido" A de la entrada cuando setrasladó del estado del estado S al estado T. Por último, si hayuna arista de S a T etiquetada con E, el analizador sintáctico vainmediatamente del estado S al T, sin avanzar la entrada.Como ejemplo considerese la siguiente gramática, observando eldesarrollo de su diagrama de transiciones respectivo así como suimplementación en un algoritmo.S : EXPEXP --> id OP FOP --> +OP --> *F --> idF --> numeroFormemos la derivación de la cadena na+3n.44


El analizador sintáctico lleva un token adelante <strong>para</strong> poder saberque regla de producción irá a ocuparAl comenzar con el símbolo inicial, reemplazamos EXP por ellado derecho de la producción id OP F al <strong>hacer</strong> esto, yallevamos un token leído por adelantado (a), por lo cual asociamosla a con el símbolo terminal "idn. Adelantamos un token (+) , conlo cual sabremos que producción reemplazar por el no terminal OP,que es el que está a continuación. Al reemplazar la producción,avanzamos el cursor, adelantamos un token (3) e intentamosaveriguar con éste, que producción útiliear <strong>para</strong> el símbolo noterminal F (numero). Con lo cual obtenemos el árbol de derivaciónmostrado arriba.Con la expresión Oa++lB, veamos como se comportará nuestragramática.ID OP Fa + ERROR DE SINTAXISAl comenzar con el símbolo inicial, reemplazamos EXP por ellado derecho de la producción id OP F al <strong>hacer</strong> esto, yallevamos un token leído por adelantado (a), por lo cual asociamosla a con el símbolo terminal "id". Adelantamos un token (+) , conlo cual sabremos que producción reemplazar por el no terminal OP(OP-->+), que es el que está a continuación. Al reemplazar laproducción, avanzamos el cursor, adelantamos un token (+) eintentamos averiguar con éste, que producción útilizar <strong>para</strong> elsímbolo no terminal F (numero, 6 id). Puesto que no coincide connunguno de ellos, podemos concluir que la expresión no estádentro del lenguaje reconocido por esta gramática.El autómata <strong>para</strong> esta gramática se muestra a continuación+45


Fig. 2.5 Automatas <strong>para</strong> una gramáticaSu implementación se muestra a continuación en forma dealgoritmos.PROCEDIMIENTO EXPComienzatoken e-- LEX;SI token ERROR cod-id ENTONCESOTROComienzatoken


Veamos ahora la siguiente gramática, sus autómatas y suimplementaciónS : DECLDECL --> var id TIPODECL --> const idTIPO --> enteroTIPO --> caracterde acuerdo a la gramática tenemos los siguientes autómatasf \?odonde la implementación de los autómatas es la siguiente:PROCEDIMIENTO DECLComienzatoken c-- LEXSI token = cod-var ENTONCESComienzatoken c-- LEXSI token c> cod-id ENTONCESERROROTROComienzatoken c-- LEXTIPOTerminaTerminaOTROSI token = cod-const ENTONCESComienzatoken c-- LEXSI token c> cod-id ENTONCESERRORTerminaOTROERRORTerminaOTRO token c-- LEX


PROCEDIMIENTO TIPOComienzaSI token cod-entero ENTONCEStoken DERDERid = DER--> EXP--> LLAMADAEXP --> numeroEXP --> -EXPEXP --> (EXP + EXP)LLAMADA--> id()LLAMADA--> id (PARAM)PARAM --> idPARAM --> id, PARAMEl diagrama de las dos primeras producciones es:40


Fig. 2.6 Autómatas <strong>para</strong> una gramática LL(1).la implementación del primer diagrama de transiciones <strong>para</strong> laprimer producción se muestra en el siguiente algoritmoPROCEDIMIENTO ASIGComienzatoken e-- LEXSI token cod-id ENTONCESERROROTROComienzatoken


3.2.2 Calculo del FIRST(x).1.- Si x es un símbolo terminal, entonces FIRST(x) = (x}2.- Si x es un no terminal, se consideran todas las produccionesde x y <strong>para</strong> cada una de ellas se hace lo siguiente:i) Si la producción es de la forma: x --> Yl,Y2,Y3,..,Yk,entonces se útiliza el siguiente algoritmo:i + T GT --> F UU --> * F UU --> EF --> idnumCalculemos ahora el FIRST de cada símbolo no terminal, deacuerdo al procedimiento anteriorpuesto que existen tres derivaciones de FF --> idnumI Eanalicemos cada una de ellas:FIRST(1d) = { id }FIRST(num)= { num }y además la cadena vacía está derivada por F, por lo tanto:FIRST(F) = { id, num, E }.Puesto que existen dos derivaciones de UU --> * F UU --> Eanalicemos cada una de ellas:FIRST(*) = { * }FIRST(E) = { E }por lo tanto:


FIRST(U) = ( *, E }Calculemos ahora el FIRST(T) , <strong>para</strong> esto, siguiendo el algoritmo,debemos de agregar el FIRST(F) al FIRST(T) , coa0 la cadena vacíapertenece al FIRST(F), entonces debemos de agregar también elFIRST (U) al FIRST (T) , dandonos como resultadoFIRST(T) = ( id, num, *, E }Calculemos ahora el FIRST(G), puesto que sólo tiene unaderivación, y el primer símbolo de la producción es un terminal,concluimos que:FIRST(G) = ( + }Por Último, calculemos, el FIRST(E) , puesto que sólo tiene unaderivación calculemos el FIRST de su primer símbolo de laderivación: FIRST(T) = { id, num, *, E ); puesto que contiene lacadena vacía será necesario incluir el FIRST del segundo simbolode la producción al FIRST(E): FIRST(G) = ( + ), y como éste ya nocontiene la cadena vacía, hasta aquí concluye el cálculo delFIRST (E), dandonos como resultado:FIRST(E) = { id, nun, *, + )DEFINICION. Para cálcular el FIRST(Xl,X2, ..., Xk), se procede dela siguiente forma:i


FIRST de la producciónASIGN -->id = DERDER --> EXPDER --> LLAMADAEXP --> numeroEXP --> -EXPEXP --> (EXP + EXP)LLAMADA--> id()LLAMADA--> id (PARAM)PARAM -->PARAM -->idid, PARAMSi un símbolo no terminal A tiene varias produccionesasociadas A --> AllA2 IA3 I . . . IAk, <strong>para</strong> saber cual es el que debeutilizarse <strong>para</strong> formar el árbol de parse de un programa deentrada, se calculan los FIRST de los lados derechos de lasproducciones y se busca el token adelantado x en ellas. Si xpertence al FISRT(Ai) la producción que debe utilizarse es A -->Ai.Si un símbolo no terminal A tiene varias producciones de laforma A --> AllA21A31 ...)Ak, entonces FIRST(Al), FIRST(AZ),...,FIRST(Ai) deben ser conjuntos disjuntos. Esto con el fin de saberque producción es la que vamos a seguir de acuerdo a al contenidode nuestro token adelantado. Si vemos nuestra gramáticaanalizada, vemos que los FIRST de la derivación de LLAMADA no sonconjuntos disjuntos <strong>para</strong> la cual haremos uso de la siguientedefinición.DEFINICION. Factorización por la Izquierda. Sí una gramáticatiene un símbolo no terminal con varias roducciones quecomparten un prefijo común: A-->aA1 I aA2 I aA3 I . . .una gramática equivalente es:A --> mllm2l ...I milaA'A'-->AlIA21 .1Ak1 mi Iai2 I . . .I mi,Factorizamos por la izquierda la producción de LLAMADA <strong>para</strong> queesta gramática pertenezca alas gramáticas del tipo LL(1).L-DA --> id (LLAMADA --> id ()PARAM)52


factorizando por la izquierda, tenemos:LLAMADA --> id (LLAMADA'LLAMADA'-->UAHADA'-->)PARAM)Solo nos queda modificar las producciones del no terminal PARAM,puesto que sus FIRST no son conjuntos disjuntos lo que dá comoresultado que nuestra gramática no sea U(1). Modificando losordenes de las derivaciones obtenamos una producción equivalentecomo la siguientePARAM --> idPARAM --> PARAM , idDonde los FIRST de las nuevas producciones son:Donde el FIRST de la primera producción es { id ), el FIRST dela segunda producción es el FIRST de PARñM que es él mismo, porlo tanto el FIRST de la segunda producción es { id ). Para evitarposibles confuciones en este tipo de producciones, veamos lasiguiente definición.DEFINICION. Eliminación de la recursión por la Izquierda. Sea Aun símbolo no terminal con producciones recursivas por laizquierda :A --> AallAa2l ...I AanlBll82l ...I BmDonde: 81, 82,...,8m no comienzan con A.Una gramática equivalente sin recursión por la izquierda es lasiguiente:A --> BlA'I82A'I ...lBmA'A --> alA81 a2A'l ...I anA'IEDe acuerdo a la definición enunciada arriba, eliminemos larecursión por la izquierda de las producciones:PARAM --> idPARAM --> PARAM , idEliminando la recursión por la izquierda tenemos:PARAM --> id PARAM'PARAM*--> , id PARAM'PARAM'--> ECon ésta modificación tenemos ya totalmente una gramática =(I),sólo nos resta realizar los algoritmos <strong>para</strong> la implementación delos últimos dos símbolos no terminales, lo cual se deja al lector53


interesado. Por lo tanto tenemos como conclusión la siguientegramática U( 1)ASIGN --> id = DERDERDER--> EXP--> LLAMADAEXP --> numeroEXP --> -EXPEXP --> (EXP + EXP)LLAMADALLAMADA' --> )LLAMADA' -->--> id (LLAMADA'PARAM)PARAM --> id PARAM'PARAM' --> , id PARAM'PARAM' --> E3.2.3 OTRAS CARACTERIZTICAS DE LAS GRAMATICAS LL(1)1.- Si A --> AllA21 ... lAn, es una producción, entonces FIRST(Al),FIRST(A2) , . . . , FIRST(An) deben ser conjuntos disjuntos.Dada la siguiente gramáticas --> AxyA --> XA --> Epodemos generar los siguientes árboles de parsef-bA X YIXSA X YIEdonde el símbolo no terminal A, produce los símbolos x 6 E.Dada la siguiente gramáticaS --> a B cl c2 c3 ... cnB --> B1 82 ... BnB --> al a2 ... anI E54


podemos generar el siguiente árbol.de derivación o de parseSdonde el símbolo no terminal B puede generar cualquiera de sustres producciones, en particular B puede derivar la cadena vacía,por lo cual debemos ver si el siguiente token (FOLLOW) es cl,despues de haber leido la entrada a, si es así el símbolo noterminal B producirá la cadena vacía, en caso contrario produciráalguna de las dos anteriores producciones. Para esto útilizaremosla siguiente definición.DEFINICION. Dado un símbolo no terminal x, el FOLLOW(x) es elconjunto de todos los tokens que pueden aparecer inmediatamente ala derecha de x en una árbol de parse.3.2.4 CALCULO DEL FOLLOW.Para calcular el FOLLOW(x) se procede de la siguiente manera:i) Si x es el símbolo inicial, se agraga EOF al FOLLOW(x).ii) Si hay una producción de la forma A --> ax, se agrega todolo que esté en FOLLOW(A) al FOLLOW(x) .iii) Si hay una producción de la forma A --> axB, se agrega todolo que esté en el FIRST(B) al FOLLOW(x) salvo la cadenavacía. Si el FIRST(B) incluye la cadena vacía, se agregátodo lo que esté en el FOLLOW(A) al FOLLOW(x).Tómese como ejemplo la siguiente gramática y calculese los FIRSTy los FO-WS de cada producción.S:AA --> D eA --> B CB --> d A EI EE --> e Ea B A cI ED --> dSiguiendo los pasos anteriore claculemos los FIRST Y FOLLOWS delas producciones.FOLLOW(D) = { e )FIRST(e) = { e }


FOLLOW(B) = { C, d }FIRST(C) = { c }FIRST(Ac) = { c, d }FIRST(A) = { d, c }PIRST(D) = { d }FIRST(B) = { d, e }FOLIiOW(A) = { e, a, c, d, EOF }FIRST(E) = { e, a, E }FOLIXIW(E) = { C, d }Si una gramática incluye un símbolo no terminal A que puedaderivar la cadena vacía, A --> Al(A2( ...IAk( E, <strong>para</strong> decidir cuales la producción que debe utilizarse <strong>para</strong> formar el árbol deparse de un programa de entrada, se lee el siguiente token x, six pertenece al FIRST(Ai) , la producción que debe utilizarse es A--> Ai; si x pertenece al FOLIXIW(A), la producción que debeutilizarse es A --> E. Ejemplo:S --> A x yA --> X FIRST(A) = { x }I E FOLLOW(A) = { x }El FIRST(A) y el FOLLOW(A) deben ser conjuntos disjuntos.2.- Si A --> A1IA21 ...I AnlE, son producciones, entonces,FIRST(A1) , FIRST(A2) !. . . , FIRST(An) y el FOLLOW(A) deben serconjuntos disjuntos.Ejemplo: Retomando la gramática del ejemplo anterior, y puestoque no comple con la segunda regla de las gramáticas U(1) y nola podemos pasar a dicha gramática (por factoriazacidn izquierdao recursión); la intentaremos reescribiresta, sí es gramática LL(1) , pues el FIRST(A) = { xI y } y elFOLLOW(S) = { EOF }.56


~~~~.3.3 ANALISIS SINTACTICO ASCENDENTEEn esta sección se introduce un estilo general de análisissintáctico ascendente, conocido como análisis sintáctico pordesplazamiento y reducción. El análisis sintáctico IiR se útilizaen varios generadores autómaticos de analizadores sintácticos.El análisis sintáctico por desplazamiento y reducción intentaconstruir un árbol de análisis sintáctico <strong>para</strong> una cadena deentrada que comienza por las hojas (el fondo) y avanza hacia laraíz (la cima). Se puede considerar eete proceso como de"reducirw una cadena w al símbolo inicial de la graglática. Encada paso de reducción se substituye una cadena determinada queconcuerde con el lado derecho de una producción por el símbolodel lado izquierdo de dicha producción y si en cada paso eligecorrectamente la subcadena, se traza una derivación por laderecha en sentido inverso.La técnica que revizaremos <strong>para</strong> el análisis sintácticoascendente es de las más eficientes (hasta ahora), la cual sepuede Utilizar <strong>para</strong> analizar una clase más amplia de gramáticasindependientes del contexto. La técnica se denomina análisissintáctico LR(K); la "L" es por el examen de la entrada deizquierda a derecha (en inglés, left to right), la "RW porconstruir una derivación por la derecha (en inglés, rightmostderivation) en orden inverso, y la k por el número de símbolos deentrada de examen por anticipado utilizados <strong>para</strong> tomar lasdesiciones del análisis sintáctico. Cuando se omite, se asume quek, es 1. El análisis sintáctico LR es atractivo por variasrazones.- Se pueden construir analizadores sintácticos LR <strong>para</strong> reconocerprácticamente todas las construcciones de los lenguajes deprogramación <strong>para</strong> los que se pueden escribir gramáticasindependientes del contexto.- La clase de gramáticas que pueden analizarse con los métodos LRes un supraconjunto de la clase de gramáticas que se puedenanalizar con los analizadores sintácticos predictivos.- Un analizador sintáctico LR puede detectar un error sintácticotan pronto como sea posible <strong>hacer</strong>lo en un examen de izquierda aderecha de la entrada.El principal inconveniente del método es que supone demasidotrabajo construir un analizador sintáctico LR a mano <strong>para</strong> unagramática de un lenguaje de programación típico. Se necesita unaherramienta especializada -un generador de analizadoressintácticos LR -. Por fortuna, existes disponibles estosgeneradores, por lo tanto estudiaremos el diseño y uso de uno, elprograma YACC. Con este generador se puede escribir una gramáticaindependiente del contexto y el generador produce automáticamenteun analizador sintáctico <strong>para</strong> dicha gramática. Si la gramáticacontiene ambiguedades u otras construcciones dificiles deanalizar en una examen de izquierda a derecha de la entrada, el57


generador puede localizar dichas construcciones e informar aldisñador del compilador de su presencia.Veamos ahora un ejemplo de como se realiza el análisissintáctico ascendente sobre una gramática en particular.EJEMPLO:S-->aABeA --> AbC I bB --> dConsidérese la gramáticaLa frase abbcde se puede reducir a S, por los siguientes 4pasos :AAa b b c d eSASIBDonde cada árbol representa cada uno de los pasos seguidos<strong>para</strong> reducir la cadena w al símbolo inicial S. El último árbolobtenido representa todas las secuencias de pasos seguidos.La frase abbcde se redujo a S por los siguientes pasos:abbcdeaAbcdeaAdeaABeS


Se examina abbcde buscando una subcadena que concuerde con ellado derecho de alguna producción. Las subcadenas b y d sirven.Elijase la b situada más a la izquierda y sustitúyase por A, ellado izquierdo de la producción A --> b; así 88 obtiene lasubcadena aAbcde. A continuación, las subcadenas Abc, b y dconcuerdan con el lado can el lado derecho derecho de algunaproducción. Aunque b es la subcadena situada más a la izquierdaque concuerda con el lado derecho de alguna producción, se eligesistituir la subacdena Abc por A, que es el lado derecho dela producción A --> Abc. Se obtiene ahora aAde. Sustituyendodespués d por B, que es el lado izquierdo de la producción B -->d, se obtiene aABe. Ahora se puede sustituir toda esta cadena porS. De hecho, estas reducciones trazan la siguiente derivación porla derecha en orden inverso:S --> aABe --> aAde --> aAbcde --> abbcde.Para formar el árbol de parse en forma ascendente lassituaciones que debemos considerar son:1.- Qué secuencias de símbolos forman el lado derecho de unaproducción.2.- Qué producción útilizar <strong>para</strong> formar el árbol de parse de laentrada.DEFINICION. Un mango de una cadena w, es una subcadena 0 que esel lado derecho de alguna producción A --> B tal que alremplazar B por A en w se obtiene una nueva cadena w' con lacual se puede completar el árbol de parse <strong>para</strong> w.Puesto que en muchos casos no basta tener en consideración lospuntos anteriores devido a que en ocasiones, la subcadena situadamás a la izquierda B que concuerda con el lado derecho de algunaproducción A --> B produce una cadena no reducible al símboloinicial, como en el siguiente ejemplo:A AI Ia b b c d ey de estas reducciones ya no podemos obtener el símboloinicial. Por lo cual siempre aplicaremos lo siguiente: Para versi se reduce una cadena en una cadena en una constante (símbolono terminal) veremos si el FOLIXIW(cte) contiene nuestro tokenadelantado sí es así, procederemos la reducción; en casocontrario no lo realizaremos y observaremos más tokens <strong>para</strong> versi podemos reducir con alguna otra producción aplicando el mismométodo.Analizamos nuevamente nuestro ejemplo anterior. Dada lagramática y cadena anterior veremos la primer reducción.59


AIa b b c d ePuesto que la subcadena a no aparece del lado iequierdo de algunaproducción; leeremos el siguiente token de la entrada, el cual esb; puesto-que la b sí aparece en el lado izquierdo de unaproducción A --> b y el token adelantado b aparece en elFOLIXIW(A) ( b, d }; por lo tanto se procede la reducción. Acontinuación tendremos la siguiente cadenaa A b c d ePuesto hasta ahora tenernos la subcadena aA leida y ésta noaparece de el lado derecho de alguna producción, leeremos elsiguiente token, la b, la buscamos del lado izquierdo de lasproducciones, hallandose en A --> b; pero como el tokenadelantado c no pertenece al FOLLOW(A) no haremos la reducción.Leeremos el siguiente token, la c, teniendo leido hasta ahora lasubcadena aAbc, vemos si esto produce un mango, lo cual esafirmativo puesto que A --> Abc y realizamos la reducción puestoque el token adelantado es la d E FOLLOW(A) . Teniendo elsiguiente árbol de parse.mAa A b c d eformando la siguiente subcadena aAde, de la cual hemos leido aA;puesto que no forma un mango, leeremos el siguiente token d,veremos si forma un mango, lo cual es afirmativo, pues B --> d yel token adelantado es e E FOWW(B). Por lo cual, realizaremosla reducción <strong>para</strong> formar el árbolBIa A d eahora tenemos la cadena aABe de la cual hemos leido aAB, puestoque no forma un mango, leeremos el siguiente token e, el cual noforma un mango, pero toda la cadena hasta ahora leida sí, pues, S--> aABe, la cual se reduce al símbolo inicial. Por lo tanto, lagramática sí es LR(l), pues se pudo formar el árbol de parseASI1B60


3.3.1 IMPLEMENTACION POR MEDIO DE UNA PILA DEL ANALISISSINTACTIC0 POR DESPLAZAMIENTO Y REWCCION.Un modo adecuado de implantar un analizador sintáctico pordesplazamiento y reducción es mediante la utilización de una pila<strong>para</strong> manejar los símbolos gramaticales, y un buffer de entrada<strong>para</strong> manejar la cadena w que se ha de analizar. Se utiliza $ <strong>para</strong>marcar el fondo de la pila y el extremo derecho de la entrada. Alprincipio, la pila está vacía, y la cadena w está en la entrada,como sigue:Pila$Entradaw $El analizador sintáctico funciona desplazando cero o más símbolosde la entrada a la pila hasta que un mango B esté en su cima. Elanalizador repite este lazo hasta que detecta un error o hastaque la pila contiene el símbolo inicial y la entrada está vacía:PilaEntrada$5 $Despuds de esta configuración, el analizador se <strong>para</strong> y anuncia laterminación con éxito del analisis sintáctico.EJEMPLO: Hágase el recorrido paso a paso de las acciones quepuede realizar un analizador sintáctico por desplazamiento yreducción <strong>para</strong> analizar la cadena de entrada id1 + id2 * id3según la gramáticaE --> E + EE --> E * EE --> (E)E --> idLa secuencia se muestra en el siguiente esquema. Obsérvese, quecomo la gramática tiene dos derivaciones por la derecha <strong>para</strong> estaentrada, existe otra secuencia de pasos que puede dar unanalizador por desplazamiento y reducción.Pila$$id1$E$E +$E + id2$E + E$E+E*$E + E * id3$E+E*E$E + E$EEntradaid1 + id2 * id3 $+ id2 * id3 $+ id2 * id3 $id2 * id3 $* id3 $* id3 $id3 $$$$$Acciondesplazarreducir por E --> iddesplazardesplazarreducir por E --> iddesplazardesplazarreducir por E --> idreducir por E --> E * Ereducir por E --> E + EaceptarL61


Aunque las principales operaciones del analizador son eldesplazamiento y la reducción, existen en realidad cuatroacciones posibles que un analizador por desplazamiento yreducción puede realizar: l)desplazar, 2) reducir, i)aceptar,4 ) error .1.- En una acción de desplazar, el siguiente símbolo de entradase desplaza a la cima de la pila.2.- En una acción de reducir, el analizador sabe que el extremoderecho del mango está en la cima de la pila. Entonces debelocalizar el extremo izquierdo del mango dentro de la pila ydecidir el no termina con que debe sustituir el mango.3.- En una acción de aceptar, el analizador anuncia laterminación con éxito del análisis sinthtico.4.- En una acción de error, el analizador descubre que se haproducido un error sintáctico y llama a una rutina derecuperación de errores.Hay un hecho importante que justifica el uso de una pila en elanálisis sintáctico por desplazamiento y reducción: el mangosiempre aparecerá en la cima de la pila, nunca dentro.3.3.2 CONFLICTOS DURANTE EL ANALISIS SINTACTICO PORDESPLAZAMIENTO Y REWCCION.Existen gramáticas independientes del contexto <strong>para</strong> las cualesno se pueden utilizar analizadores sintácticos por desplazamientoy reducción. Todo analizador por desplazamiento y reducción <strong>para</strong>estas gramáticas puede alcanzar una configuración en la que elanalizador sintáctico, conociendo el contenido total de la pila yel siguiente símbolo de entrada, no puede decidir si desplazar oreducir (un conflicto de desplazamiento/reducción), o no puededecidir que tipo de reducción efectuar (un conflictoreducción/reducción). A continuación se verán algunos ejemplos deconstrucciones sintácticas que dan lugar a dichas gramáticas.Técnicamente, estas gramáticas no están dentro de la clase LR(K).A este tipo de gramáticas se les denomina, gramáticas no LR. La kde LR(K) se refiere al número de símbolos de preanálisis sobre laentrada. Por lo general, las gramáticas utilizadas en compilaciónse incluyen en la clase LR(l), con un símbolo de anticipación.Como ejemplos ilustrativos, veamos las siguientes gramáticas


EJEMPLO: Considérese la siguiente gramática:S 9-> yA xs --> c xC --> y AA --> aw = yaxTenemos inicialmente la pila vacía y la cadena de entrada w.Obtenemos el primer token y, puesto que no forma un mango,hacemos un shift introduciendolo ala aim de la pila. Para locual tenemosHLeemos el siguiente token a, y vemos si forma un mango; como A--> a y el token adelantado en este momento x E FOLIXIW(A) = (x),entonces podemos realizar la reducción, obteniendo en la pilaa continuación podemos <strong>hacer</strong> dos cosas reducir Ay por C, pues C--> Ay, o bien leer el siguiente token x, <strong>hacer</strong> un desplazamientoy después reducir utilizando la producción S --> yAx. A este tipode situaciones se les conoce como nconflictos shift-reduce" ocomo "conflictos de desplazamiento/reducción", pues en etsemomento no sabemos si desplazar o reducir ya que en ambos casosobtendremos árboles de parse correctosSSY a XPuesto que la gramática es ambigua, devido a que genera dosároles distintos provocando en la pila un conflicto shift-reduce,entonces por definición la gramática no es LR(1).


EJEMPLO: Consideremos ahora la gramáticaS --> a C dS --> E dE --> a BB --> xB --> xw = axdIniciamos con la pila vacía y la cadena w. obtenemos el primertoken a, puesto que no forma un mango, hacemos un desplazamientointroduciendo el token a la cima de la pila. Para lo cualtenemos :leemos el siguiente token x, el cual forma un mango con laproducción B --> x, pues el token adelantado d E FOLLOW(B) = {d).Pero también forma un mango con la producción C --> x, pues eltoken adelantado d E FOLLOW(C), por lo tanto, nos encontramosante el problema de que producción utilizar <strong>para</strong> <strong>hacer</strong> lareducción. A este tipo de conflictos se les conoce como"conflictos reduce-reduce" o bien como "conflictos reducirreducir".Ya que si contin<strong>uam</strong>os con el análisis podremos formardos árboles de parse distintos <strong>para</strong> la misma gramáticaSSa X dPor io cual concluimos que la gramática no es =(I), pues,contiene un conflicto reducir-reducir.En general, cualquier gramática que contenga conflictos ygenere dos árboles de parse distintos <strong>para</strong> ésta, no puede serm(1)3.3.3 EL AXORITMO DE ANALISIS SINTACTICO LR(1).En la figura siguiente se muestra la forma esquemática de unanalizador sintáctico LR. Consta de una entrada, una salida, unapila, un programa conductor y una tabla de análisis sintácticocon dos partes (acción e ir-a). El programa analizador leecaracteres de un buffer de entrada de uno en uno; Qtiliza una64


pila <strong>para</strong> almacenar una cadena de la foma SO X1 sl X2 s2 ,...,Xm sm, donde sm está en la cima de la pila. Cada Xi es un símbologramatical y cada si es un símbolo llamado estado. Cada símbolode estado resume la información contenida debajo de la pila.Programa <strong>para</strong>análisissintáctico LR-------.,, Salidaacciónir-aFig. Modelo de un analizador sintáctico LR.En la sección siguiente abordaremos el tema de como construirlas tablas de análisis sintáctico.3.3.4 CONSTRUCCION DE TABLAS DE ANALISIS SINTACTICOUna gramática <strong>para</strong> la que se puede construir una tabla deanálisis sintáctico se denomina gramática LR.Un analizador LR no tiene que examinar la pila completa <strong>para</strong>sabeer cuando aparecen los mangos en la cima. Por el contrario,el símbolo del estado en la cima de la pila contiene toda lainformación necesaria. Es un hecho curioso que, si se puedereconocer un mango conociendo sólo los símbolos gramaticales dela pila, entonces existe un autómata finito que puede, leyendolos símbolos gramaticales de la pila, entonces existe un autómatafinito que puede, leyendo los símbolos gramaticales de la pila dearriba a abajo, determinar el mango, si existe, que esta en eltope de la pila. La función ir-a de una tabla de análisissitáctico LR es escencialmente dicho autómata finito. Sinembargo, el autómata no necesita leer la pila <strong>para</strong> cadamovimiento. El símbolo estado almacenado en la cima de la pila esel estado en que estaría el autómata finito reconocedor de losmangos si hubiera leído los símbolos gramaticales de la piladesde abajo hasta la cima. Por tanto, el analizador sintáctico LRpuede determinar a partir del estado de la cima de la pila todo65


lo que necesita saber sobre lo que hay en ella.Existe una diferencia significativa entre las gramáticas U ylas LR. Para que una gramática sea LR(K) 8 hay que ser capaz dereconocer la presencia del lado derecho de una producción,habiendo visto todo lo que deriva de dicho lado derecho con ksímbolos de examen por anticipado. Este requisito es mucho menosriguroso que el de las gramáticas LL(K), donde hay qua ser capazde reconocer el uso de una producción viendo sólo 108 primeros ksímbolos de los que deriva su lado derecho. Por consiguiente, lasgramáticas LR pueden describir más lenguajes que las gramáticasLL .Un elemento del análisis sintactico LR de una gramática G, esuna producción de G con un punto en alguna posición del ladoderecho. Por tanto, la producción A --> XYZ produce los cuatroelementosA --> DX Y ZA --> XDY ZA --> X YDZA --> X Y ZmLa producción A -- E genera sólo un elemento, A --> D.Intuitivamente, un elemento indica hasta donde se ha visto unaproducción en un momento dado del proceso del análisissintáctico. Por ejemplo, el primer elemento de arriba indica quese espera ver a continuación en la entrada una cadena derivablede XYZ. El segundo elemento indica que se acaba de ver en laentrada una cadena derivable de X y que a continuación se esperaver una cadena derivable de YZ.La idea central es construir primero a partir de la gramáticaun autómata finito determinista <strong>para</strong> reconocer los prefijosviables. Los elementos se agrupan en conjuntos, que dan lugar alos estados de un AFN que reconoce los prefijos viables, y el"agrupamientoI8 es en realidad la construcción de subconjuntos.Si G es una gramática con símbolo inicial S, entonces G 8, lagramática aumentada <strong>para</strong> G, es G con un nuevo símbolo inicial S'y la producción S' --> S. El propósito de esta nueva produccióninicial es indicar al analizador cuándo debe detener el análisissintáctico y anunciar la aceptación de la cadena. Es decir, laaceptación se produce cuando, y solo cuando, el analizador está apunto de reducir por S' --> S.3.3.4.1 LA OPERACION CERRADURASi I es un conjunto de elmentos <strong>para</strong> una gramática G, entonces lacerradura(1) es el conjunto de elementos construidos a partir deI por las dos reglas:1.- Inicialmente, todo elemento de I se añade al cerradura(1).2.- Si A --> aiBB está en la cerradura(1) y B --> r es unaproducción, entonces añádase el elemento B --> mr a la66


cerradura (I), si todavía no está ahí. Se aplica esta reglahasta que no se puedan añadir más elementos a cerradura(1).Intuitivamente, si A --> amBB está en cerradura(1) indica que, enalgún momento del proceso de análisis sintáctico, se cree posiblever a continuación una cadena derivable de B como entrada. Si B--> n es una producción, también se espera ver una subcadenaderivable de n en este punto. Por esta razón se incluye B --> mnen cerradura(1).EJEMPLO: Considérese la gramática de expresiones aumentada:E'--> EE --> E + TE --> TT --> T * FT --> FF --> (E) I idSi I es el conjunto de un elemento ( [E8 -->cerradura(1) contiene los elementosHE] ), entoncesE'--> .EE --> .E + TE --> mTT --> mT * FT --> mFF --> .(E)F --> midAquí, E8 -->mE se coloca en cerradura(1) por la regla 1. Comohay una E inmediatamente a la derecha del punto, por la regla 2se añaden las producciones de E con puntos en el extremoizquierdo, es decir, E --> mE + T y E --> iT . Ahora hay una Tinmediatamente a la derecha de un punto, así que se añade T -->mT * F y T --> DF. A continuación, la F a la derecha de unpunto obliga a añadir F --> .(E) y F --> wid. Por la regla 2 nose colocan más elementos dentro de cerradura(1).A continuación mostramos un algoritmo que calcula lacerradura (I).FUNCTION cerradura (I);BEGINJ := I;REPEATFOR cada elemento A --> auB0 en J y cada producciónB --> n de G tal que B --> mn no esté en J DOañadir B --> mn a JUNTIL no se puedan añadir más elementos a J;RETURN J;END;67


Observese que si se añade una producción de B a la cerradura de Icon el punto en el extremo izquirdo, entonces todas lasproducciones de B se añadirán de manera similar a la cerradura.3.3.4.2 LA OPERACION ir-a.La segunda función Útil es ir-a(I,X) I donde I es un conjuntode elementos y X es un símbolo de la gramática. Se defineir a(1,X) como la cerradura del conjunto de todos los elementos[A---> am01 tales que [A --> amXB] esté en I. Intuitivamente, siI es el conjunto de elementos válidos <strong>para</strong> al- prefijos viable,entonces ir-a(1,X) es el conjunto de elementos válidos <strong>para</strong> elprefijo viable nX.EJEMPLO: Si el conjunto de dos elementos ([E' --> Em], [E --> Ei+ TI}, entonces ir-a(I,+) consta deE --> E + HTT --> iT * FT --> iFF --> i(E)F --> iidSe calculó ir-a(I,+) examinando I <strong>para</strong> buscar elementos con +inmediatamente a la derecha del punto. E'--> Em no es uno deestos elementos pero E --> iE + T, sí. Se desplazó el punto másallá de + <strong>para</strong> obtener {E --> E + ff) y después se tomó lacerradura de este conjunto.La construcción de conjuntos de elementos.Ahora ya se puede dar el algoritmo <strong>para</strong> construir C, lacolección canónica de conjuntos de elementos LR <strong>para</strong> unagramática aumentada GI; el algoritmo esPROCEDURE elementos (GI) ;BEGINC := { cerradura( {[S' --> is]} );REPEATFOR cada conjunto de elementos I en C y cada símbologramatical X tal que ir-a(i,x) no esté vacio y noesté en C Doañadir ir-a(I,X) a CUNTIL no se puedan añadir más conjuntos de elementos a CEND.De acuerdo a la gramática con la que hemos venido trabajando, ysiguiendo los algoritmos mostrados, obtenemos la siguiente68


colección canónica de conjuntos LR <strong>para</strong> la gramática tratada.IO: E'--> HEE --> iE + TE --> iTT --> iT * FT --> iFF --> i(E)F --> mid15: F --> idi16: E --> E +iTT --> DT* FT --> .FF --> .(E)F --> iid11: E'--> Ei 17: T --> T *iFE --> Ei+ TF --> .(E)F --> iid12: E --> TiT --> Ti* F 18: F --> (Ei)E --> Ei+ T13: T --> Fi19: E --> E + Ti14: F --> (DE) T --> Ti* FE --> iE + TE --> iT110: T --> T * FiT --> iT * FT --> iF Ill: F --> (E)iF --> .(E)F --> iiddel cual obtenemos un autómata finito no determista N cuyosestados son los elementos, con aristas proporcionadas por lafunción ir-a aplicada a cada elemento. Dicho autómata reconoceexactamente los prefijos viables de la gramática, tal como semuestra a continuación69


3.3.4.3 TABLAS DE ANALAISIS SINTACTIC0 LR.A continuación se muestra cómo construir las funciones de accióne ir-a del anáisis sintáctico LR a partir del autómata finitodeterminista que reconoce prefijos viables.Dada una gramática G, se aumenta G <strong>para</strong> producir G', y apartir de G se construye C, la colección canónica de conjuntos deelementos <strong>para</strong> G'. Se construye acción, la función de acciones deanalizador sintáctico, e ir-a, la función de transiciones deestados, a partir de C utilizando el siguiente algoritmo.ENTRADA: Una gramática aumentada G'.SALIDA: Las funciones acción e ir-a de la tabla de análisissintáctico LR <strong>para</strong> G'.METODO:1.- Construyase C = { 10,11,12,...,1n } la colección de conjuntosde elementos LR <strong>para</strong> G'.2.- El estado i se construye a partir de Ii. las acciones deanálisis sintáctico <strong>para</strong> el estado i se determinan como sigue:a) Si [A --> aiaB] está en Ii e ir-a(Ii,a) = Ij, entoncesasígnese "desplazar ji8 a acción(i,a) . Aquí, a debe ser unterminal.b) Si [A --> ai J está en Ii, entonces asígnese "reducir A--> air a acción[ i,aJ <strong>para</strong> toda a en FOUOW(A) ; aquí, Apuede no ser S'.c) si [s' --> si] está en ii, entonces asígnese "aceptar" aacción[i,$].Si las reglas anteriores generan acciones contradictorias, sedice que la gramática no es LR(1) . El algoritmo no consigue eneste caso producir un analizador sintáctico.3.- Las transiciones ir-a <strong>para</strong> el estado i se construyen <strong>para</strong>todos los no terminales A utilizando la regla : si ir-a(Ii,A)= Ij, entonces ir-a[i,A] = j..4.- Todas las entradas no definidas por las reglas 2 y 3 sonconsideradas nerrorii5.- El estado inicial del analizador es el construido a partirdel conjunto de elementos que contiene IS8 --> is).EJEMPLO: Construcción de la tabla LR <strong>para</strong> la gramáticaE'--> EE --> E + TE --> TT --> T * FT --> FF --> (E) I id70


7 +-de la cual ya obtuvimos su serie canónica de conjuntos deelementos anteriormente. Primero considérese el conjunto deelementos IO:IO: E'--> iEE --> iE + TE --> iTT --> iT * FT --> iFF --> .(E)F --> iidEl elemento F --> =(E) da lugar a la entrada acción[ O, (1 =desplazar 4, y el elemento F --> mid a la entrada acción[O,id] =desplazar 5. Los otros elementos en IOno dan lugar a acciones.Ahora considérese 11:E' --> TmE --> Ti* FComo FOLiDW(E) = ( $, +, ) ), el primer elemento hace queacción[2,$] = acción[2,)] = reducir E --> T. El segundo elementohace acción[2,*] = desplazar 7. Si se continúa así, se obtienenlas siguientes tablas de acción e ir-a <strong>para</strong> el análisissintáctico.ESTADO IO1234567a91011acciónid + * ( 1 $ E T Fd5d5d5d5d4d6acepr2 d7 r2 r2r4 r4 r4 r4d4r6 r6 r6 r6d4d4d6dllrl d7 rl rlr3 r3 r3 r3r5 r5 r5 r51 2 30 2 39 310Fig. Tabla de análisis sintáctico <strong>para</strong> la gramática deexpresiones.De acuerdo a los algoritmos anteriores vemos que no importa cualsea la gramática de entrada, siempre y cuando sea LR(1)produciremos una tabla de análisis sintáctico (accibn, e ir-a): y71


junto con el programa conductor, la pila y la entrada,produciremos nuestro analizador sihthctico. Dicha tabla y mMulosnos los proporciona ya el compilador YACC, lo único que tenemosque <strong>hacer</strong> es darle como entrada una gramática no ambigua y 61 nosgenerará la tabla de analisis sintáctico, la pila y losalgoritmos codificados en lenguaje C <strong>para</strong> manupular dicha tabla ypila. Es decir, nos generará nuestro analizador sintáctico, dichocompilador es nuestro siguiente tópico de estudio.3.4 YACC (GENERADOR DE ANALIZADORES SINTACTICOS).YACC es una generador de analizadores sintáctios que recibe comoentrada una gramática (almacenada en un archivo de texto, el quese denominará archivo de especificación de entrada <strong>para</strong> YACC) yproduce como salida el analizador sintáctico ascendente quereconoce las estructuras definidas por las producciones de lagramática de acuerdo a la siguiente figura.Especificaciónde entrada <strong>para</strong> ---YACCcompiladorde YACC ---> yytab.ccompiladoryytab.c ---- >uxxx . execompiladorentrada ---- > de YACC ---> salidasi por alguna razón no es posible crear el analizador sintático,YACC envia un mensaje indicando cual es el problema.Para invocar a YACC, desde el sistema operativo se da el comandoA:\>yacc si no hubo errores, YACC produce como salida el analizador72


sintático (escrito en C) correspondiente ala gramdtica contenidaen el archivo de especificación de entrada. El analizadorsintáctico se almacena en un archivo llamado yytab.c y la funciónque debe invocarse <strong>para</strong> realizar el análisis sintáctico de algúnprograma fuente se llama yyparse().El usuario debe proporcionar el analizador léxico quereconozca los tokens indicados en los lados derechos de lasproducciones de la gramática; este analizador léxico (que debellamrse yylex) es invocado por el analizador sintácticoproducido por YACC cada vez que se requiere el siguiente token dela entrada.Es posible asociar a cada símbolo no terminal de la gramáticaun conjunto de atributos y a cada producción un conjunto deacciones semánticas, de tal forma, que el programa producido porYACC pueda realizar otras tareas a parte de las propias delanalizador sintáctico, tales como análisis senántico,interpretación, generación de código, etc. El programa producidopor YACC está escrito en C. Las reglas gramaticalesproporcionadas a YACC deben tener cierto formato <strong>para</strong> que YACCpueda comprenderlas.3.4.1 ESPECIFICACIONES BASICASUn nombre puede denotar tanto un token como un símbolo noterminal; Yacc requiere que los nombres <strong>para</strong> los tokens seandeclarados como tales.Todo archivo de especificación consta de tres secciones:reglas gramaticales y funciones del usuario. Las secciones sese<strong>para</strong>n con el delimitador a%. De manera esquemática, un archivode especificación <strong>para</strong> Yacc tiene el siguiente formatodeclaraciones%%reglas gramaticales%%funciones del usuariolas secciones de declaraciones y funciones de ususario sonopcionales, por tanto, el formato de especificación mínimaadmisible <strong>para</strong> Yacc es el siguiente:%%reglasLos espacios en blanco, tabuladores y caracteres de nuevalinea son ignorados. Los comentarios, que pueden aparecer encualquier lugar en que un nombre es admisible, son encerradosentre /* y */ como en C.73


144172La sección de reglas consta puede tener una o más reglasgramaticales. Una regla gramatical tiene la formaLADO-IZQUIERDO : CUERPOdonde LADO_IZQUIERDO representa un no terminal y CUERPOrepresenta una secuencia de cero o más noaibres y literales. Losdos puntos y el punto y coma son signos de puntuación <strong>para</strong> Yacctel punto y coma es opcional.Yacc diferencia las letras mayúsculas de las minúsculas. Losnombres empleados en el cuerpo de una regla gramatical puedenrepresentar tanto tokens como símbolos no terminales.Una literal consiste de un cardcter encerrado entreapostrofes. La diagonal inversa n\n tiene el mismo empleo que enC, de modo que\n' representa el'\r? representa el'\?representa el'\\? representa la\t representa el?\bt representa el\f representa el?\xxx' representa elcaracter nueva línearegreso de carroapostrofodiagonal inversatabuladorcaracter de backspacecaracter salto de hojacaracter ascii xxx (en octal)Por varias razones técnicas, el carácter nulo '\O'emplearse como parte de una regla gramatical.nunca debeSi existen varias reglas gramaticales con el mismo ladoizquierdo, puede emplearse la barra vertical H(vv <strong>para</strong> evitarescribir el lado izquierdo varias veces. El punto y coma con quetermina cada regla alternativa se reemplaza por la barravertical. Por ejemplo, las reglasA : B C DA:EFA : G;pueden escribirse de la formaA : B C D1 EFNo es necesario que todas las reglas gramaticales que tenganel mismo símbolo no terminal en el lado izquierdo aparezcanjuntas en la sección de reglas de la especificación, sin embargo,si se hace así, la especificación es más legible y fácil demodificar.Si algún símbolo no terminal X deriva la cadena vacía, seescribe la reglax : ;74I. *~


Los nombres que representan tokens deben de ser declarados, laforma más simple de <strong>hacer</strong>lo es empleando la directiva Woken:%token nombrel, nombrel,...YAcc asume que todo nombre que no sea declarado en la secciónde declaraciones es un símbolo no terminal. Todo símbolo noterminal debe aparecer en el lado izquierdo de cuando menos unaregla gramatical.Entre los símbolos no terminales, hay uno denominado símboloinicial, que es el de mayor importancia. De no indicarse de otromodo, se asume que el símbolo inicial es el que aparece en ellado izquierdo de la primer regla gramatical; <strong>para</strong> indicarexplicitamente el símbolo inicial, en la sección de declaracionespuede incluirse la declaración%start símbolo-inicialEl final de la entrada al analizador sintáctico debe serseñalado mediante un token especial que llamaremos marca-de-fin.El analizador sintáctico acepta una entrada si, al terminar deformar el árbol de parse de la entrada, recibe del analizadorléxico la marcade-fin. Si la marca-de-fin aparece en cualquiercontexto, se trata de un error.El analizador léxico incluido en la especificación de entrada<strong>para</strong> Yacc debe de devolver la marcade-fin al encontrar el findel archivo.3.4.2 ACCIONES.Con cada regla gramatical, el usuario puede asociar acciones quese ejecutan cada vez que la regla es reducida durante el procesode análisis; estas acciones pueden calcular valores y acceder alos valores calculados por acciones previas. Más ah, el valordel atributo asociado a algún token puede utilizarse en dichasacciones.Una acción debe encerrarse entre llaves, como sigue:A : '(' B ')' { funcion(1,"abc"); )o bienxxx : YYY zzz{ printf ("Ocurrió YYY ZZZ") tbandera = 1;1son reglas gramaticales con acciones.75


Es posible asociar atributos a los símbolos no terminales dela gramática y emplear los valores de 108 atributos en lasacciones. El valor del atributo del símbolo no terminal del ladoizquierdo de una producción se denota en Yacc mediante lapseudovariable $$.Por ejemplo, una acción que no hace nada más que asignar elvalor 1 al atributo asociado al símbolo no terminal del ladoizquierdo esPara denotar los valores de los atributos de símbolos noterminales colocados del lado derecho de una producción, seemplea la siuiente notación:$1 es el valor del atributo del primer SírPbolO del lado derecho$2 es el valor del atributo del segundo símbolo del ladoderecho.Así <strong>para</strong> la producciónA:BCD;$2 denota el valor del atributo asociado a C y $3 el valor delatributo del símbolo D. como ejemplo más concreto, considérese lareglaexpr : '(' expr ')' { $$ = $2 1Salvo que se especifique otra cosa, el valor del atributo delsímbolo no terminal del lado izquierdo de una regla es el valordel atributo del primer símbolo del lado derecho de ésta. Así,<strong>para</strong> reglas gramaticales de la formaA:B;generalmente no es necesario definir una acción explícita.En los ejemplos anteriores, todas las acciones se han indicadoal final de las reglas. En ocasiones, es deseable realizar algunaacción en algún lugar dentro del lado derecho de una producción;así, <strong>para</strong> la reglaA:BC{S$=11(x=$2; 2 =el efecto de las acciones esvalor asociado al símbolo no$3: 1asignar a x el valor de 1, y a z elterminal C.Las realas con acciones semánticas que no se encuentren alfinal de una regla son modificadas por Yacc, quien crea un nuevosímbolo no terminal que derive la cadena vacía y la acción76


interior es colocada al final de la nueva regla. Así Yacc tratael ejemplo anterior como si en realidad tuviera las siguientesreglas:A : B NUEVO-SIMBQLü C( x = $2; 2 = $3; }NUEVO-SIMBOIX): /* cadena vacía */ ($$ = l;}.En el archivo de especificación de entrada <strong>para</strong> Yacc elusuario puede definir otras variables <strong>para</strong> ser empleadas por lasacciones. Las declaraciones y definiciones pueden aparecer en lasección de declaraciones encerradas entra 108 delimitadores %( y%}. Estas declaraciones y definiciones tienen un alcance global,de modo que son conocidas tanto por las acciones como por elanalizador léxico. Por ejemplo la definición%( int variable-0; %)puede colocarse en la sección de declaraciones, haciendo que lavariable sea accesible a todas las acciones y al analizadorléxico. El analizador sintáctico generado por Yacc empleavariables globales cuyo nombre comienza con el prefijo yy; elusuario debe evitar nombres que inicien con dicho prefijo.El usuario debe proporcionar a Yacc un analizador léxico quelea la entrada y encuentre los tokens presentes de la misma. Elanalizador léxico es una función que devuelve un entero y debellamarse yylex. El valor entero devuelto por la función es elcódigo del siguiente token. Si se desea devolver un valoradicional asociado a dicho token, éste puede asociarse a lavariable externa llamada yyval.El analizador sintáctico y el léxico deben asignar los mismoscódigos a los tokens <strong>para</strong> <strong>hacer</strong> posible la comunicación entreellos. Para realizar esto selecionamos los tokens y debemos defijarnos que concuerden los de los dos analizadores. En el códigogenerado por Yacc se define con algún valor númerico cada tokende manera global, debido a esto no hace falta definirlo en elanalisis sintactico pues los dos archivos fuente se van a unir enuno sólo. De acuerdo a esto, y a lo expuesto presentamos unejemplo de una gramatica <strong>para</strong> el analisis sintáctico, la cualnos servirá <strong>para</strong> trabajar con ella en las siguientes dossecciones <strong>para</strong> generar así sus acciones semánticas y degeneración de código.77


3.4.3 GRAMATICA.PROGRAMACTESDECLSDECLLI STA-I DFUN-DECLSFUN-PRINCFUN-DECLENCAARGSCUERPOLISTA-INSINSINSTSCONDEXPTERMINO: CTES DECLS FUN-DECLS F"-PRINC: /* cadena vacía */I CTES id = string: /* cadena vacía */I DECLS DECL: entero LISTA-ID: idI LISTA-ID, id: /* cadena vacía */I FUN-DECLS FüN-DECL: funcionprincipal () CUERPO: ENCA DECLS CUERPO: funcion id( ARGS )I funcion id (): idI id , ARGS: { LISTA-INS }: INSI INS i LISTA-INS: id = EXPsi COND entonces INSTS otro INSTSsi COND entonces INSTSmientras COND haz INSTSregresa ( EXP ): INSI CUERPO: EXP REWP EXP: TERMINOI EXP ADOP TERMINO: FACTOR


FACTORLISTA-EXPRELOPADOPMUWPI TERMINO MULOP FACTOR: idid0id ( LISTA-EXP )numero( EXP 1: EXPI EXP , LISTA-EXP:>= I I < I = I :+ I -:* I /


CAPITULO VI. AblALISI8 8-100El presente capítulo tiene como finalidad dar un breve repasosobre la fase de compilación de análisis sem4ntico. A partir deeste capítulo reduciremos el material de estudio y nosconcentraremos en la manera de como realizar las siguientesfases, tomando como material de apoyo el compilador Yacc.A partir del presente capítulo supondr-os (en realidad así loes), que todos los símbolos no terminales de la gramática tienenalgún atributo asociado, por default se considera que el atributode estos símbolos es del tipo entero.Considere la siguiente gramtktica, los atributos de lossímbolos no terminales y sus respectivas acciones semánticas.Atributo Reglas de la Gramática Acciones semánticasValValE:E+TI TT:T*FIFE.val


E .va1=3IT. val=3IF. val=33IIF. va114I'T . va158F.val=2II4 * 2F.valo2Fig. 4.1 Arbol de análisis sintático con BUS respectivas accionessemánticas.DEFINICION. Un atributo de un símbolo no terminal x essintetizado sí su valor depende de los valores de los atributosde los hijos de x en el árbol de parse.Para ilustrar la anterior definición veamos el siguientee j emplo :Ejemplo 2: Considérese la siguiente gramática con sus respectivosatributos de los símbolos no terminales y sus accionessamánticas.Atributo Reglas de la Gramática Acciones semánticasTipoTipoT : enterocaracterIL : idL, idIT.tipo


Tomemos como entrada la cadena w = "entero a,bn. Al leer elprimer token y <strong>hacer</strong> el shift, nos damos cuenta que formamos unmango con el símbolo de la cima de la pila (entero), por lo querealizarnos la reducción de acuerdo ala producción T : entero; surespectiva acción semántica nos indica que a T.tipo le asignamosent. Leemos el siguiente token (a), y lo üerrplazaiaos a la cima dela pila, puesto que forma un mango de acuerdo ala producción L :id, realizamos la reducción, y su acción semántica es: leasignamos a ap-idA.tipo L.tipo (nótese que el atributo de L loigualamos en la primer regla al atributo de T, por lo tantoL.tipo = T.tipo = ent). Contin<strong>uam</strong>os así hasta obtener elsiguiente árbol de derivación junto con sus respectivas accionessemánticas.1T.tipo=entSI+arL.tipo=ententeroa I bFig. 4.2 Arbol de análisis sintático con sus respectivas accionessemánticas.DEFINICION. Un atributo de un shbolo no terminal x es heredadosí su valor depende de los valores de los atributos de loshermanos o del padre de x en el árbol de parse.De acuerdo a las definiciones concluimos que en el ejemplo 2 elsímbolo no terminal T es del tipo sintetizado, pues el valor deéste depende del token leido (su hijo) i y el símbolo no terminalL es del tipo heredado, pues su atributo depende del atributo deT (su hermano en la derivación del árbol de parse).DEFINICION. Alas producciones con atributos y reglas semánticasse les llama "Definición dirigida por la sintaxisBg.Si revizamos los ejemplos anteriores nos damos cuanta que noindicamos en que momento se deberían ejecutar las accionessemánticas, pues siempre las colocamos al final; por lo cual82


utilizaremos el mismo ejemplo 2, <strong>para</strong> indicar cuando see j ecutarhnEJEMPIX) 3:DECL : T (L.tipo


~%%PROGDECLLISTATIPOCUERPOINSCOND..I.I.DECL { printf(98main()\nn); } CuERPo "."var LISTA f8:88 TIPO { printf(w8s áe;\nn,Tipo.t,Lista.1); }id { Lista.1 = yytext }LISTA II,II id ( Lista.1 = concat(Lista.1, I) n,yytext) ;integer ( Tip0.t = nintn }character { Tip0.t = "char"}begin { printf(" { \nw) } INS end ( printf(" }\n) }while { printf("whi1e ( ): } COND do INSid ( printf("%s ",yytext) } {printf(n=-n) }id { printf(Il%s ) n,yytext) }Si introducimos el siguiente fragmento de programa escrito enPascalVara,b: integer;beginwhile a = b doend .obtendríamos como salida el siguiente fragmento de programaescrito en lenguaje Cint a,b;main (){while (a==b)De acerdo a la sección 3.4.2 <strong>para</strong> la especificación de entrada<strong>para</strong> el compilador Yacc, un símbolo no terminal tiene por defaultun atributo entero. Si se desea declarar un atributo distinto alde default se debe de realizar de la siguiente manera: se declarael tipo o tipos y se introducen en una unión tal como se ilustraen el siguiente ejemploEJEMPLO 5: Mostremos un esquema de traducción en unaespecificación de entrada <strong>para</strong> Yacc.84


De acuedo a lo visto en el capitulo anterior, el atributo delsímbolo no terminal del lado izquierdo de la producción loidentificamos con $$ y los atributos de los símbolos noterminales del lado derecho de la producción los identificamoscon $1,$2,...,$n.%1/* Declaración de los atributos */struct nodo { /* apuntador a un nodo de una tabla de hash,utilizada como tabla de símbolos */char *apnombre; /* apuntador al nombre de lavariable */struct nodo *sig-nodo: /* apuntador al siguiente nodo */1struct val-1 { /* declaracion de números o literales */int val; /* si fue número, guardamos su valor */struct nodo *1[10]; /* si fue literal la guardamos aqui */1typedef struct nodo* tipo-Atypedef struct val-1 tipo-ET%union 4%type A%type E%type T/* declaramos el atributo del símbolo noterminal A, del tipo 1. *//* declaramos el atributo del símbolo noterminal E, del tipo 2. *//* declaramos el atributo del símbolo noterminal T, del tipo 2. */%IA/* gramática con acciones semánticas */id {$$=ap-id) E { printf(w%sw,$$->ap nom);if ($$.val != O ) printf(N%dw,$4.val):ETE 8+8 T { $$.val = $l.val + $3.val;$$.l = concatena($l.l,$3.val): )T { $$ = $1 1id { $$.val = O:$$.i[o] = ap-id;85


$$.1[1] = NULL;11 nun { $$.val = valor-nun;$$.l[O) = NULL:1Nota: Cuando Yacc encuentra acciones enmedio de los símbolos noterminales, sustituye estas por un símbolo no terminal inventadopor él. Este símbolo no terminal forma una nueva producción lacual es colocada al final; el símbolo no terminal deriva lacadena vacía y realiza las acciones eliminadas de la reglaanterior. Modificando así la forma de programar los atributos.Un ejemplo de ello es la producciónA : id {$$=ap-id) '=' E { printf(N9sw,$$->ap-nom);if ($$.val 1s O ) printf(n2dN,$4.val);< laacción semántica que se ecuentra entre id y *=' seráreemplazada por el símbolo no terminal X, el cual derivará lacadena vacía y realizará las acciones semánticas eliminadas, estoes IA : id X E ( printf(ll%sll,$$->ap-nom) tif ($$.val != O ) printf(H%d11,$4.val);Nosotros podemos realizar esto, <strong>para</strong> ganar claridad, pues ahora$$ en la producción derivada por X, es el atributo de X y no deA, el atributo de A lo referenciaremos por $$# donde tipoes el tipo del atributo del símbolo A.Además, <strong>para</strong> referenciar a los atributos de los simbolos noterminales de la regla de A, dentro de la regla de X, se hará dela siguiente manera: se coloca en vez de $x# $-x, endonde x=O,1,2,...,nf x indicará el lugar del símboloreferenciado, en donde O indica el símbolo inmediatamente a laizquierda de él (en este caso id), 1 indicará el segundo símboloencontrado a la izquierda de él, etc. , y es el tipo delsímbolo no terminal X.Por lo cual debemos de introducir esta regla con sus accionessemánticas de la siguiente maneraA : id X E { printf("%c@@,$$->ap-nom);if ($$.val != O ) printf(H%d1q,$4.val) t86


A continuación explicaremos las acciones semánticas de lagramática dada como ejemplo al final del capítulo 3 y en elsiguiente capítulo produciremos el código de dicha graiaática.Las especificaciones semánticas requeridas <strong>para</strong> nuestra gramáticason :1.- Que una variable no sea declarada más de una vez.2.- Que una variable se declare antes de usarse.3.- Que el número de argumentos de una llamada sea igual alnúmero de argumentos de una función.4.- Que las constantes tipo string sólo se utilizen como<strong>para</strong>metros5.- Que en una llamada de la forma f(xl,x2,. ..,xn), f haya sidodeclarada como una función.ias reviciones semánticas que realizaremos son:1.- Declarar un identificador antes de usarlo.2.- No declarar un identificador más de una vez.3.- Revizar los tipos de operandos.4.- Revizar el número de argumentos.Para esto nos ayudaremos de algunas variables globales como:- Declaración: contendrá el valor de 1, si al revizar el archivode entrada esperamos que la variable seadeclarada y contendrá el valor de O, si esperamosque la variable ya haya sido declarada, pues seva a ocupar en alguna expresión.- Es-Global : contendrá el valor de 1, si el lugar en donde seencuentra declarada la variable es de ámbitoglobal, y contendrá O, en caso de ser de ámbitolocal.y además utilizaremos algunas funciones, entre ellas una <strong>para</strong> elmanejo de identificadores la cual será ejecutada por elanalizador léxico, el cual regresará al analizador sintáctico elapuntador a la localidad donde insertó el identificador, o erroren otro caso.87


F"CI0N maneja-id (id)COMIENZASI (declaración = 1) ENTONCES /* declaramos */SI (EsGlobal = 1) ENTONCES /* de manera global */Comienzaap


Introduciremos las siguientes acciones semántfcas a una parte dela gramática presentada al final del capítulo 3S1 : Declaración


1 I ifuncion id $$2 ( )\ IfuncionIO: ENCA : ifuncion id $$1 ( ARGS )fIO: ENCA : funcion mid $$1 ( ARGS )1 I funcion mid $$2 ( )ENCA : funcion id i$$i ( AF2GS )I funcion id m$$2 ( )funcionI funcion id i$$2 ( )conflictoreducir-reducirPara eliminar este conflicto tenemos que modificar un poco lagramática de entrada <strong>para</strong> Yacc.Nota: Para ver todos los conflictos generados por nuestragramática y reportados por Yacc, tenemos que invocar a Yacc desdeel sistema operativo de la siguiente manera:a:\> yacc -u Yacc producirá como salida un archivo llamado youtput, el cualcontendrá todos los conflictos de nuestra gramática.Además de modificar la gramática <strong>para</strong> Yacc, agregaremos a losnodos de la tabla de símbolos los siguientes campos: tipo, númerode argumentos, y otros más que posteriormente se irán mencionandoconforme se vayan necesitando. Esto con la finalidad de facilitarlas acciones y revisiones semánticasLa regla modificada de nuestra gramática quedará:90


ENCA : funcion id (52) ( FF : ARGS (ap-fun->num-args = $1: 1{ap-fun->num-args = O; }I )ARGS : id (S2) ($$ = 1:)I id (S2) , ARGS { $$ = 1 + $4: )si vemos, la regla ARGS no generará conZlicto pues el tokenadelantado puede deducir cual es la producción que evaluará.A continuación veremos otra parte de la gradtica y le anexaremossus acciones semánticas como:S5 : ap-fun = ap-id:S6 : SI (ap fun->num-args $4) ENTONCES ERRORs7 : $$ = eñteroLa otra parte de la gramática quedaráINS : id {S7} = EXP {SI ($4 entero) ENT ERROR: }I regresa ( EXP ) {SI ($3 entero) ENT ERROR; )COND : EXP RELOP EXP { SI ($loentero) o ($3oentero)ENT ERROR:OTRO $$ = entero: }EXP : TERMINO { $$ = $1 )I EXP ADOP TERMINO ( SI ($loentero) o (S3oentero)ENT ERROR:OTRO $$ = entero: }TERMINO : FACTOR ( $$ = $1; }I TERMINO MULOP FACTOR { SI ($loentero) o (S3oentero)ENT ERFIOR;OTRO $$ = entero: }FACTOR : id { $$ = ap - id->tipo; }I id() ( SI (ap id->num-args O) ENT ERROROTRO 3s entero;I id (S5) ( LISTA-EXP ) {S6}I numero { $$ = entero: 1I ( EXP 1 { $$ = $2: 1LISTA-EXP : EXP { $$ = 1; }I EXP , LISTA-EXP { $$ = 1 + $3: }91


En el siguiente capítulo veremos como introducir las accionesde generación de código a nuestra especificación de entrada <strong>para</strong>Yacc, lo iremos haciendo paso a paso tal y como hems estadointroducciendo las acciones semántica., <strong>para</strong> que al finalmostremos el compilador completo con todas las fases contempladasdentro de la gramática de entrada.92


CAPITULO 5 OENBRACION DS COD160La fase de generación de código es la última y <strong>para</strong> estogeneraremos código <strong>para</strong> una máquina y un procesador enparticular, tomaremos <strong>para</strong> esto la familia de proce8adores del8086. Para esto necesitaremos tener algunoe antecedentes sobre ellenguaje ensamblador de la &quina, por lo tanto a continuacióndaremos una breve explicacíon sobre dicho lenguaje.5.1 LENGUAJE ENSAMBLADOR PARA EL 808808086.El procesador 8086 puede tener como máximo lmb. de memoria deentre los cuales serán repartidos de la siguiente manera:1ICOMMAND. COMSIST. OPARTIVOUTILES PARA-> 1 Mb.364 KbfTECLADO, PUERTOS, ETC .Fig. 5.1 División de la memoria del 8086.Tiene 14 registros algunos de ellos de propósito general como:algunos de direccionamiento y de apuntadores como


SI : source index. Se utiliza <strong>para</strong> marcar el inicio de una cadenaa copiarDI : destination index. Se utiliza <strong>para</strong> marcar el destino de unacadena a copiarSP : stack pointer. Indica el aputador a la pila.IP : Instruction pointer. Contiene la dirección de la siguienteinstrucción a ejecutarse.BP : base pointer. Se utiliza <strong>para</strong> el manejo de la pila, sobretodo <strong>para</strong> el paso de <strong>para</strong>metros por el stack.los registros de segmento (almacenan la dirección de inicio delsegmento)CS : code segment. Contiene la dirección de inicio del segmentode código.DS : data segment. Contiene la dirección de inicio del segmentode datos.SS : stack segment. Contiene la dirección de inicio del segmentode stack.ES : extra segment. Contiene la dirección de inicio del segmentoextra utilizado generalmente <strong>para</strong> datos.y por último tenemos el registro de banderas.Cuando realizamos una llamada de una función en nuestroarchivo de entrada a ser compilado, lo que realizaremos esguardar los <strong>para</strong>metros de la función, la dirección de retorno,las variables locales y el registro de activación en el stack. EnDS guardaremos las variables globales. Por lo cual los modos dedireccinamiento que utilizaremos en nuestro cornpilador serán:Direccionamiento directo: <strong>para</strong> variables globales (DS).Direccionamiento base: <strong>para</strong> variables locales (SS).cs 1DSssBP > ..XES4var a,b; (globales1[bp+2]+SS {x local}Fig. 5.2 Variables globales y locales dentro de la memoria.94


veamos algunas de las operciones del lenguaje ensambladorMOV destino, fuente : destino c--fuenteoperandos: registro, dirección y constantes.e j emplo :MOVMOVregreg , dirctedir , regcte<strong>para</strong> referenciar nuestras variable ejemplificadas en la figura5.2 tenemos que <strong>hacer</strong>lo asíglobales: a, b,...locales : x-2, y-2 o bien y-r, donde r es el número de bytesdesde el BP hasta donde se localiza la variable y en el SS.de tal forma que <strong>para</strong> intercamibiar el valor de a con el de xtendríamos que <strong>hacer</strong> algo similar a lo siguiente:MOV AX,AMOV BX, [BP+2]MOV A, BXMOV [BP+2],AXADD destino, fuente ; destino c-- destino + fuenteSUB destino, fuente : destino c-- destino - fuenteejemplo; x = a+2-yMOV AX, WORD PTR AADD AX, 2SUB AX, WORD PTR [BP-21MOV WORD PTR[BP+2], AXIMUL OP ; DX:AX C--AX * OPdonde op puede ser un registro o una localidad de memoriaejemplo: a


IMüL BXMOV WORD PTR A, AXIDIV OP : AX


siguientes pasos:IP B ENTONCES A = A+i OTRO BPXen ensamblador quedaría codificado:MOV Ax, ACMPAx, BJG ENTONCESJMP OTROENTONCES: ADD WORD PTR A, 1JMP FIN-SIOTRO: MOV AX, WORD PTR[BP-21MOV WORD PTR B, AXFIN-SI : ....MIENTRAS A>B HAZA = A-1;en ensamblador quedaría codificado:MIENTRAS: MOV AX, WORD PTR ACMP AX, BJLE FIN MSUB WORD PTR A, 1JMP MIENTRASFIN-M : ....97


5.2 GENERANDO CODIGO.Para la fase de generación de código de nuestro compilador,necesitamos tener los siguientes campos en cada nodo de nuestratabla de símbolos: nombre, tipo, num-args, sig, offset,num-var-locales, global. Esto <strong>para</strong> facilitar la generación decódigo.Introduciremos una nueva variable global llamada "offset" yalgunas acciones de generación de código como:s1 : ap-fun c-- ap-id;52 : ap-fun->nun-locales offset = offset;offset = offset-2;I LISTA-ID , id ; { $$si;ap id->offset = offset:offset = offset-2;1FUN-DECL : ENCA DECLS (S2) CUERPOENCA : funcion id {Sl) ( ARGS ) (S3)I funcion id ()ARGS: id { ap-id->offset = offset;offset = offset-2;1I id { ap-id->offset = offset;offset = offset-2;1, ARGSy así contin<strong>uam</strong>os sucesivamente hasta obtener la siguienteespecificación de entrada <strong>para</strong> Yacc.98


5.3 EJEMPm DE UN COMPILADOR COMPLETO./* Archivo con las especificaciones de entr8ba <strong>para</strong> YACC y conlas acciones semanticas y de generación de código requeridas*/NODO ap-fun;NODO ap loc;NODO apIasig;void inic-tab simb() ;int declaradon;int es-global ;int ap-id;extern NODO tab simb-loc[TAMSIMB];extern FILE *AS%;extern char tab-~ad[MAXCAD][80];extern int pos-cad;int offset=O;extern int val-num;extern int num-linea;unsigned i;REGISTRO reg, emiteadop () , emite-mulop () ;void escribe-op(), emitegush(), emite-salto-neg();void gen-etiqo, emite-asig(), emite-com<strong>para</strong>();%start programa%token ENTERO FüNCION PRINCIPAL%token ENTONCES SI OTRO MIENTRAS HAZ REGRESA%token ID NüM CAD%token MA1 MAY ME1 MEN NO1%token SUM SUB MüL DIV%union (int tipoi;tipoexp tipo2;char cad[l4];I%type decls%type decl%type lista-id%type args%type lista-exp%type adop%type mulop%type relop%type exp%type termino%type factor%type X1%type X2%type X399c


%%programactesdeclsdecllista-idf un-decl sfungrincfun-decl: (declaracion = TRUE:es-global = TRUE;fprintf (ASM, NINCLüDE INICIO.Asn\nN) :fprintf(ASM,WATOS SEGXENT PüBLiC\nn);} ctes decls ( fprintf (ASH, NMTOS ENDS\nn) ::: /* vacia */I ctes ID (ap-id->tipo = cad:fprintf(AsH8NaDIm SEGMEhfT WBLIC\nn):) fun-declsfungrinc {fprintf(ASM,%ET PRINCIPAL :\nn):fprintf (ASM, VOP- si\nPOP dx\nPOcx\nPOP bx\nPOP ax\nRET\n"):fprintf (ASM, VRINCIPAL ENDP\nN) :fprintf(ASM,NCODIGO ENDS\nn);fprintf (ASM,%ND ejecuta") :}fprintf(ASM,"%s LABEL BYTE\nn,ap id->simbolo);}f=f CAD 1.f , ( for ( i-O : icstrlen (tab-cad [ peg-cad] ): i++)fprintf (ASM, DB %d\nn,tab-cad[pos-cad]fprintf (ASM," DB O\n") t )#: /* vacia */ ($$=O;)I decls decl ($$=$1+$2:):: ENTERO lista-id 'tf ($$=$2;}:: ID ($$=l;ap-id->tipo = ent;ap id->offset = offset:offset -= 2:if (ap-id->global==TRUE)fprintf(ASM,"%s DW O\nn,ap - id->simbolo);}I lista-id f , f ID ($$-$l+l;ap-id->tipo = ent;ap-id->offset = offset:offset -= 2;if (ap-id->global==TRUE)fprintf(ASM,N%s DW O\n",ap-id->simbolo):: /* vacia */I fun - decls fun-decl:: FüNCION PRINCIPAL '(' O ) f (declaracion = FALSE:es-global = TRUE:fprintf(ASM,nPRINCIPAL PROC NEARfprintf(ASM,"JSH ax\nPüSH bx\nPcuerpo:: enca decls (declaracion = FALSE;ap-fun->no-locales = $2;fprintf (ASM, "MOV ax, O\nn) :for (is0: i


;) cuerpo {inic-tab-sinb(tab-simb-loc);declaracion = TRUE;es-global = TRUE;fprintf(-,nRET-%s :\nn,ap-fun->simbofprintf(ASBí,nADD SP,%d\nn,2*ap-fun->nofprintf(ASM,VOP si\nPOP dx\nPOP cx\nPfprintf(ASM,@@bs ENDP\nn,ap-fun->sinbol1enca : F"C1ON ID{ap-id->tipo = fun;ap-fun = ap-id;es-global = FALSE;fprintf(ASM,n%s PROC NEAR\nu,ap-fun->simboio);fprintf(ASM,nPUSH ax\nPUSH bx\nPüSH cx\nPüSH dx\nPUSH si\n) '(' <strong>para</strong>metros;<strong>para</strong>metros : {offset = -2;) args ')* {ap-fun->nogar = $2;offset -= 12;)I 1)' {offset = O; ap-fun->nogar = O;)Iargs : ID {$$ = 1;ap id->tipo = ent;ap-id->offset = offset;offset -= 2;)I ID {ap-id->tipo = ent;ap id->offset = offset;ofTset -= 2;) 8 , ' argsIcuerpo : '{' lista-ins ')'tlista-ins : insI ins ';' lista-insinsx1IC$ = 1 + $7;): ID {if (ap-id->tipo != ent)maneja-error(ap-id->simbolo,num-linea,4);ap-asig = ap-id;} '=I exp{if ($l.tipo != ent)maneja-error ("EN ASIGNACIONn, nu-linea, 5) ;emite-asig(ap-asig,$4);}SI cond ENTONCES X1 insts OTRO X2 insts (fprintf(ASM,"%s :\SI cond ENTONCES X1 insts (fprintf(ASM,n%s :\nn,$4);}MIENTRAS X3 cond HAZ X1 insts (fprintf(ASM,nJMP %s\nn,$2);fprintf (ASM,"%s :\nn, $5) :}I REGRESA '(' exp ')' {if ($3.tipo != ent)maneja error ("EN INSTRUCCION regrefprintf (A&, "MOV di, ") ;escribe-op(S3) ;fprintf (ASM,"JMP RET-%s\n", ap-fun->simt: /* vacia */ {emite-salto-neg($-1);gen-etiq ( $$I :fprintf (ASM, "%s\nn, $$) ; }I101


x2x3instscondexpterminofactor: /* vacia */ {gen-etiq($$);: /* vacia */ (gen-etiq($$);: insI cuerpo;fprintf(ASM,nJMP %s\na,$$);fprintf(ASM,n%s :\nu,$-2);)fprintf (ASM, a%rs : \na, $$) t ): exp reiop exp (if ($lotipo I= ent 11 $3.tipo I= ent)manej a-error ("EN OPERACION DE COMPARACIONemite com<strong>para</strong>($i,$3) t$$ = $2;)t: termino ($$ = $1;)I exp adop termino (if ($l.tipo I= ent 11 $3.tipo != ent)maneja-error(aEN OPERACION r+r O 8-1w I$$.tipo = enttreg = emite-adop($1,$2,$3) ;$$.clase = REG;$$.registro = reg:)I: factor ($$ = $1;)I termino muiop factor {if ($i.tipo I=;: ID ($$.tipo = ap-id->tipo;$$.clase = MEM;$$.pos = ap-id;)ent 11 $3.tipo != ent)maneja-error("EN OPERACION 8*r O$$.tipo = enttreg = emite-muiop($i,$2,$3);$$.clase = REG;$$.registro = reg;)I ID '(' (ap-loc = ap-id;fprintf (ASM,"PUSH BP\nn) ;)argumentos ($$.tipo = ent;fprintf(ACM,"MOV BP,SP\nN)tfprintf(ASM,nADD BP,%d\nn,2*ap-loc->nogar);fprintf (ASM, VALL %s\n", ap-loc->simbolo) :fprintf (ASM,"WOV SP,BP\n") ;fprintf (ASM, 9vpoP BP\nn) ;$$.clase = REG;$$.registro= dit)INUM ($$.tipo = ent;$$.clase = CTE;$$.valor = val num;)I exp 8 ) 1 {$$ = $%I,argumentos : ')' (if (ap-id->nogar != O)manejaerror("EN LLAMADA AF"CIONn,num-linea,7);)I lista-exp ')' {if (ap-ioc->noqar != $1)maneja-error(wEN LLAMADA A FüNCIONn,num-l;lista-exp : exp ($$ = 1;emitejush ($1) ; )I exp 8,8 (emitegush(S1);) lista-exp ($$ = 1 + $4;)102


elopadopmulopt: MA1ME1MAYMENf=fNO1:: SUMI SUB:: MUL ($$ = MUL;)I DIV {$$ = DIV;}fdonde las funciones requeridas, como mulop, adop, etc., semuestran a continuación./* La funcion emite-adop() genera codigo ensamblador <strong>para</strong> lasoperaciones de suma y resta. Recibe los atributos de losoperandos: opl y op2; y tambien el codigo de la operacion: op.*/REGISTRO emite-adop(tipoexp opl, int op, tipoexp op2){REGISTRO reg;REGISTRO asigna reg ( ) ;void escribe-op (7;if (opl.clase != REG){reg = asigna-reg() ;fprintf(ASM,"MOV %s, ",nom-reg[reg]);escribe-op (opi);1elsereg = opl.registro;if (op===SUM)fprintf (ASM,"ADD I*) ;elsefprintf (ASM,"SUBif print f ( ASM, ll%s, , nom-reg [ reg] ) ;escribe-op (op2) ;if (op2.clase==REG)ocupado[op2.registro]=FALSE;return (reg);1/* La funcion asigna-reg () busca algun registro libre entre[ax,bx,cx,dx,si], que se va a utilizar <strong>para</strong> guardar algunoperando al generar codigo <strong>para</strong> la suma-resta, multiplicacion-103


division, los PUSH, las com<strong>para</strong>ciones y las asignaciones cuandono se puede utilizar el modo inmediato en alguna de estasoperaciones. */REGISTRO asigna-reg(){REGISTRO ind=axtwhile (ocupado[ind]==TRUE && ind


if (ocupado[ax]==TRUE) /* ax debe estar libre <strong>para</strong> guardar a opl *//* Si op2 esta en ax, guardamos op2 en otro registro que no sea dxif (op2.clase-REG && op2.registro==ax)(regl = busca-reg() tfprintf(ASM,"MOV %s,ax\na,nom~reg[reglJ)top2.registro=regi;1else/* Si opl no esta en ax, guardamos en la pila lo que este en axif (opl.clase!=REG I I opl.registro!=ax)(fprintf (ASM, "PUSH ax\n") tsalvo-ax=TRUE;1/* Guardarnos opi en ax si es que aun no esta hai */if (opl. clase! =REG) (fprintf (ASM, WOV ax, #I);escribe-op (opl):1elseif (opl.registro!=ax) (fprintf(ASM,VIOV ax,8s\n~~,nom~reg[opl.registroJ):ocupado[opl.registroJ=FA~,sE:1/* Si dx esta ocupado: */if (ocupado[ dx] )/* Si op2 en dx, copiamos op2 a otro registro */if (op2.clase==REG && op2.registro-dx)(regl = busca-reg() tfprintf(ASM,w8MOV %s,dx\nn,nom-reg[reglJ):1op2.registro=regi;/* Si op2 no esta en dx guardamos en la pila lo que este en dx */else(fprintf (ASM, "PUSH dx\n") tsalvo-dx=TRUE;}/* Para IMUL y IDIV op2 no puede ser una constante *//* en este caso debemos pasar op2 a un registro */if (op2. clase==CTE) (regl = busca-reg ( );fprintf(ASM,WOV %s,%d~~,nom~reg[regl],op2.valor);1/* Escribimos el NMONICO <strong>para</strong> la operacion */if (op==Mü~)fprintf (ASM,"IMUL Ig) telse(fprintf (ASM,"CWD\nn) tfprintf (ASM, IIIDIV @I) i1/* Escribimos el segundo operando op2 */if (op2.clase==CTE)fprintf (ASM, lt%s@*,nom-reg[regl))else105i


1escribe-op (opZ) t/* Si guardamos el contenido de dx en la pila, ponemos lo queeste en dx en otro registro y sacamos de la pila el valoranterior de dx. Lo mismo se hace <strong>para</strong> ax. */if (salvo dx) (reg2 =-busca-reg ( ) tfprintf(ASM,"MOV %s,dx\nn,nom-reg[reg2]);fprintf (ASM,"POP dx\nw) t1if (salvo-ax) {reg3 = busca-reg() tfprintf(ASM,%OV %s,ax\nn,nom-reg[r~3]);fprintf (ASM,"POP ax\nn) t1/* Liberamos los registros que ya no guardan nada importante */if (opl.clase==REG && opl.registro!=ax)ocupado[opl.registro]=FALSE;if (op2. clase-REG)ocupado[op2.registro]=FALSE~elseif (opZ.clase==CTE)ocupado[regl]=FALSE;/* Retornamos el resultado de la operacion */if (salvo ax)return(reg3 telse(ocupado[ax]=TRUE;return (ax) t1/* La funcion buscarego es similar a la funcion asigna-reg() ,solo que en este caso los registros ax y dx se suponen nodisponibles. */REGISTRO busca-reg(){if (!ocupado[bx])return (bx) telseif (!ocupado[cx])return (cx) telseif (!ocupado[si])treturn (si) tprintf ("REGISTROS OCUPADOS\n PROGRAMA ABORTADO\n") iexit (1) t/* La funcion emite-push() se utiliza al <strong>hacer</strong> el paso de<strong>para</strong>metros, antes de llamar a una funcion.*/void emitegush (tipoexp op)106


{REGISTRO reg;/* No se puede <strong>hacer</strong> un PUSH de una constante, antes hay quepasar esta a un registro. */if (op.clase==CTE){reg = asigna-reg() ;fprintf (MM,"MOV %~,~~,nom-reg[regJ) iescribe-op (op) ifprintf(MM,"PUSH %s\nm,nom-reg[regJ)iocupado[reg]=FALSE;1elseif (op.clase==REG)fprintf(AS~,w~ü~~ %s\n",nom-reg[op.registro~) telseif (op. pos->global==FAiSE)fprintf(ASM,"PUSH WORD PTR [BPld]\nw,op.pos->offset);elseif (op.pos->tipo==ent)fprintf (ASM,llPüSH WORD PTR %s\n",op.pos->simbolo) t/* Si el operando es el identificador de una cadena, hayque <strong>hacer</strong> el PUSH del OFFSET de ese identificador */else{reg = asigna-reg() ;fprintf(ASM,WOV %s,OFFSET %s\nn,nom-reg[regJ,op . pos->aimbolo) tfprintf (ASM,VüSH %s\ntl,nom-reg[regJ) tocupado[reg]=FALSE;1/* La funcion emite salto-neg() escribe en el archivo de codigoensamblador el NMONTCO <strong>para</strong> el salto condicional cuando generamoscodigo <strong>para</strong> las sentencias SI, SI OTRO y MIENTRAS. */void emite-salto-neg(int op){switch (op){case MAI : fprintf (ASM,"JL ") tbreak:case ME1 : fprintf(ASM,"JG n):break;case : fprintf (ASM,"JNE ,I) tbreak;case MAY : fprintf (ASM,lIJLE ") tbreak tcase MEN : fprintf (ASM,"JGE ") tbreak;case NO1 : fprintf (ASM,l@JE I,) ;11/* i,a funcion gen etiq genera una etiqueta <strong>para</strong> utilizarla en lossaltos condicioñales <strong>para</strong> las sentencias: SI, SI OTRO y107


MIENTRAS. */void gen-etiq(char etiq[l4]){1/*char etiql[l4];char etiq2 [ 141 ietiq[O]='\O'ietiqi[0]='\o8;etiq2[O]='\08;num-etiqueta++; /* global */itoa(num-etiqueta,etiql,lO)istrcat (etiq2, "EH) istrcat(etiq2,etiql);strcpy ( et iq , et iq2 ) iLa funcion emite-asigo genera codigo <strong>para</strong> las instruccionesde asignacion de la forma id = exp donde: id es una variable dememoria global o local y exp puede ser una variable de memoriaglobal o local, una expresion aritmetica, un numero entero, ouna llamada a funcion. */void emite-asig(NODO opl, tipoexp op2){REGISTRO reg;if (op2.clase==MEM)(reg = asigna-reg() ifprintf (ASM,"MOV %s, U, nom-reg [ reg] ) iescribe op (op2) iif (opl=>global==TRUE)fprintf(ASM,WOV WORD PTR %s,%s\nn,opl->simbolo,nom-reg [ reg J ) ielsefprintf(ASM,"MOV WORD PTR [BP%d],b\nn,opl->offset,nom-reg[reg]):ocupado[reg]=FALSE;1else{if (opl->global==TRUE)fprintf (ASM, I'MOV WORD PTR %s,",opl->simbolo) ielsefprintf(ASM,"MOV WORD PTR [BP%d],n,opl->offset);escribe-op (op2) iif (op2.clase==REG)ocupado[op2.registro]=FA~~E;11/* i,a funcion emite-com<strong>para</strong>() genera codigo <strong>para</strong> las expresionesen que se deben com<strong>para</strong>r dos expresiones. Es exactamente sihicieramos una resta, pero no se retorna ningun resultado, solose modifica el registro de banderas.*/108


void emite-com<strong>para</strong>(tipoexp opl, tipoexp opa){REGISTRO reg;1if (opl.clase != REG)(reg = asigna-reg() ;fprintf (ASM, mmMOV $8, In, nom-reg[ reg J ) tescribe-op (opl);1elsereg = op1.registro;fprintf (ASM,WMP %~,~~,nom-reg[reg]) ;escribe-op (op2) ;if (op2. clase==REG)ocupado[op2.registro]=FALSE;ocupado[reg]=FALSE;109


BIBLIOGRAFIA.Aho,Sethi, Ulman, Vompilers principles techniques and designtoolsn, Addison Wesley.Aho, Hopcrof t , and Ulmman, @@Data structures andalgoritmsn,Addison Wesley, Reading, Xass., 1974.Backhouse, %intax of programming lenguages theory and practice1@,Prentice Hall.Trembly, Sorenson, "The theory and practice of compilerwritting" , Mc Graw-Hill.Manual de Referencia de Yacc.Manuel de Referencia de Lex.110

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!