Procesamiento de Lenguajes (PL) Curso 2014/2015 Gramáticas Conceptos básicos sobre gramáticas Gramáticas y lenguajes Dado un alfabeto Σ, un lenguaje es un conjunto (finito o infinito) de cadenas de sı́mbolos pertenecientes al alfabeto. Es posible que la cadena vacı́a, , pertenezca a un lenguaje Una gramática es una forma compacta de representar un lenguaje IMPORTANTE: Una gramática genera un único lenguaje, pero un mismo lenguaje puede ser generado por varias gramáticas Una gramática es una cuádrupla G = (VN , VT , S, P ), donde VN es el conjunto de sı́mbolos variables o no terminales, VT es el conjunto de sı́mbolos terminales (todos los terminales deben pertenecer al alfabeto), S es el sı́mbolo inicial de la gramática (S siempre es un no terminal), y P es el conjunto de producciones o reglas de la gramática Producciones y derivaciones Una producción o regla de una gramática tiene una parte izquierda y una parte derecha. Tanto la parte izquierda como la parte derecha son una cadena de sı́mbolos terminales y no terminales Normalmente, solamente se especifica el conjunto de producciones P , y se asume que el sı́mbolo inicial de la gramática es la parte izquierda de la primera producción. Ejemplo: → → → → → A B B C C a B C b bas big C boss c G = (VN = {A, B, C }, VT = {a, b, bas, big, boss, c}, S = A, P = {A −→ . . .}) Una derivación es una secuencia de cadenas de sı́mbolos (llamadas formas sentenciales) en la que cada cadena es resultado de la aplicación de una regla de la gramática a la cadena anterior. Una derivación válida es aquella en la que la primera cadena de la secuencia es el sı́mbolo inicial, y la última es una cadena de terminales. Ejemplos: A ⇒ ⇒ ⇒ ⇒ a a a a A ⇒ ⇒ ⇒ a B C a b bas C a b bas B C B c big C boss c big boss c En los ejemplos, la primera derivación se dice que es una derivación por la derecha, porque siempre se deriva el no terminal situado más a la derecha, y la segunda es una derivación por la izquierda IMPORTANTE: El lenguaje generado por una gramática es el conjunto de cadenas de terminales obtenidas a partir de derivaciones válidas usando las reglas de la gramática Un árbol de derivación es un árbol en el que se representa una derivación válida de una cadena (pero no se especifica el orden en que se han aplicado las reglas) PL, 2013/2014 2 A a B b C bas ε Caracterı́sticas especiales Se dice que una gramática es ambigua cuando, para una cadena determinada, existe más de un árbol de derivación IMPORTANTE: la única forma de saber si una gramática es ambigua es encontrando una cadena con más de un árbol de derivación, no hay otra forma Ejemplo: E E → → E opsuma E num La cadena “2+3-4” tiene dos árboles de derivación, y la cadena “2+3-4+5” tiene más de dos árboles (¿cuántos?) Una gramática se dice que es recursiva por la izquierda si tiene al menos una regla de esta forma: E → E opsuma T De forma similar, una gramática puede presentar recursividad por la derecha: E → T opsuma E Una gramática se dice que tiene factores comunes por la izquierda si tiene en al menos dos reglas (con la misma parte izquierda) sı́mbolos comunes al principio de la parte derecha de la regla: A A ... → → B a C B a d Jerarquı́a de gramáticas Según la forma de las producciones, las gramáticas se clasifican en: Regulares: en la parte izquierda sólo hay un no terminal, y la parte derecha puede haber: no terminal → terminal no terminal → terminal no terminal no terminal → Independientes del contexto (context-free): en la parte izquierda sólo hay un no terminal, en la parte derecha no hay restricciones Dependientes del contexto: en la parte izquierda puede haber terminales y no terminales, pero al menos debe haber un no terminal, y la longitud de la parte derecha debe ser mayor o igual que la de la parte izquierda No restringidas PL, 2013/2014 3 ¿Qué tipos de gramáticas se usan en los compiladores? En los compiladores se utilizan solamente gramáticas regulares y gramáticas independientes del contexto (GIC) Las gramáticas regulares se utilizan para especificar los tokens (en realidad, se utilizan expresiones regulares, pero son equivalentes) Las GIC se utilizan para especificar la sintaxis de las construcciones del lenguaje fuente En los lenguajes de programación hay restricciones semánticas (p.ej. es necesario haber declarado una variable antes de utilizarla), que hacen que en realidad los lenguajes de programación sean lenguajes sensibles al contexto, pero no se utilizan gramáticas sensibles al contexto, se utilizan GIC a las que se añaden acciones para la comprobación de las restricciones semánticas. Diseño de gramáticas para expresiones en lenguajes de programación Un buen diseño de la gramática nos permitirá reflejar de forma natural caracterı́sticas semánticas del lenguaje en el árbol de derivación, y esto permitirá que la traducción sea más sencilla Es importante por tanto diseñar una buena gramática, pero luego es posible que se tenga que modificar según el tipo de analizador sintáctico que se desee utilizar Además, es posible que al diseñar el proceso de traducción sea necesario rediseñar la gramática para facilitar el diseño del traductor Diseño de gramáticas para expresiones: asociatividad La asociatividad indica cómo se agrupan los operandos en un operador cuando aparecen más de dos operandos. Por ejemplo, “4-3-2” normalmente vale “-1”, porque el operador “-” suele tener asociatividad por la izquierda, y primero se evalua “4-3” y al resultado se le resta “2”. Sin embargo, si la asociatividad fuera por la derecha, el resultado serı́a “3” (“4-(3-2)”) Los paréntesis permiten alterar la asociatividad por defecto de un operador ¿Cómo se puede reflejar la asociatividad en una gramática? asociatividad izquierda asociatividad derecha E E → → E opsuma T T opsuma E Pero... ¿no son todos los operadores asociativos por la izquierda? Sı́, casi todos, pero no todos: “a=b=c=0” Diseño de gramáticas para expresiones: precedencia En la mayorı́a de los lenguajes de programación, unos operadores se evaluan antes que otros. Por ejemplo, “2+3*4” casi siempre vale “14”. Como ocurre con la asociatividad, los paréntesis permiten alterar la precedencia de los operadores: “(2+3)*4” ¿Cómo se puede reflejar la precedencia en una gramática? con un no terminal diferente para cada nivel de precedencia: Expr Expr EBool EBool ExpRel E E T T F −→ −→ −→ −→ −→ → → → → → Expr or EBool EBool EBool and ExpRel ExpRel E oprel E E opsuma T T T opmul F F ... Algunos operadores no permiten usar más de dos operandos, como por ejemplo: “a<b<c” ⇒ “a<b && b<c” PL, 2013/2014 4 Tabla de asociatividades y precedencias Es una tabla en la que aparecen los operadores y su asociatividad, ordenados de menor a mayor precedencia: Operador @ % # A A B B C C D −→ −→ −→ −→ −→ −→ −→ Asociatividad izquierda derecha izquierda A @ B B C % B C C # D D ... Operadores unarios Los operadores unarios son más difı́ciles de reflejar en una gramática, requiere un buen conocimiento del lenguaje y de las gramáticas Los ejemplos más conocidos son: • el operador de negación, “!”, que además permite que se repita el operador: “!!!true” ExpRel −→ ! ExpRel • el operador de cambio de signo, “-” o “+”, que no permite repeticiones (por ejemplo, “----3” no es correcto) E −→ opsuma T El problema del dangling-else if (a<3) if (b<4) c=7; else c=8; ¿Con qué “if” se asocia el “else”? La regla que se usa normalmente es asociar el “else” al “if” más cercano: if (a<3) if (b<4) c=7; else c=8; Consejo: es mejor usar bloques entre llaves de forma explı́cita if (a<3) { if (b<4) { c=7; } else { c=8; } } PL, 2013/2014 5 Fragmento de gramática para la instrucción “if”: Instr Instr Instr Instr −→ −→ −→ −→ id opasig Expr ... if ( Expr ) Instr if ( Expr ) Instr else Instr Tiene dos caracterı́sticas no deseables: 1. Tiene factores comunes por la izquierda 2. ¡¡¡¡¡Es ambigua!!!!! . . . y sin embargo, se utiliza en prácticamente todos los compiladores basados en análisis sintáctico ascendente (los que usan yacc o bison).