Analizador SintácticoMateriales de lectura • Cap. 3 de Appel • Cap. 4 de Aho • Manuales de Yacc – JCUP - Bison Diseño de Compiladores Analizador Sintáctico programa Analizador Lexicograf. Analizador 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 (id,“a”) “<“ (INT,4) (id,“a”) ASSGN_STMT (id,“a”) ASSGN_STMT EXPR_ARIT + (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 S à id := E S à print( L ) E à id E à num EàE+E Eà(S,E) LàE LàL,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 id := id+(id := num+num. id := E. id := num. id).S S . id := E+(S. id := num. id := num. id := id+(id := num+E. id := num. id := num. id := num. E). a:= 7 . id). d) Diseño de Compiladores . id := num. id := id+(id := E. id := id+(id := E+E. id). id := E. id := id+(S. E).Derivamos aplicando producciones S S. id := E. id := id+(id := E+E. E). id := num. id := num. id := E. id := E+E. E). b:= c + (d := 5 +6 . S id := E num id ( id := S := E E . E E id + E num Diseño de Compiladores ) num .Y se va construyendo el árbol de S parser S . por ej: id + id + id S S id := E id E + E E E + E id id id E + E id + E := E id id ¿Qué pasa si la tira derivada fuese id := id – id – id o id := id + id * id ? Diseño de Compiladores .Gramática ambigua cuando de una misma tira se construyen dos árboles de parser diferentes. Gramática no ambigua Una gramática ambigua: E à id E à num EàE*E EàE/E EàE+E EàE-E Eà(E) en general puede ser reescrita de forma no ambigua: EàE+T EàE -T EàT TàT *F TàT /F TàF F à id F à num 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 T E + T T * F F id F id id Diseño de Compiladores . LR • Construye el árbol de parser bottom-up • LR. YACC Diseño de Compiladores .LALR.SLR (left scan.Parsers • Top-Down . herramientas automáticas.LL • Construye el árbol de parser top-down • LL (left scan. left derivation) • Fáciles de programar a mano • Bottom-up . right derivation) • Difícil de programar. no los veremos ) • Parsers predictivos. predicen usando símbolos de lookahead ( a veces es necesario reescribir la gramática ) 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. • Una cláusula por cada producción del no terminal Diseño de Compiladores .SL E à num = num Se escriben usando: • Una función por cada no terminal.Parsers Predictivo: Recursivo Descendente Dada la gramática: S à if E then S else S | begin S L | print E L à end | . break. } void L( ) { switch(tok) case END: eat (END). eat(EQ) . case PRINT: eat (PRINT) . E ( ). E( ) . S( ) . S( ) . break . } 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. L( ) . } void E( ) { eat ( NUM) . default: error( ). case SEMI: eat(SEMI). eat (ELSE). default : error ( ) . break . eat (THEN).Parsers Recursivo Descendente void S( ) { switch( tok ) case IF: eat (IF) . eat(NUM) . break. break . L( ) . case BEGIN: eat (BEGIN) . Diseño de Compiladores . S( ) . S( ) . Parsers Recursivo Descendente Pero que pasa si la gramática es la de las expresiones aritméticas vista ? SàE$ EàE+T EàE -T EàT TàT *F TàT /F TàF F à id F à num Fà(E) Diseño de Compiladores . Parsers Recursivo Descendente void S( ) { E ( ) . T ( ). break . break . default: eat (MULT) . break . default : error ( ) . F ( ). case ?: F( ). case ?: T ( ). F ( ). case ?: T ( ). } Diseño de Compiladores . } void T( ) { switch(tok) case ?: T ( ). T ( ). eat(EOF). } void E( ) { switch (tok) case ?: E ( ). eat (MENOS) . break . break . break . eat (DIV) . case ?: E ( ). error( ). eat (MAS) . Parsers Recursivo Descendente La función E no puede determinar que cláusula usar. Ejemplo: ( 1 * 2 –3 ) + 4 pero ( 1 * 2 –3 ) usaría E à T usaría Eà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 . 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 α string de terminales y no terminales • FIRST(α ): conj. CONJUNTO FIRST • Sea la gramática: Zàd ZàXYZ Yàε Yàc XàY X àa • Parecería que FIRST (X Y Z) dependiera sólo de FIRST( X ) • Pero que pasa si X .como aquí ? • FIRST ( XYZ ) debe incluir FIRST ( Z ) pues X e Y son símbolos anulables Diseño de Compiladores . Y pueden ser nulos. .. Es decir todo lo que está en FIRST(Y1 ). entonces agregamos FIRST(Y2). a ∈ FIRST(Yi) y Y1 . YK : .FIRST (X) = { X } • Si X à Y1 Y2 . Diseño de Compiladores .. está en FIRST(X)...........Cálculo de FIRST • Si X es un terminal : ..a está en FIRST (X) si para * algún i. Pero si además Y1 à ε .. Yi-1 à ε . CONJUNTOS NULLABLE y FOLLOW • NULLABLE (X) : true si X deriva el string vacío • FOLLOW (X) : conj. t e FOLLOW( X ) si hay una derivación que contiene Xt Puede ser que XYZ t donde Y y Z deriven ε. de terminales que siguen inmediatamente a X. Diseño de Compiladores . Cálculo de • Si X->α Y β FOLLOW 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 . FIRST( Z ) = { Z } For each producción X à Y1 Y2 ... YJ-1son anulables then FOLLOW[Yi] = FOLLOW[Yi ] U FIRST[YJ ] if Yi+1. Yi-1 son anulables then FIRST[ X ] = FIRST[ X ] U FIRST[Yi ] if Yi+1..NULLABLE For each símbolo terminal Z.. YK For each i from 1 to k... Yk son anulables then FOLLOW[Yi]=FOLLOW[Yi ] U FOLLOW[X ] Diseño de Compiladores ...... each j from i+1 to k if todos los Yi son anulables then nullable[ X ] = true if Y1 ........ FIRST (Y) ß c De 6. Y es anulable.Ejemplo: cálculo NULLABLE . Y à ε 4. FIRST ( Y ) à FIRST ( X ) De 2. y entonces X es anulable De 1. X à a De 3. Y à c 5. FIRST ( X ) à FIRST ( Z ) Nullable X Si Y Si Z No FIRST ac c acd FOLLOW Diseño de Compiladores .FIRST 1. X à Y 6. Z à d 2. FIRST (X) ß a De 5. FIRST (Z) ß d De 4. Z à X Y Z 3. Ejemplo: cálculo FOLLOW 1. Y à ε 4. FIRST ( Z ) à FOLLOW ( Y ) De 2. Z à X Y Z 3. FIRST ( Y ) à FOLLOW ( X ) De 2. Y à c 5. X à Y 6. FIRST ( Z ) à FOLLOW ( X ) De 5. FOLLOW (X) à FOLLOW (Y) Nullable X Si Y Si Z No FIRST ac c acd FOLLOW acd acd Diseño de Compiladores . Z à d 2. X à a De 2. columna t. Nullable X Si Y Si Z No FIRST ac c acd FOLLOW acd acd a Xàa XàY Yàε ZàXYZ c XàY d XàY X Y Z Yàε Yàε Yàc ZàXYZ Zàd ZàXYZ Diseño de Compiladores . para cada t ∈ FOLLOW (X). en fila X. para cada t ∈ FIRST (α). columna t. Si α es anulable. entrar la producción en fila X.Construcción de tabla de parsing predictivo para el ejemplo Entrar la producción Xà α. En efecto.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. Diseño de Compiladores . d Zàd y Z à XYZ à d La gramática es entonces ambigua. para elegir la producción a utilizar. hay tiras que tiene más de un árbol de parser: por ej. Las columnas de la tabla tienen secuencia de k terminales. Diseño de Compiladores .Tabla de parsing predictivo LL1 . LL(k) : à Gramáticas con tablas de parser predictivo con entradas no duplicadas y k símbolos de lookahead.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. e introduciendo un no terminal nuevo. • La eliminamos usando recursión por la derecha.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. E à T E’ E’ à + T E’ E’ à ε T à F T’ T’ à * F T’ T’ à ε Diseño de Compiladores F à id F à(E) . • En prod. 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. Diseño de Compiladores . S à A a | b 2.176-177). se sustituye las producciones con S por S à A a A à Ac | Aad | bd | ε y obtenemos recursividad en un paso.Eliminación de la recursividad por la izquierda • Gramáticas recursivas en más de un paso: 1. • La eliminamos factorizando por la izquierda. no sabemos que producción elegir con un símbolo de lookahead.Factorización por la izquierda • Gramáticas con una producción que comienza con el mismo terminal por la izquierda no pueden ser LL1. 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 . a continuación. Diseño de Compiladores .sin recursividad por la izquierda y . .factorizada por la izquierda • Parsers: .Predictivos no recursivos ( usando stack y tablas de parsing ).Parsing Predictivos • Gramáticas deben ser: .Recursivos descendentes ( con procesos recursivos ). anteriormente vistos. a] 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=$.a] vacía*/ Error a + b $ X Y Z $ input prog.a]= X-->UVW /*Reemplazar X por UVW*/ pop() push(W) push(V) push(U) Sino /* M[X. fin exitoso Si X=a pop(X) y avanza input Sino error Sino /* X no es un terminal*/ Si M[X. parsing predictivo output tabla de parsing M[X. Parsing Predictivos no recursivos • • • Se parte del stack. por el lado derecho de la producción. haciendo el push de los símbolos en orden inverso. inicializado con símbolo inicial S. (De esta manera se corresponde con una derivación de más a la izquierda). se consume Se sustituyen el no terminal del tope del stack. coincide con input. Si símbolo tope del stack. • Si la tira es válida. el stack queda con el token $ Diseño de Compiladores . FIRST (T’) ß * De 3.Parsing Predictivos no recursivos Ejemplo expresiones aritméticas 1. E --> T E’ 4. FIRST (F) ßid ( De 5. T --> F T’ 3. E’ y T’ son anulables. E’--> + T E’ | ε 5. S à E $ 2. T’ --> * F T’ | ε De 3 y 5. FIRST(F) à FIRST(T) De 2. F --> ( E ) | id FOLLOW De 4. FIRST(T) à FIRST(E) De 1. FIRST (E’) ß + nullable FIRST ( id ( id + ( id * ( id Diseño de Compiladores 6. FIRST(E) à FIRST(S) S E E’ T T’ F Si Si - . De 6. FIRST(E’) está en FOLLOW(T) 6. T --> F T’ 3. E’--> + T E’ | ε 5. E --> T E’ 4.FIRST(T’) está en FOLLOW(F) De 2.Parsing Predictivos no recursivos Ejemplo expresiones aritméticas 1. T’ --> * F T’ | ε De 1 y 6. FOLLOW(E)= { ) $} De 4. S à E $ 2. F --> ( E ) | id nullable S E E’ Si T T’ Si F - FIRST ( id ( id + ( id * ( id FOLLOW ) $ + * Diseño de Compiladores . Parsing Predictivos no recursivos Ejemplo expresiones aritméticas 1.Todo lo que está en FOLLOW(T) está en FOLLOW(T’) y en FOLLOW(F) nullable S E E’ Si T T’ Si F FIRST ( id ( id + ( id * ( id FOLLOW ) $ ) $ +) $ +) $ *+) $ Diseño de Compiladores . T’ --> * F T’ | ε 6. E --> T E’ 4. S à E $ 2. F --> ( E ) | id De 2.Todo lo que está en FOLLOW(E) está en FOLLOW(E’) y en FOLLOW(T) De 4. E’--> + T E’ | ε 5. T --> F T’ 3. E’--> + T E’ | ε 4. T --> F T’ 5. T’ --> * F T’ | ε 6. E --> T E’ 3.Tabla de Parsing Expr. S à E $ 2. Aritméticas S E E’ T T’ F id + * SàE$ EàTE’ E’à+TE’ TàFT’ T’à*FT’ T’àε Fàid ( ) SàE$ EàTE’ E’àε TàFT’ T’àε Fà (E) FIRST ( id ( id + ( id * ( id $ E’àε T’àε 1. F --> ( E ) | id nullable S E E’ Si T T’ Si F - FOLLOW ) $ ) $ +) $ +) $ *+) $ Diseño de Compiladores . $E’T’id 15. $E’T’F* 13. $E’T’id 5. $E’T’ 12. $E’T 9. $E’ 17. $E’T+ 8. $E’T’F 4. $E’T’id 11. $E’T’F 10. $E 2.STACK 0. $E’T’ 16. $ INPUT 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$ id$ $ $ aplico S->E$ aplico E->TE’ aplico T->FT’ aplico F->id consumo aplico T’-->ε y E’->+TE’ consumo aplico T->FT’ aplico F->id consumo aplico T’->*FT’ consumo aplico F->id consumo aplico T’-->ε E’-> ε Diseño de Compiladores . $E’T 3. $E’T’F 14. $E’T’ 6. S 1. Recuperación de errores • Lenguajes: no describen como el compilador debe responder a errores • Planificar manejo de errores desde el principio: .lógico: call recursivo infinito pero la mayoría de los errores se detectan en el análisis sint. Diseño de Compiladores .sintáctico: expresión aritmética con paréntesis desbalanceados .mejora la respuesta frente a los errores • Errores pueden suceder en distintas fases: .semántico: operador aplicado a un operando incompatible .léxico: mal un identificador.simplifica la estructura del compilador .operador .palabra clave. indica que la función T() no espera un x. 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 • Un blanco en una fila T. • Si viene una x à se produce un error. • Políticas de recuperación: • Reportar error y abandonar: no es amigable • Insertar.columna x de una tabla de parsing LL1. skipto(Tprimo_follow). EOF } void Tprimo( ) { switch (tok) case POR: eat(POR). case MAS: break. Tprimo( ). case EOF: break. default: printf(“se esperaba *. PARDER.Recuperación de errores en parser predictivos • Consideremos la gramática de las expresiones aritméticas. ) o eof”). break.+. • FOLLOW(T’) = { ) + $ } • El código con recuperación sería: int Tprimo_follow [ ] = { MAS. y la producción : T’ à * F T ‘ | ε. F ( ).} Diseño de Compiladores . case PARDER: break.