Sunteți pe pagina 1din 41

Analizador Sintáctico

Materiales de lectura

• Cap. 3 de Appel
• Cap. 4 de Aho
• Manuales de Yacc – JCUP - Bison

Diseño de Compiladores
Analizador Sintáctico
Analizador Analizador
programa
Lexicograf.
get_token( ) Sintáctico árbol de
parser

Tabla de
Símbolos

• Pide tokens al scanner y construye un árbol de parser.


• Verifica que las líneas del programa sean sintáctica y
semánticamente correctas.

Diseño de Compiladores
Analizador Sintáctico
• IF a < 4 THEN a := a + 3 ELSE a := 10

scanner: ER --> ( token,valor)


IF (id,”a”) “<“ (int,4) THEN (id,”a”) “:=“ (id,”a”) ......

parser: árbol
IF_STMT

BIN_EXPR ASSGN_STMT ASSGN_STMT

(id,“a”) EXPR_ARIT
(id,“a”) “<“ (INT,4)
(id,“a”) + (INT,3)
Diseño de Compiladores
Gramáticas Independientes de Contexto
Describen la sintaxis de los lenguajes.

Consideremos el lenguaje de sentencias (de una línea, sin


condicionales ni iteraciones) y de expresiones.

SàS;S E à id LàE
S à id := E E à num LàL,E
S à print( L ) EàE+E
Eà(S,E)

a:= 7 ; b:= c + (d := 5 +6 , d) pertenece al lenguaje, si se


deriva de aplicar las reglas de producción del lenguaje

Diseño de Compiladores
Derivamos aplicando producciones
S
S;S
S ; id := E;
id := E; id := E;
id := num; id := E;
id := num; id := E+E;
id := num; id := E+(S, E);
id := num; id := id+(S, E);
id := num; id := id+(id := E, E);
id := num; id := id+(id := E+E, E);
id := num; id := id+(id := E+E, id);
id := num; id := id+(id := num+E, id);
id := num; id := id+(id := num+num, id);

a:= 7 ; b:= c + (d := 5 +6 , d)
Diseño de Compiladores
Y se va construyendo el árbol de
S parser

S ; S

id := E id := E

num ( S , E )

id := E id

E + E
num num
Diseño de Compiladores
Gramática ambigua
cuando de una misma tira se construyen dos árboles de
S parser diferentes, por ej: id + id + id
S

id := E
id := E

E + E
E + E

E + E id
id E + E

id id id id
¿Qué pasa si la tira derivada fuese id := id – id – id
o id := id + id * id ?
Diseño de Compiladores
Gramática no ambigua
Una gramática ambigua:

E à id EàE/E Eà(E)
E à num EàE+E
EàE*E EàE-E

en general puede ser reescrita de forma no ambigua:

EàE+T TàT *F F à id
EàE -T TàT /F F à num
EàT TàF Fà(E)

Diseño de Compiladores
Gramática no ambigua
E • Ahora existe un único
árbol de parser asociado
a la tira id + id * id
E + T

T T * F

F F id

id id

Diseño de Compiladores
Parsers
• Top-Down - LL

• Construye el árbol de parser top-down


• LL (left scan, left derivation)

• Fáciles de programar a mano

• Bottom-up - LR

• Construye el árbol de parser bottom-up


• LR,LALR,SLR (left scan, right derivation)

• Difícil de programar, herramientas automáticas, YACC

Diseño de Compiladores
Parsing Top-Down
• Construye el árbol de parser comenzando por la raíz y
“adivinando” el próximo paso de derivación

S --> cAd
A --> ab | a

Para derivar cad de la gramática

S ==> cAd ==> cabd à No llego por este camino

• Parsers con backtracking ( muy lentos, no los veremos )


• Parsers predictivos, predicen usando símbolos de lookahead
( a veces es necesario reescribir la gramática )

Diseño de Compiladores
Parsers Predictivo: Recursivo
Descendente
Dada la gramática:

S à if E then S else S
| begin S L
| print E

L à end
| ;SL

E à num = num

Se escriben usando:
• Una función por cada no terminal.
• Una cláusula por cada producción del no terminal
Diseño de Compiladores
Parsers Recursivo Descendente
void S( ) { switch( tok )
case IF: eat (IF) ; E ( ); eat (THEN); S( ) ;
eat (ELSE); S( ) ; break ;
case BEGIN: eat (BEGIN) ; S( ) ; L( ) ; break ;
case PRINT: eat (PRINT) ; E( ) ; break ;
default : error ( ) ; }
void L( ) { switch(tok)
case END: eat (END); break;
case SEMI: eat(SEMI); S( ) ; L( ) ; break;
default: error( ); }
void E( ) { eat ( NUM) ; eat(EQ) ; eat(NUM) ; }

eat ( t ): compara t con el símbolo de input leído en tok


• si es igual lee el próximo símbolo y retorna
• si son distintos da error.
Diseño de Compiladores
Parsers Recursivo Descendente
Pero que pasa si la gramática es la de las expresiones
aritméticas vista ?

SàE$
EàE+T TàT *F F à id
EàE -T TàT /F F à num
EàT TàF Fà(E)

Diseño de Compiladores
Parsers Recursivo Descendente
void S( ) { E ( ) ; eat(EOF); }

void E( ) { switch (tok)


case ?: E ( ); eat (MAS) ; T ( ); break ;
case ?: E ( ); eat (MENOS) ; T ( ); break ;
case ?: T ( ); break ;
default : error ( ) ; }

void T( ) { switch(tok)
case ?: T ( ); eat (MULT) ; F ( ); break ;
case ?: T ( ); eat (DIV) ; F ( ); break ;
case ?: F( ); break ;
default: error( ); }

Diseño de Compiladores
Parsers Recursivo Descendente
La función E no puede determinar que cláusula usar.

Ejemplo:

( 1 * 2 –3 ) + 4 usaría EàE+T

pero

( 1 * 2 –3 ) usaría E à T

Los parsers recursivos descendentes funcionan sólo en


gramáticas donde el primer símbolo terminal debe
permitir decidir qué produción usar.

Diseño de Compiladores
CONJUNTO FIRST
Sea α string de terminales y no terminales

• FIRST(α ): conj. de terminales que comienzan


strings derivados desde α.

• Por ej: FIRST ( T * F ) = { id, num, ( }

• Si X à α 1 | α 2 si k ∈ FIRST(α 1 )
k ∈ FIRST(α 2 )
la gramática no puede ser parseada por un
parser recursivo descendente

Diseño de Compiladores
CONJUNTO FIRST
• Sea la gramática:

Zàd Yàε XàY


ZàXYZ Yàc X àa

• Parecería que FIRST (X Y Z) dependiera sólo de FIRST( X )


• Pero que pasa si X , Y pueden ser nulos,como aquí ?

• FIRST ( XYZ ) debe incluir FIRST ( Z ) pues X e Y son


símbolos anulables

Diseño de Compiladores
Cálculo de FIRST

• Si X es un terminal : - FIRST (X) = { X }

• Si X à Y1 Y2 ....... YK : - a está en FIRST (X) si para


* ε.
algún i, a ∈ FIRST(Yi) y Y1 ...... Yi-1 à

Es decir todo lo que está en FIRST(Y1 ), está en FIRST(X).


Pero si además Y1 à ε , entonces agregamos FIRST(Y2).....

Diseño de Compiladores
CONJUNTOS NULLABLE y
FOLLOW

• NULLABLE (X) : true si X deriva el string vacío

• FOLLOW (X) : conj. de terminales que siguen


inmediatamente a X.
t e FOLLOW( X ) si hay una derivación que contiene Xt

Puede ser que XYZ t donde Y y Z deriven ε.

Diseño de Compiladores
Cálculo de FOLLOW

• Si X->α Y β

todo lo que está en FIRST(β ) (excepto ε) se pone en


FOLLOW(Y)

• Si X->α Y ο
X->α Y β y FIRST(β ) contiene ε

todo lo que está en FOLLOW(X) está en FOLLOW(Y)

Diseño de Compiladores
Algoritmo FIRST – FOLLOW - NULLABLE
For each símbolo terminal Z, FIRST( Z ) = { Z }
For each producción X à Y1 Y2 ....... YK
For each i from 1 to k, each j from i+1 to k
if todos los Yi son anulables
then nullable[ X ] = true
if Y1 ...... Yi-1 son anulables
then FIRST[ X ] = FIRST[ X ] U FIRST[Yi ]
if Yi+1...... YJ-1son anulables
then FOLLOW[Yi] = FOLLOW[Yi ] U FIRST[YJ ]
if Yi+1...... Yk son anulables
then FOLLOW[Yi]=FOLLOW[Yi ] U FOLLOW[X ]

Diseño de Compiladores
Ejemplo: cálculo NULLABLE - FIRST
1. Z à d 3. Y à ε 5. X à Y
2. Z à X Y Z 4. Y à c 6. X à a

De 3. Y es anulable, y entonces X es anulable


De 1. FIRST (Z) ß d De 4. FIRST (Y) ß c De 6. FIRST (X) ß a
De 5. FIRST ( Y ) à FIRST ( X )
De 2. FIRST ( X ) à FIRST ( Z )

Nullable FIRST FOLLOW


X Si ac
Y Si c
Z No acd
Diseño de Compiladores
Ejemplo: cálculo FOLLOW
1. Z à d 3. Y à ε 5. X à Y
2. Z à X Y Z 4. Y à c 6. X à a

De 2. FIRST ( Z ) à FOLLOW ( Y )
De 2. FIRST ( Y ) à FOLLOW ( X )
De 2. FIRST ( Z ) à FOLLOW ( X )
De 5. FOLLOW (X) à FOLLOW (Y)

Nullable FIRST FOLLOW


X Si ac acd
Y Si c acd
Z No acd

Diseño de Compiladores
Construcción de tabla de parsing
predictivo para el ejemplo
Entrar la producción Xà α, en fila X, columna t, para cada
t ∈ FIRST (α). Si α es anulable, entrar la producción en fila X,
columna t, para cada t ∈ FOLLOW (X).

Nullable FIRST FOLLOW a c d


X Si ac acd X Xàa XàY XàY
Y Si c acd XàY
Z No acd Y Yàε Yàε Yàε
Yàc
Z ZàXYZ ZàXYZ Zàd
ZàXYZ

Diseño de Compiladores
Tabla de parsing predictivo
Existen entradas duplicadas en la tabla à gramática no sirve
para parsing recursivo descendente, no alcanza con conocer el
no terminal y el símbolo de lookahead, para elegir la producción
a utilizar.

En efecto, hay tiras que tiene más de un árbol de parser:


por ej. d

Zàd y Z à XYZ à d

La gramática es entonces ambigua.

Diseño de Compiladores
Tabla de parsing predictivo LL1 - LLk
Gramáticas ambiguas è entradas duplicadas en la
tabla de parsing predictivos

LL(1) : à Gramáticas con tablas de parser predictivo con


entradas no duplicadas y un símbolo de lookahead.
LL(k) : à Gramáticas con tablas de parser predictivo con entradas
no duplicadas y k símbolos de lookahead. Las columnas de la
tabla tienen secuencia de k terminales.

Diseño de Compiladores
Eliminación de la recursividad
por la izquierda
• Gramáticas con recursividad por la izquierda no pueden ser LL1
EàE+T TàT *F F à id
EàT TàF F à(E)

Ej: FIRST(T) = FIRST(E+T) à entradas duplicadas en tabla.

• La eliminamos usando recursión por la derecha, e introduciendo


un no terminal nuevo.

E à T E’ T à F T’ F à id
E’ à + T E’ T’ à * F T’ F à(E)
E’ à ε T’ à ε
Diseño de Compiladores
Eliminación de la recursividad
por la izquierda
• Gramáticas recursivas en más de un paso:
1. S à A a | b
2. A à A c | S d | ε

SàAaàS daàAadaàSdada

• Si no hay reglas del tipo A * A o A à ε existe un algortimo para construir


una gramática sin recursión por la izquierda (Ullman p.176-177).

• En prod. 2. se sustituye las producciones con S por S à A a

A à Ac | Aad | bd | ε y obtenemos recursividad en un paso.

Diseño de Compiladores
Factorización por la izquierda
• Gramáticas con una producción que comienza con el mismo
terminal por la izquierda no pueden ser LL1, no sabemos que
producción elegir con un símbolo de lookahead.

• La eliminamos factorizando por la izquierda, posponiendo la


decisión para cuando hayamos leído más símbolos de input.

• Ejemplo:
S à if E then S else S
S à if E then S

se transforma en:

S à if E then S X
Xà else S | ε
Diseño de Compiladores
Parsing Predictivos

• Gramáticas deben ser: - sin recursividad por la izquierda y


- factorizada por la izquierda

• Parsers:
- Recursivos descendentes ( con procesos recursivos ),
anteriormente vistos.

- Predictivos no recursivos ( usando stack y tablas de


parsing ), a continuación.

Diseño de Compiladores
Parsing Predictivos no recursivos
• Usamos stack en vez de llamadas a procesos recursivos
Algoritmo:
Si X es un terminal
Si X=$, fin exitoso
a + b $ input Si X=a pop(X) y avanza input
Sino error
X Sino /* X no es un terminal*/
Si M[X,a]= X-->UVW
Y /*Reemplazar X por UVW*/
prog.
Z parsing pop()
output push(W) push(V) push(U)
$ predictivo Sino /* M[X,a] vacía*/
Error

tabla de
parsing M[X,a]

Diseño de Compiladores
Parsing Predictivos no recursivos

• Se parte del stack, inicializado con símbolo inicial S.

• Si símbolo tope del stack, coincide con input, se consume


• Se sustituyen el no terminal del tope del stack, por el lado
derecho de la producción, haciendo el push de los símbolos en
orden inverso. (De esta manera se corresponde con una derivación de
más a la izquierda).

• Si la tira es válida, el stack queda con el token $

Diseño de Compiladores
Parsing Predictivos no recursivos
Ejemplo expresiones aritméticas
1. S à E $
2. E --> T E’ 4. T --> F T’ 6. F --> ( E ) | id
3. E’--> + T E’ | ε 5. T’ --> * F T’ | ε

De 3 y 5. E’ y T’ son anulables.
De 6. FIRST (F) ßid ( De 5. FIRST (T’) ß * De 3. FIRST (E’) ß +
nullable FIRST FOLLOW
De 4. FIRST(F) à FIRST(T) S - ( id
De 2. FIRST(T) à FIRST(E) E - ( id
De 1. FIRST(E) à FIRST(S) E’ Si +
T - ( id
T’ Si *
F - ( id
Diseño de Compiladores
Parsing Predictivos no recursivos
Ejemplo expresiones aritméticas

1. S à E $
2. E --> T E’ 4. T --> F T’ 6. F --> ( E ) | id
3. E’--> + T E’ | ε 5. T’ --> * F T’ | ε

De 1 y 6. FOLLOW(E)= { ) $} nullable FIRST FOLLOW


S - ( id
De 4.FIRST(T’) está en FOLLOW(F) E - ( id ) $
De 2.FIRST(E’) está en FOLLOW(T) E’ Si +
T - ( id +
T’ Si *
F - ( id *

Diseño de Compiladores
Parsing Predictivos no recursivos
Ejemplo expresiones aritméticas
1. S à E $
2. E --> T E’ 4. T --> F T’ 6. F --> ( E ) | id
3. E’--> + T E’ | ε 5. T’ --> * F T’ | ε

De 2.Todo lo que está en FOLLOW(E) está en FOLLOW(E’) y en FOLLOW(T)


De 4.Todo lo que está en FOLLOW(T) está en FOLLOW(T’) y en FOLLOW(F)

nullable FIRST FOLLOW


S - ( id
E - ( id ) $
E’ Si + ) $
T - ( id +) $
T’ Si * +) $
F - ( id *+) $
Diseño de Compiladores
Tabla de Parsing Expr. Aritméticas
id + * ( ) $
S SàE$ SàE$
E EàTE’ EàTE’
E’ E’à+TE’ E’àε E’àε
T TàFT’ TàFT’
T’ T’àε T’à*FT’ T’àε T’àε
F Fàid Fà (E)

nullable FIRST FOLLOW


1. S à E $
S - ( id
2. E --> T E’
E - ( id ) $
3. E’--> + T E’ | ε
E’ Si + ) $
4. T --> F T’
T - ( id +) $
5. T’ --> * F T’ | ε
T’ Si * +) $
6. F --> ( E ) | id
F - ( id *+) $
Diseño de Compiladores
STACK INPUT
0. S id+id*id$ aplico S->E$
1. $E id+id*id$ aplico E->TE’
2. $E’T id+id*id$ aplico T->FT’
3. $E’T’F id+id*id$ aplico F->id
4. $E’T’id id+id*id$ consumo
5. $E’T’ +id*id$ aplico T’-->ε y E’->+TE’
6. $E’T+ +id*id$ consumo
8. $E’T id*id$ aplico T->FT’
9. $E’T’F id*id$ aplico F->id
10. $E’T’id id*id$ consumo
11. $E’T’ *id$ aplico T’->*FT’
12. $E’T’F* *id$ consumo
13. $E’T’F id$ aplico F->id
14. $E’T’id id$ consumo
15. $E’T’ $ aplico T’-->ε
16. $E’ $ E’-> ε
17. $ Diseño de Compiladores
Recuperación de errores
• Lenguajes: no describen como el compilador debe responder a
errores
• Planificar manejo de errores desde el principio:
- simplifica la estructura del compilador
- mejora la respuesta frente a los errores
• Errores pueden suceder en distintas fases:
- léxico: mal un identificador,palabra clave,operador
- sintáctico: expresión aritmética con paréntesis desbalanceados
- semántico: operador aplicado a un operando
incompatible
- lógico: call recursivo infinito
pero la mayoría de los errores se detectan en el análisis sint.

Diseño de Compiladores
Recuperación de errores en parser
predictivos
• Un blanco en una fila T,columna x de una tabla de parsing LL1,
indica que la función T() no espera un x.
• Si viene una x à se produce un error.

• Políticas de recuperación:
• Reportar error y abandonar: no es amigable
• Insertar, borrar o reemplazar tokens (modo pánico)

• Insertar es peligroso por los errores en cascada


• Borrar: saltea tokens hasta que un token del conjunto
FOLLOW es encontrado

Diseño de Compiladores
Recuperación de errores en parser
predictivos
• Consideremos la gramática de las expresiones aritméticas, y la
producción : T’ à * F T ‘ | ε.
• FOLLOW(T’) = { ) + $ }

• El código con recuperación sería:

int Tprimo_follow [ ] = { MAS, PARDER, EOF }


void Tprimo( ) { switch (tok)
case POR: eat(POR); F ( ); Tprimo( ); break;
case MAS: break;
case PARDER: break;
case EOF: break;
default: printf(“se esperaba *,+, ) o eof”);
skipto(Tprimo_follow);}
Diseño de Compiladores

S-ar putea să vă placă și