Análisis Sintáctico Descendente

Anuncio
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
Descargar