Análisis Sintáctico Descendente Tema 4 Juan A. Botı́a Blaya juanbot@um.es http://ants.dif.um.es/staff/juanbot/traductores/traductores.html Departamento de Ingenierı́a de la Información y las Comunicaciones Universidad de Murcia ´ ´ Analisis Sintactico Descendente – p.1/65 Índice Tema 4. Análisis Sintáctico Descendente 1. Análisis descendente general con retroceso 2. Análisis descendente predictivo (a) Gramáticas LL(1). Construcción de PRIMERO y SIGUIENTE. Construcción de la tabla de análisis (b) Análisis descendente predictivo no recursivo (c) Análisis descendente predictivo recursivo ´ ´ Analisis Sintactico Descendente – p.2/65 Análisis descendente general con retroceso Sea una gramática cualquiera, G del tipo CFG y su correspondiente AP no determinista. Vamos a simular los movimientos de ese AP ante una cadena de entrada w. Para cada transición del autómata vamos a tener un conjunto de posibles movimientos que podríamos realizar para simular la lectura de un determinado símbolo de w. Podríamos ir comprobando todos los posibles movimientos que se pueden generar a partir de cada transición (cjto. transiciones destino finito). Inconveniente: puede dar lugar a un aumento exponencial en el número de caminos a explorar. Ventaja: método muy sencillo de codificar. Si w ∈ / L(G) entonces habrá que hacer una búsqueda exhaustiva de todos los posibles movimientos del automata. Si w ∈ L(G), Podría interesarnos el primer árbol de derivación que obtengamos Si quisieramos obtener todos los posibles árboles de derivación que puedan darse la búsqueda debería ser también exhaustiva. ´ ´ Analisis Sintactico Descendente – p.3/65 Ejemplo Informal Sea la gramática G = (VN , VT , P, S), con las producciones de P = {S → aSbS|aS|c}. Asumimos como orden predeterminado el mismo en el que aparecen. Sea la cadena de entrada w = aacbc. Usaremos un apuntador, que nos indica en todo momento el símbolo ai ∈ w que estamos intentando reducir. El árbol de derivación va a contener inicialmente el símbolo S. En todo momento, en el árbol va a haber un nodo activo. Inicialmente el nodo activo es S. ´ ´ Analisis Sintactico Descendente – p.4/65 Ejemplo (Cont.) A partir de ahí se van a ejecutar los siguientes pasos: 1. Si el nodo activo está etiquetado con A ∈ VN , se escoge la primera alternativa correspondiente a las partes derechas de A. Sea esta X1 X2 · · · Xk . Creamos k descendientes directos: X1 , X2 , . . . , Xk . Ahora X1 es el nodo activo (si k = 0 el nodo activo es el que se encuentra inmediatamente a la derecha de A). 2. Si el nodo activo está etiquetado con a ∈ VT , se compara a con el símbolo de w apuntado por el apuntador anteriormente mencionado. Si coinciden, el nodo activo pasa a ser el inmediatamente derecho de el etiquetado con a, y se avanza el puntero un lugar a la derecha. Si no, se vuelve al nodo en donde se aplicó la producción última, se ajusta el puntero y se aplica la siguiente producción de entre las alternativas. Si no hubiera más alternativas entonces, volver a subir un nivel, y repetir el proceso. ´ ´ Analisis Sintactico Descendente – p.5/65 Ejemplo (Cont.) Si aplicamos el proceso anterior al ejemplo previamente introducido, etiquetamos al primer nodo con S y lo activamos. Al elegir y aplicar la primera S−producción, tenemos el árbol siguiente: S a S b S ´ ´ Analisis Sintactico Descendente – p.6/65 Ejemplo (Cont.) Ahora hacemos que el nodo activo sea el etiquetado como ’a’. Dado que a1 = a, avanzamos el apuntador que pasa ahora a señalar a a2 . Hacemos nodo activo a S (i.e. el que se encuentra inmediantamente a la derecha de a) y aplicamos la primera S−producción, obteniendo el árbol parcial siguiente: S a b S a S b S S ´ ´ Analisis Sintactico Descendente – p.7/65 Ejemplo (Cont.) Volvemos a hacer activo el nuevo nodo etiquetado con ’a’ del nivel más profundo del árbol, y comprobamos que el caracter a2 coincide con ese terminal. Por lo tanto, actualizamos el puntero a a3 , y hacemos activo el nodo de la derecha (S ), y aplicamos la primera S−producción a partir de él. Obtenemos: S S S b S a a b S a b S S ´ ´ Analisis Sintactico Descendente – p.8/65 Ejemplo (Cont.) En el que, despues de hacer nodo activo el más a la izquierda de los recientemente generados, podemos comprobar que a3 6= a, y por lo tanto rechazamos esta alternativa. Volviendo a la siguiente S−producción, expandimos nuevamente el árbol y generamos S b S a b S a a S S S que tampoco va a coincidir. ´ ´ Analisis Sintactico Descendente – p.9/65 Ejemplo (Cont.) Si probamos la última opción, S a b S a S b S S c vemos que, haciendo previamente nodo activo al que se acaba de generar, a3 = c. ´ ´ Analisis Sintactico Descendente – p.10/65 Ejemplo (Cont.) Como al nivel de profundidad actual no hay más nodos a la derecha del etiquetado con c, subimos un nivel en el árbol, haciendo activo al nodo b, a la derecha de el que acabamos de expandir (S). Se comprueba que su etiqueta coincide con a 4 y por lo tanto, actualizamos el apuntador, y hacemos activo el nodo a la derecha del etiquetado con b, que lleva la etiqueta S. Si lo expandimos con la primera opción fallará, al igual que con la segunda. Sin embargo la tercera generará el árbol S a b S a S c b S S c ´ ´ Analisis Sintactico Descendente – p.11/65 Ejemplo (Cont.) Ahora la etiqueta del nuevo nodo activo coincide con el último símbolo, a 5 de w. Hemos hecho un reconocimiento de la cadena completa, sin embargo aun quedan bS: este camino no es el correcto. Se vuelve a recuperar como nodo activo al padre pero no existen más alternativas. Como es el que está más a la derecha, tenemos que recuperar como activo a su padre. Al llegar a b, en profundidad inmediatamente por debajo de la raíz el apuntador referencia a a2 , y el nodo etiquetado con la primera S de derecha a izquierda es nuevamente el activo. Elegimos la siguiente opción para S y obtenemos el árbol S a a b S S S ´ ´ Analisis Sintactico Descendente – p.12/65 Ejemplo (Cont.) El nuevo nodo activo, etiquetado con una a coincide con a 2 . Avanzamos el puntero y hacemos activo a S, a su derecha. Aplicamos la primera S−producción y obtenemos el árbol S b S a S S a b S a S que no resulta válido. Aplicando la segunda tampoco obtenemos un árbol adecuado. Si aplicamos la tercera, obtenemos S a b S S S a c Si seguimos aplicando el algoritmo de esta forma obtendremos como primer árbol de derivación válido, el siguiente: S a a b S S S c c ´ ´ Analisis Sintactico Descendente – p.13/65 Retroceso y Recursividad Si la gramática de entrada es recursiva, el procedimiento puede caer en ciclos infinitos. En el ejemplo de la producción A → ab|Ab, si llegamos a un nodo con A como etiqueta, la primera opción no daría problemas, pero para una cadena de entrada en la que fuera necesario probar con la segunda opción, se generaría un árbol de profundidad infinita. Si la recursividad no fuera inmediata, el problema aun persistiría. Si P = {S → AB, A → SC, . . .}, podríamos tener la secuencia de derivación S ⇒ SC ⇒ ABC que se repetiría indefinidamente. ´ ´ Analisis Sintactico Descendente – p.14/65 Retroceso y Recursividad Para evitar esto podríamos limitar el número de nodos del árbol de derivación dependiendo de la longitud e la cadena de entrada. Una gramática G = (VN , VT , P, S) con |VN | = k y para una cadena de entrada w tal que |w| = n − 1, si w ∈ L(G), existe al menos un árbol de derivación para w que no tiene una profundidad mayor que kn. El espacio de búsqueda de árboles de derivación de profundidad ≤ d es una función con un crecimiento muy fuerte, dependiente de d. Por ejemplo, para P = {S → SS|λ}, el número de árboles posibles, con profundidad d viene dado por la expresión recursiva: D(1) = 1 D(d) = (D(d − 1))2 + 1 Cuyo crecimiento puede verse en la siguiente tabla: Profundidad 1 2 3 4 5 6 Árboles Posibles 1 2 5 26 677 458330 ´ ´ Analisis Sintactico Descendente – p.15/65 Algoritmo de análisis sintáctico general descendente con retroceso Algoritmo 1 Análisis descendente general con retroceso. Entrada: Una gramática CFG, G = (VN , VT , P, S), sin recursividad por la izquierda, y una cadena de entrada w = a1 a2 . . . an , n ≥ 0. Las producciones en P han de estar numeradas, con índices 1, 2, . . . , p. Salida: Una derivación izquierda, si existe. Si no un error. Preparativos Para cada A ∈ VN , ordénense las alternativas de tal forma que si A → α1 | · · · αk entonces, Ai es el índice escogido para αi . Todos los Ai , 1 ≤ i ≤ k van a formar el conjunto IA que determina un orden fijo. El algoritmo va a funcionar a la manera de un autómata de pila. Una configuración va a estar formada por una cuádrupla (s, i, α, β), en donde: s denota un estado en {q, b, t}. El primero, q, denota que se está en situación normal. El estado b denota que se está en situación de retroceso. El tercero, t, que se ha reconocido la cadena. i representa el símbolo actual ai de w en el que nos encontramos en el análisis. El símbolo de entrada (n + 1)-ésimo es $. α representa una primera pila de símbolos, L1 , en la que se van a almacenar un histórico de todas las elecciones Ai para cada A ∈ VN que ha participado en una producción en el árbol parcial estamos construyendo junto con los correspondientes símbolos terminales de la cadena w que hasta ahora se han reducido. La vamos a representar con la cabeza de la pila a la derecha. β es una segunda pila de símbolos, L2 , que contiene la forma sentencial que se ha obtenido hasta el momento, y en su cabeza aparece el nodo activo actualmente, si usamos la terminología vista en el ejemplo informal anterior. La vamos a representar con la cabeza de la pila a la izquierda. ´ ´ Analisis Sintactico Descendente – p.16/65 Algoritmo de análisis sintáctico general descendente con retroceso La configuración inicial del algoritmo es (q, 1, λ, S$). La notación (s, i, α, β) ` (s0 , i0 , α0 , β 0 ) indica que el movimiento, desde la configuración (s, i, α, β) es hacia la configuración (s0 , i0 , α0 , β 0 ). El índice i, va a ser tal que 1 ≤ i ≤ n + 1, α ∈ (VT ∪ I)∗ , en donde I es el correspondiente cjto. de índices de alternativas, y β ∈ (VN ∪ VT )∗ . Los tres primeros tipos de movimientos son: 1. Expansión del árbol (q, i, α, Aβ) ` (q, i, αA1 , γ1 β) en donde A → γ1 ∈ P , y γ1 es la primera alternativa, según el órden establecido, para A que es el no terminal no expandido aun más a la izquierda en la frontera del árbol parcial. 2. Coincidencia del símbolo de entrada y un símbolo derivado (q, i, α, aβ) ` (q, i + 1, αa, β) siendo αi = a, e i ≤ n. 3. Reconocimiento de la cadena (q, n + 1, α, $) ` (t, n + 1, α, λ) Se observa que se alcanza el final de la entrada, con i = n + 1 y además se ha obtenido una derivación izquierda que es igual a w. El árbol de derivación izquierdoAnestá en α.Descendente – p.17/65 ´ ´ alisis Sintactico Algoritmo de análisis sintáctico general descendente con retroceso Los tres siguientes 4. No coincidencia del símbolo de entrada y el símbolo derivado (q, i, α, aβ) ` (b, i, α, aβ) if ai 6= a 5. Retroceso en la entrada (b, i, αa, β) ` (b, i − 1, α, aβ) siendo a ∈ VT . 6. Nueva alternativa (b, i, αAj , γj β) ` (a) (q, i, αAj+1 , γj + 1β), si γj+1 es la alternativa j + 1-ésima para A. (b) No hay más configuraciones. Si i = 1, A = S y solamente hay j alternativas para A. Esto quiere decir que hemos recorrido todo el árbol, y ya no quedan más alternativas que expandir. La cadena w ∈ / L(G). (c) (b, i, α, Aβ) en otro caso. Las alternativas de A se han acabado, y hacemos retroceso eliminando A de L1 y reemplazando γj por A en L2 . ´ ´ Analisis Sintactico Descendente – p.18/65 Algoritmo de análisis sintáctico general descendente con retroceso Ejecución: Paso 1: Comenzando en la configuración inicial, computar las configuraciones sucesivas C0 ` C 1 ` · · · ` C i ` · · · hasta que no se pueda calcular ninguna más. Paso 2: Si la última Cu computada es (t, n + 1, γ, λ) entonces calcular, a partir de γ la derivación obtenida y finalizar. Si no, emitir una señal de error y finalizar. Fin Algoritmo ´ ´ Analisis Sintactico Descendente – p.19/65 Análisis Descendente Predictivo ADGR → n3 como complejidad temporal, y n2 como complejidad espacial Vamos a estudiar un conjunto especial de gramáticas de análisis sintáctico con complejidad espacial y temporal c1 n y c2 n Número de gramática pequeño, aunque suficiente Algoritmos deterministas Con las gramáticas de tipo LL(k), concretamente las LL(1), va a ser suficiente el mirar el siguiente token en la cadena de entrada para determinar cual va a ser la regla de producción a aplicar en la construcción del árbol de derivación. Las gramáticas con análisis de una pasada son: Tipo LL(k) Tipo LR(K) Gramáticas de Precedencia Dado que este capítulo está dedicado al análisis descendente, nos centraremos en las gramáticas LL(K), y concretamente en las LL(1). ´ ´ Analisis Sintactico Descendente – p.20/65 Gramáticas LL(1) Para introducir formálmente el concepto de gramática LL(1) primero necesitamos definir el concepto de F IRSTk (α). Definición 1 Sea una CFG G = (VN , VT , S, P ). Se define el conjunto ∗ ∗ F IRSTk (α) = {x|α ⇒lm xβ y |x| = k o bien α ⇒ x y |x| < k} en donde k ∈ N y α ∈ (VN ∪ VT )∗ . Ahora podemos definir el concepto de gramática LL(k). Definición 2 Sea una CFG G = (VN , VT , S, P ). Decimos que G es LL(k) para algún entero fijo k, cuando siempre que existens dos derivaciones más a la izquierda ∗ ∗ ∗ ∗ 1. S ⇒lm wAα ⇒lm wβα ⇒ wx y 2. S ⇒lm wAα ⇒lm wγα ⇒ wy tales que F IRSTk (x) = F IRSTk (y), entonces se tiene que β = γ. ´ ´ Analisis Sintactico Descendente – p.21/65 Gramáticas LL(1) Ejemplo: Sea G1 la gramática con cjto. P = {S → aAS|b, A → a|bSA}. Vamos a ver que esta gramática es LL(1). Entonces, si ∗ ∗ ∗ ∗ S ⇒lm wSα ⇒lm wβα ⇒lm wx y S ⇒lm wSα ⇒lm wγα ⇒lm wy Si x e y comienzan con el mismo símbolo, se tiene que dar β = γ. Por casos, si x = y = a, entonces se ha usado la producción S → aAS. Como únicamente se ha usado una producción, entonces β = γ = aAS. Si x = y = b, se ha usado S → b, y entonces β = γ = b. Si se consideran las derivaciones ∗ ∗ ∗ ∗ S ⇒lm wAα ⇒lm wβα ⇒lm wx y S ⇒lm wAα ⇒lm wγα ⇒lm wy se produce el mismo razonamiento. ´ ´ Analisis Sintactico Descendente – p.22/65 Gramáticas LL(1) El decidir si un lenguaje es LL(1) es un problema indecidible. Vamos a definir ahora una gramática LL(1) Definición 3 Sea una CFG G = (VN , VT , S, P ). Decimos que G es LL(1) cuando siempre que existens dos derivaciones más a la izquierda ∗ ∗ ∗ ∗ 1. S ⇒lm wAα ⇒lm wβα ⇒ wx y 2. S ⇒lm wAα ⇒lm wγα ⇒ wy tales que F IRST1 (x) = F IRST1 (y), entonces se tiene que β = γ. Para poder construir un analizador sintáctico predictivo, con k = 1, se debe conocer, dado el símbolo de entrada actual ai y el no terminal A a expandir, cuál de las alternativas de la producción A → α1 | · · · |αn es la única que va a dar lugar a una subcadena que comience con ai . Piénsese, por ejemplo, en el conjunto de producciones siguiente: prop → if expr then prop else prop | while expr do prop | begin lista props end ´ ´ Analisis Sintactico Descendente – p.23/65 Gramáticas LL(1) Podríamos conseguir una gramática LL(1) Si se tiene cuidado al escribir la gramática, eliminando la ambiguedad, la recursión por la izquierda, y factorizandola por la izquierda. ´ ´ Analisis Sintactico Descendente – p.24/65 Factorizando una gramática por la izquierda Algoritmo 2 Factorización por la izquierda de una gramática. Entrada: la gramática G. Salida: Una gramática equivalente y factorizada por la izquierda. Método: Para cada no-terminal A, sea α el prefijo más largo común a dos o más de sus alternativas. Si α 6= λ, o lo que es lo mismo, existe un prefijo común no trivial, se han de sustituir todas las producciones de A, A → αβ1 |αβ2 | · · · |αβn |γ en donde γ representa a todas las partes derechas que no comienzan con α, por A → αA0 |γ A0 → β1 |β2 | · · · |βn Aplicar la transformación hasta que no haya dos alternativas para un no-terminal con un prefijo común no trivial. ´ ´ Analisis Sintactico Descendente – p.25/65 Ejemplo Ejemplo: Sea la gramática prop → if expr then prop | if expr then prop else prop | otra Si le aplicamos la transformación anterior, la gramática resultante sería prop siguiente_prop → if expr then prop siguiente prop | otra → else prop | λ Sigue siendo ambigua Aunque se puede expandir prop a if expr then prop siguiente prop, con la entrada if, y esperar hasta que if expr then prop haya aparecido, para decidir entonces si expandir siguiente prop a else prop ó a λ. Para la entrada else las dos gramáticas siguen siendo ambiguas. Veremos, más adelante, como solucionar este problema. ´ ´ Analisis Sintactico Descendente – p.26/65 Conjuntos F IRST y F OLLOW Conjuntos de apoyo para la construcción del analizador sintáctico descendente predictivo. Vamos a introducir el conjunto F OLLOWk (β) formalmente Definición 4 Sea G = (VN , VT , S, P ) una gramática CFG. Definimos F OLLOWkG (β), en donde k es un entero, β ∈ (VN ∪ VT )∗ , como el conjunto ∗ {w|S ⇒ αβγ junto con w ∈ F IRSTkG (γ)} Particularizándolo para F OLLOW1 ≡ F OLLOW , sea A un no terminal de una gramática determinada. Definimos F OLLOW (A) como el conjunto de terminales a tal que haya una derivación de la ∗ forma S ⇒ αAaβ, para algún α y β, si A es el símbolo más a la derecha en determinada forma sentencial de la gramática, entonces el símbolo $ ∈ F OLLOW (A). ´ ´ Analisis Sintactico Descendente – p.27/65 Algoritmo para el cálculo de F IRST Algoritmo 3 Cálculo del conjunto F IRST para todos los símbolos no terminales y terminales de la gramática de entrada. Entrada: Una gramática G = (VN , VT , S, P ) de tipo CFG. Salida: Los conjuntos F IRST (X) para todo X ∈ (VN ∪ VT ). Método: Ejecutar el siguiente método para todo X ∈ (V N ∪ VT ). 1. Si X ∈ VT , entonces F IRST (X) = {X}. 2. Sino, si X ∈ VN y X → λ ∈ P , entonces añadir λ a F IRST (X). 3. Sino, si X ∈ VN y X → Y1 Y2 · · · Yk ∈ P añadir todo a ∈ VT tal que para algún i, con 1 ≤ i ≤ k, a ∈ F IRST (Yi ) y λ ∈ F IRST (Y1 ), . . . , F IRST (Yi−1 ), o lo que es lo mismo, ∗ Y1 Y2 . . . Yi−1 ⇒ λ. Además, si λ ∈ F IRST (Yj ) para todo j = 1, 2, . . . , k, añadir λ a F IRST (X). ´ ´ Analisis Sintactico Descendente – p.28/65 Algoritmo para el cálculo de F OLLOW Algoritmo 4 Cálculo del conjunto F OLLOW para todos los símbolos no terminales de la gramática de entrada. Entrada: Una gramática G = (VN , VT , S, P ) de tipo CFG. Salida: Los conjuntos F OLLOW (X) para todo X ∈ VN . Método: Ejecutar el siguiente método para todo X ∈ V N hasta que no se pueda añadir nada más a ningún conjunto FOLLOW. 1. Añadir $ a F OLLOW (S), en donde $ es el delimitador derecho de la entrada. 2. Si existe una producción A → αBβ ∈ P añadir todo F IRST (β) − {λ} a F OLLOW (B). 3. Si existen una producción A → αB ∈ P , ó A → αBβ ∈ P tal que λ ∈ F IRST (β), entonces añadir F OLLOW (A) a F OLLOW (B). ´ ´ Analisis Sintactico Descendente – p.29/65 Ejemplo de construcción de F IRST y F OLLOW Sea la siguiente gramática: E E0 T T0 F → → → → → T E0 +T E 0 |λ FT0 ∗F T 0 |λ (E)|id Los conjuntos F IRST para todos los símbolos terminales de V T = {(, ), +, ∗} son ellos mismos. Para el no terminal F , aplicando el paso 3 introducimos al conjunto F IRST los símbolos ( y id. Para el no terminal T 0 , aplicando el paso 2 introducimos a F IRST λ, y por el paso 3, el símbolo ∗. Para el no terminal T , por el paso tres, con la regla de producción T → F T 0 , añadimos F IRST (F ) a F IRST (T ). Para E 0 , con el paso 2 se añade λ y con el tres se añade +. Para E, F IRST (E) queda con el contenido {(, id} al darse la producción E → T E 0 , aplicando el paso 3. Los conjuntos F IRST quedan como sigue: F IRST (F ) = {(, ID} F IRST (E 0 ) = {+, λ} F IRST (T 0 ) = {∗, λ} F IRST (E) = {(, ID} F IRST (T ) = {(, ID} ´ ´ Analisis Sintactico Descendente – p.30/65 Ejemplo de construcción de F IRST y F OLLOW (II) Pasamos ahora a calcular los conjuntos F OLLOW . Para el símbolo E, el conjunto F OLLOW (E) = {$, )}, añadiendo el $ por el paso 1, y el paréntesis derecho por el paso 3 y la producción F → (E). Al conjunto F OLLOW (E 0 ) añadimos el contenido de F OLLOW (E) por el paso 3, y la producción E → T E 0 . Al conjunto F OLLOW (T ) se añade + por el paso 2 y la producción E → T E 0 . Además, como E 0 → λ ∈ P , añadimos el contenido de F OLLOW (E 0 ). Como tenemos que T → F T 0 ∈ P , añadimos F OLLOW (T ) a F OLLOW (T 0 ). Por el paso 2, y las producciones T → F T 0 y T 0 → ∗F T 0 añadimos el contenido de F IRST (T 0 ) − λ a F OLLOW (F ). Además, como T 0 → λ añadimos F OLLOW (T 0 ). Y obtenemos los conjuntos F OLLOW siguientes: F OLLOW (E) = {$, )} F OLLOW (E 0 ) = {$, )} F OLLOW (T ) = {+, $, )} F OLLOW (T 0 ) = {+, $, )} F OLLOW (F ) = {∗, +, $, )} ´ ´ Analisis Sintactico Descendente – p.31/65 Construcción de la tabla de análisis sintáctico Vamos a construir una tabla de análisis sintáctico que dos diga en todo momento las posibles producciones a aplicar, dado un no-terminal a reducir y un símbolo de la entrada ai . Esta tabla de análisis va a venir definida, algebraicamente, como: M : VN × VT ∪ {$} → 2P ´ ´ Analisis Sintactico Descendente – p.32/65 Construcción de la tabla de análisis sintáctico El contenido de la tabla se produce con el algoritmo que aparece a continuación. Algoritmo 5 Construcción de una tabla de análisis sintáctico predictivo. Entrada: Una gramática G = (VN , VT , S, P ), CFG. Salida: La tabla de análisis sintáctico M . Método: 1. Créese una tabla M|VN |×(|VT |+1) , con una fila para cada no-terminal y una columna para cada terminal más el $. 2. Para cada A → α ∈ P , ejecutar los pasos 3 y 4. 3. Para cada a ∈ F IRST (α), añadir A → α a M [A, a]. 4. Si λ ∈ F IRST (α), añadir A → α a M [A, b], para cada terminal b ∈ F OLLOW (A). Si además, $ ∈ F OLLOW (A), añadir A → α a M [A, $]. 5. Introducir, en cada entrada de M vacía un identificador de error. Si alguna casilla de M contiene más de una producción de P , la gramática no es LL(1). ´ ´ Analisis Sintactico Descendente – p.33/65 Construcción de la tabla de análisis sintáctico Para la gramática anterior la tabla de análisis sintáctico predictivo queda: id E E0 T T0 F + * ( E → T E0 E 0 → +T E 0 T → FT0 $ E → T E0 E0 → λ E → T E0 E0 → λ T0 → λ T0 → λ T → FT0 T0 → λ F → id ) T 0 → ∗F T 0 F → (E) ´ ´ Analisis Sintactico Descendente – p.34/65 Gramáticas no LL(1) Ahora vamos a ver un ejemplo, con una gramática no LL(1) prop expr → if expr then prop | if expr then prop else prop | a|b → p|q Si eliminamos la ambigüedad, como ya habíamos visto en otro tema, la gramática queda: prop → prop1 | prop2 prop1 → if expr then prop1 else prop1 | prop2 → | expr → a|b if expr then prop if expr then prop1 else prop2 p|q ´ ´ Analisis Sintactico Descendente – p.35/65 Gramáticas no LL(1) Si factorizamos la gramática por la izquierda, tenemos prop → prop1 | prop2 prop1 → if expr then prop1 else prop1 | a|b prop2 → if expr then prop2’ prop2’ → prop | expr → prop1 else prop2 p|q ´ ´ Analisis Sintactico Descendente – p.36/65 Tabla de gramática no LL(1) Se obtiene la tabla de análisis siguiente, que como se puede ver, no es LL(1). if p p1 p2 p02 then else p → p1 |p2 p1 → if expr then p1 else p1 p2 → if expr then p02 p02 → p p02 → p1 else p2 expr p p1 p2 p02 expr a b p → p1 p1 → a p → p1 p1 → b p02 → p p02 → p1 else p2 p q expr → p expr → q $ p02 → p p02 → p1 else p2 ´ ´ Analisis Sintactico Descendente – p.37/65 Modificando la gramática Compruébese que modificando el lenguaje, añadiendo delimitadores de bloque (e.g. endif) la gramática producida es LL(1). ´ ´ Analisis Sintactico Descendente – p.38/65 Otras soluciones Una manera ad-hoc de solucionar el problema es adoptando la convención de determinar, de antemano, la producción a elegir de entre las disponibles en una celda determinada de M . Si en el ejemplo de la gramática anterior, factorizamos la gramática original, sin eliminar la ambiguedad tenemos: → |a |b prop’ → | expr → | prop if expr then prop prop’ else prop λ p q ´ ´ Analisis Sintactico Descendente – p.39/65 onstrucción de la tabla de análisis sintáctico (VI Si construímos la tabla de análisis para esta gramática, nos queda: if p p0 else p → if expr then p p0 a b p → a p → b p q p0 → else p p0 → λ p0 → λ expr → p expr $ expr → q En M [p0 , else] hay dos producciones. Si, por convenio, elegimos siempre p0 → else p, escogemos el árbol de derivación que asociaba el else con el if más próximo. En cualquier caso, no existe un criterio general para elegir una sola regla de producción cuando hay varias en una misma casilla. ´ ´ Analisis Sintactico Descendente – p.40/65 Análisis Descendente Predictivo No Recursivo (ADPNR) Para el diseño de un analizador sintáctico, descendente y no recursivo necesitamos una estructura de pila. Vamos a usar la tabla que se ha estudiado anteriormente. La cadena de entrada para el análisis. El modelo de parser de este tipo es el de la figura a + b $ Pila X Analizador Y Sintáctico Z Predictivo $ No Recursivo Salida Tabla M El final del buffer de entrada está delimitado con el signo $, así como el fondo de la pila. La pila podrá albergar tanto símbolos terminales como no-terminales. Estará vacía cuando el elemento que aparezca en la cabeza de la misma sea $. ´ ´ Analisis Sintactico Descendente – p.41/65 nálisis Descendente Predictivo No Recursivo (II Siempre se tiene en cuenta la cabeza de la pila y el siguiente carácter a la entrada. Sea X la cabeza de la pila y a el símbolo de entrada actual. Dependiendo de si X es no-terminal ó terminal tendremos: Si X = a = $ el análisis finaliza con éxito. Si a ∈ VT y X = a, el analizador sintáctico saca X de la pila, y desplaza el apuntador de la entrada un lugar a la derecha. No hay mensaje de salida. Si X ∈ VN , es hora de usar M . Para ello, el control del análisis consulta la entrada M [X, a]. Si M [X, a] = {X → U V W }, por ejemplo, se realiza una operación pop, con lo que sacamos X de la cima, y una operación push(U V W ), estando U en la cima. La salida, tras esa operación, es precisamente la producción utilizada, X → UV W . Si M [X, a] = ∅, el análisis es incorrecto, y la cadena de entrada no pertenece al lenguaje generado por la gramática. La salida es error. Posiblemente se llame a una rutina de recuperación de errores. ´ ´ Analisis Sintactico Descendente – p.42/65 ADPNR → Algoritmo Algoritmo 6 Análisis Sintáctico Predictivo No Recursivo. Entrada: Una tabla de análisis sintáctico M para una gramática G = (V N , VT , S, P ), CFG y una cadena de entrada w. Salida: Si w ∈ L(G), una derivación por la izquierda de w; si no una indicación de error. Método:Sea la configuración inicial de la pila, $S. Sea w$ el buffer de entrada. Hacer que ap(apuntador) apunte al primer símbolo de w$. Repetir Sea X el símbolo a la cabeza de la pila, y a el símbolo apuntado por ap. Si X ∈ VT o X = $ Entonces · Si X = a Entonces extraer X de la pila y avanzar ap. · Si no error(); Si No · Si M [X, a] = X → Y1 Y2 · · · Yk entonces · Begin 1. Extraer X de la pila 2. Meter Yk Yk−1 · · · Y1 en la pila, con Y1 en la cima 3. Emitir a la salida la producción X → Y1 Y2 · · · Yk · End · Si no error() Hasta que (X = $). ´ ´ Analisis Sintactico Descendente – p.43/65 Ejemplos Para hacer un seguimiento de las sucesivas configuraciones que va adquiriendo el algoritmo, se usa una tabla de tres columnas: En la primera se muestra, para cada movimiento el contenido de la pila, en la segunda la entrada que aun queda por analizar, y en la tercera la salida que va emitiendo el algoritmo. ´ ´ Analisis Sintactico Descendente – p.44/65 Ejemplos PILA $E $E 0 T $E 0 T 0 F $E 0 T 0 id $E 0 T 0 $E 0 $E 0 T + $E 0 T $E 0 T 0 F $E 0 T 0 id $E 0 T 0 $E 0 T 0 F ∗ $E 0 T 0 F $E 0 T 0 id $E 0 T 0 $E 0 $ ENTRADA id + id ∗ id$ id + id ∗ id$ id + id ∗ id$ id + id ∗ id$ +id ∗ id$ +id ∗ id$ +id ∗ id$ id ∗ id$ id ∗ id$ id ∗ id$ ∗id$ ∗id$ id$ id$ $ $ $ SALIDA E → T 0E T → FT0 F → id T →λ E 0 → +T E 0 T → FT0 F → id T 0 → ∗F T 0 F → id T0 → λ E0 → λ ´ ´ Analisis Sintactico Descendente – p.45/65 Recuperación de Errores en el análisis descendente predictivo Los errores pueden darse por dos situaciones bien diferentes: Cuando el terminal de la cabeza de la pila no concuerda con el siguiente terminal a la entrada. Cuando se tiene un no-terminal A en la cima de la pila, y un símbolo a a la entrada, y la el contenido de M [A, a] = ∅. ´ ´ Analisis Sintactico Descendente – p.46/65 Recuperación de Errores en el análisis descendente predictivo Recuperación a Nivel de Frase: consiste en introducir apuntadores a rutinas de error en las casillas en blanco de la tabla M → muy complejo. Recuperación en Modo Pánico Los cjtos. de tokens deben ser formados cuidadosamente (eficiencia) Se deberá prestar más atención a aquellos errores que ocurren con más frecuencia en la práctica. Heurísticas Todo lo que viene a continuación equivalía, en teoría, a la parte derecha de un no terminal Estructura de bloque del lenguaje while(a > 0) { if (a=100) printf(‘‘Estamos en la iteracion 100’’); for(int j = 0;j < a;j++) printf(‘‘Iteracion’’); } Los tokens erroneos son añadiduras prescindibles ´ ´ Analisis Sintactico Descendente – p.47/65 Soluciones a las heurísticas Todo lo que viene a continuación equivalía, en teoría, a la parte derecha de un no terminal → Dado el símbolo A ∈ VN , para él los tokens de sincronización podrían ser aquellos pertenecientes a F OLLOW (A) → Estructura de bloque del lenguaje Incluir las palabras claves en el conjunto de sincronización para A. Incluir en los conjuntos de sincronización de no-terminales inferiores, los terminales que inician las construcciones superiores. Los tokens erroneos son añadiduras prescindibles → incluir, en el conjunto de sincronización de los correspondientes A, el contenido de F IRST (A). ´ ´ Analisis Sintactico Descendente – p.48/65 Recuperación de Errores en el análisis descendente predictivo Veámoslo con un ejemplo. Obsérvese la tabla siguiente: id E E0 T T0 F + * E → T E0 0 T → FT0 F → id E → +T E sinc 0 T →λ sinc ( ) $ E → T E0 sinc E →λ sinc 0 T →λ sinc sinc E →λ sinc 0 T →λ sinc 0 0 T → FT0 T 0 → ∗F T 0 sinc F → (E) 0 ´ ´ Analisis Sintactico Descendente – p.49/65 ración de Errores en el análisis descendente pre En ella se han incluido, como tokens de sincronización, aquellos correspondientes a los tokens de F OLLOW del no-terminal en cuestión. Para utilizar la tabla, con esos nuevos elementos, se ha de hacer: 1. Si M [A, a] = ∅, ignoramos el símbolo de entrada y lo saltamos. 2. Si M [A, a] = sinc, se saca el no-terminal de la cima de la pila y se continua el análisis. 3. Si en el caso de comparar la cima de la pila con un componente léxico de la entrada, estos no concuerdan, sacamos el componente léxico de la pila, como en el paso 1. ´ ´ Analisis Sintactico Descendente – p.50/65 ración de Errores en el análisis descendente pre Si lo estudiamos con la entrada )id ∗ +id, vemos la evolución del algoritmo en la siguiente tabla: PILA $E $E $E 0 T $E 0 T 0 F $E 0 T 0 id $E 0 T 0 $E 0 T 0 F ∗ $E 0 T 0 F $E 0 T 0 $E 0 $E 0 T + $E 0 T $E 0 T 0 F $E 0 T 0 id $E 0 T 0 $E 0 $ ENTRADA )id ∗ +id$ id ∗ +id$ id ∗ +id$ id ∗ +id$ id ∗ +id$ ∗ + id$ ∗ + id$ +id$ +id$ +id$ +id$ id$ id$ id$ $ $ $ Comentario error, ignorar ) id ∈ F IRST (E) error, M [F, +] = sinc F se ha extraído de la pila ´ ´ Analisis Sintactico Descendente – p.51/65 Recuperación de Errores en el análisis descendente predictivo En el parsing anterior se observa una primera secuencia de derivaciones más a la izquierda: E ⇒ T E 0 ⇒ F T 0 E 0 ⇒ idT 0 E 0 ⇒ id ∗ F T 0 E 0 A partir de ahí, no podríamos seguir generando la cadena. Si eliminamos F de la cima de la pila podemos continuar con: id ∗ +idT 0 E 0 ⇒ id ∗ +idE 0 ⇒ id ∗ +id Con lo que, al final somos capaces de simular la producción de la cadena errónea. ´ ´ Analisis Sintactico Descendente – p.52/65 Recuperación de Errores en el análisis descendente predictivo Otro ejemplo puede ser el de la entrada (id$, para la misma gramática. La evolución del algoritmo será: PILA $E $E 0 T $E 0 T 0 F $E 0 T 0 )E(F $E 0 T 0 )E $E 0 T 0 )E 0 T $E 0 T 0 )E 0 T 0 F $E 0 T 0 )E 0 T 0 id $E 0 T 0 )E 0 T 0 $E 0 T 0 )E 0 $E 0 T 0 ) $E 0 T 0 $E 0 $ $ ENTRADA (id$ (id$ (id$ (id$ id$ id$ id$ id$ $ $ $ $ $ $ $ Comentario E → T E0 T → FT0 F → (E) E → T E0 T → FT0 F → id T0 → λ E0 → λ Error. Sacamos ’)’ de la pila. T0 → λ E0 → λ ´ ´ Analisis Sintactico Descendente – p.53/65 Recuperación de Errores en el análisis descendente predictivo Se interpreta que se había omitido, por equivocación, el paréntesis derecho. Esto produce la derivación izquierda siguiente: E ⇒ T E 0 ⇒ F T 0 E 0 ⇒ (E)T 0 E 0 ⇒ (T E 0 )T 0 E 0 ⇒ (F T 0 E 0 )T 0 E 0 ⇒ (idT 0 E 0 )T 0 E 0 ⇒ (idE 0 )T 0 E 0 ⇒ (id)T 0 E 0 ⇒ (id)E 0 ⇒ ´ ´ Analisis Sintactico Descendente – p.54/65 Análisis Descendente Predictivo Recursivo Se basa en la ejecución, en forma recursiva, de un conjunto de procedimientos que se encargan de procesar la entrada. Se asocia un procedimiento a cada no-terminal de la gramática, con lo que se tiene que codificar cada uno de ellos según sus características. Los símbolos de los respectivos conjuntos F IRST van a determinar, de forma no ambigua, el siguiente procedimiento que se deberá invocar. Se introducirá este análisis usando la gramática CFG, G = (V N , VT , S, P ) con el siguiente conjunto de producciones en P : tipo simple → simple | ↑ id | array [simple] of tipo → integer | char | num puntopunto num Esta gramática de tipo LL(1), ya que los respectivos conjuntos F IRST (tipo) y F IRST (simple) son disjuntos. ´ ´ Analisis Sintactico Descendente – p.55/65 ADPR. Tipos de Procedimientos. procedure begin empareja(t:simbolo); if preanalisis = t then preanalisis := sigsimbolo else error end; procedure begin tipo; preanalisis is in {integer, char, num} then simple else if preanalisis = ’↑’ then begin if empareja(’↑’); empareja(id) end else if preanalisis = array then begin empareja(array); empareja(’]’); simple; empareja(’]’); empareja(of); tipo end else error end; ´ ´ Analisis Sintactico Descendente – p.56/65 ADPR. Tipos de Procedimientos. procedure begin if simple; preanalisis = integer then empareja(integer) else if preanalisis = char then empareja(char) else if preanalisis = num then begin empareja(num); empareja(puntopunto); empareja(numero); end else error end; ´ ´ Analisis Sintactico Descendente – p.57/65 ADPR. Ejemplo de análisis. Vamos a tener dos procedimientos similares, uno para cada símbolo ∈ V N . Cada uno de los procedimientos, correspondientes a los no terminales tipo y simple. Un procedimiento empareja para simplificar el código de los dos anteriores. Nótese que el análisis sintáctico debe comenzar con una llamada al no-terminal que es símbolo inicial de la gramática, tipo. Ejemplo: array [num puntonum num] of integer; El contenido de preanalisis es, inicialmente, array. Se generan las llamadas empareja(array); empareja(’[’]); simple; empareja(’[’); empareja(of); tipo que precisamente corresponde con la producción tipo → array [simple] of tipo Simplemente, se invoca al procedimiento empareja para cada símbolo terminal, y a los correspondientes simple y tipo para el tamaño y el tipo base del array, respectivamente. ´ ´ Analisis Sintactico Descendente – p.58/65 ADPR. Ejemplo de análisis. El orden de la invocación es importante, al estar realizando un análisis descendente y, por lo tanto, obteniendo una derivación más a la izquierda. El valor del símbolo de anticipación inicial (i.e. array) coincide con el argumento de empareja(array) → se actualiza la variable preanalisis al siguiente carácter a la entrada, que es ’[’. La llamada empareja(’[’) también actualiza la variable preanalisis pasando a ser ahora num. Ahora se invoca a simple, que compara el contenido de esta variable con todos los símbolos terminales que forman su correspondiente conjunto F IRST . Coincide con num y por lo tanto se hace la siguiente serie de invocaciones: empareja(num); empareja(puntopunto); empareja(num) ´ ´ Analisis Sintactico Descendente – p.59/65 ADPR. Ejemplo de análisis (II). Las llamadas anteriores resultan exitosas. Después de su ejecución, el contenido de preanalisis es of, y estamos en la llamada empareja(of). Resulta exitosa y nuevamente se actualiza el contenido de preanálisis a integer. Se llama ahora a tipo que genera su correspondiente llamada simple según dicta el símbolo de preanálisis y el conjunto F IRST (tipo). Finalmente se genera la llamada empareja(integer), y como el siguiente símbolo es $, finaliza con éxito. La secuencia de llamadas puede seguirse con tipo empareja(array) empareja(’[’) simple empareja(num) empareja(puntopunto) empareja(’]’) empareja(num) empareja(of) tipo simple empareja(integer) ´ ´ Analisis Sintactico Descendente – p.60/65 ADPR. Más ejemplos. Otro ejemplo podemos verlo con la gramática siguiente: E → T E0 E0 → +T E 0 |λ T → FT0 T0 → ∗F T 0 |λ F → (E)|id Vamos a escribir los procedimientos necesarios para el análisis recursivo descendente predicitivo, para esta gramática LL(1). Se debe escribir un procedimiento para cada símbolo no-terminal, que se encargue de analizar sus correspondientes partes derechas. En el caso especial de las λ−producciones (i.e. E 0 y T 0 ), si la variable preanalisis no coincide con + ó ∗, respectivamente, se interpreta que el correspondiente símbolo no-terminal se ha reducido a la palabra vacía y se continua el análisis. ´ ´ Analisis Sintactico Descendente – p.61/65 Procedimientos procedure empareja(t:simbolo); begin if (preanalisis = t) then preanalisis := sigsimbolo else error end; procedure No_terminal_E; begin No_terminal_T; No_terminal_E’ end; procedure No_terminal_E’; begin if preanalisis = ’+’ then empareja(’+’); No_terminal_T; No_terminal_E’ else begin end end; ´ ´ Analisis Sintactico Descendente – p.62/65 Procedimientos procedure No_terminal_T; begin No_terminal_F; No_terminal_T’ end; procedure No_terminal_T’; begin if preanalisis = ’*’ then begin empareja(’*’); No_terminal_F; No_terminal_T’ end end procedure No_terminal_F; begin if preanalisis = ’(’ then begin empareja(’(’); No_terminal_E; empareja(’)’) else if preanalisis = id then empareja(’id’); end ´ ´ Analisis Sintactico Descendente – p.63/65 ADPR. Ejemplo de análisis E ( id ) + id F emp(’(’) E’ T E T’ emp(’+’) F E’ emp(’)’) F T T T’ E’ T’ emp(’id’) emp(’id’) ´ ´ Analisis Sintactico Descendente – p.64/65 ADPR. Ejemplo de análisis (IV). ( id + id ) * id E T E’ F emp(’(’) E T F emp(’id’) T’ emp(’)’) emp(’*’) E’ T’ emp(’+’) F T F T’ emp(’id’) E’ T’ emp(’id’) ´ ´ Analisis Sintactico Descendente – p.65/65