26
UNIDAD 6 ANÁLISIS SINTÁCTICO El análisis sintáctico es la fase del compilador que se encarga de chequear el código fuente en base a una gramática dada. Y en caso de que el programa de entrada sea válido, suministra el árbol sintáctico que lo reconoce. El árbol sintáctico contiene la secuencia de tokens reconocidos, suministrada por el analizador léxico. El análisis sintáctico para lenguajes se basa en gramáticas formales, ya que de otra forma se hace muy difícil la comprensión del compilador, y se pueden corregir, quizás más fácilmente, errores de muy difícil localización, como es la ambigüedad en el reconocimiento de ciertas sentencias. Se puede describir la sintaxis de las construcciones de los lenguajes de programación por medio de gramáticas de contexto libre o notación BNF (Backus-Naur Form). En el modelo de compilador, el analizador sintáctico obtiene una cadena de tokens del analizador léxico, como se muestra en la Figura 6.1, y verifica que la cadena de nombres de los tokens pueda generarse mediante la gramática para el lenguaje fuente. 6.1 Gramáticas libres de contexto. Una gramática o sistema de estructuración de frases G consiste en: 1) Un conjunto finito N de símbolos no terminales. 2) Un conjunto finito T de símbolos terminales, en donde NT= 3) Un subconjunto finito P de [(NT)*- T*]x(NT)*, llamado conjunto de composiciones o reglas de producción. 4) Un símbolo inicial SN. Se escribe G=(N,T,P,S). Una composición (A,B)P se expresa usualmente AB Analiza dor léxico Analiza dor sintáct ico Rest o etap a inic ial Tabla de símbolo s Program a fuente toke n Árbol de análisi s sintáct ico Representac ión intermedia Obtene r siguie nte token Figura 6.1 Posición del analizador sintáctico en el modelo de compilador

Lenguajes y automatas unidad 6

Embed Size (px)

DESCRIPTION

Aqui se presenta un resumen de la asignatura lenguajes y automata de la unidad 6

Citation preview

Page 1: Lenguajes y automatas unidad 6

UNIDAD 6

ANÁLISIS SINTÁCTICOEl análisis sintáctico es la fase del compilador que se encarga de chequear el código fuente en base a una gramática dada. Y en caso de que el programa de entrada sea válido, suministra el árbol sintáctico que lo reconoce. El árbol sintáctico contiene la secuencia de tokens reconocidos, suministrada por el analizador léxico.

El análisis sintáctico para lenguajes se basa en gramáticas formales, ya que de otra forma se hace muy difícil la comprensión del compilador, y se pueden corregir, quizás más fácilmente, errores de muy difícil localización, como es la ambigüedad en el reconocimiento de ciertas sentencias.

Se puede describir la sintaxis de las construcciones de los lenguajes de programación por medio de gramáticas de contexto libre o notación BNF (Backus-Naur Form).

En el modelo de compilador, el analizador sintáctico obtiene una cadena de tokens del analizador léxico, como se muestra en la Figura 6.1, y verifica que la cadena de nombres de los tokens pueda generarse mediante la gramática para el lenguaje fuente.

6.1 Gramáticas libres de contexto.Una gramática o sistema de estructuración de frases G consiste en:

1) Un conjunto finito N de símbolos no terminales.

2) Un conjunto finito T de símbolos terminales, en donde NT=

3) Un subconjunto finito P de [(NT)*-T*]x(NT)*, llamado conjunto de composiciones o reglas de producción.

4) Un símbolo inicial SN.Se escribe G=(N,T,P,S).

Una composición (A,B)P se expresa usualmenteABy por la definición de composición A(NT)*-T* y B(NT)*. En consecuencia, A debe incluir al menos un símbolo no terminal, mientras que B puede consistir en cualquier combinación de símbolos terminales o no terminales.

Sea G una gramática y sea el arreglo nulo. Si toda composición es de la formaA en dondeAN

(NT)*-{}se dice que G es una gramática libre del contexto.

Ejemplo 6.1. La gramática G definida porT={a,b}, N={S}

Analizador léxico

Analizador

sintáctico

Resto

etapa

inicial

Tabla de símbolos

Programa fuente

token Árbol de

análisis sintáctic

o

Representación

intermediaObtener siguiente token

Figura 6.1 Posición del analizador sintáctico en el modelo de compilador

Page 2: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

con composicionesSaSb, Sab

y símbolo inicial S, es libre del contexto.

Ejemplo 6.2. La gramática para definir expresiones aritméticas:expr expr op exprexpr (expr)expr - exprexpr idop + | - | * | / | ^donde, los símbolos terminales son: id + - * / ^ ( ) y los símbolos no terminales son: expr y op, con expr como símbolo inicial, no es libre de contexto.

Alternativamente, la gramática del Ejemplo 6.2 puede especificarse concisamente como:E E A E | (E) | -E | idA + | - | * | / | ^Donde se han aplicado las siguientes convenciones de notación gramatical:1. Son terminales las minúsculas como a, b, c; los

operadores como +, *; los signos de puntuación; los dígitos decimales; y, las cadenas en negritas.

2. Son no terminales las primeras letras mayúsculas como A, B, C; la letra S, que cuando aparece suele ser el símbolo inicial; y, las cadenas en cursivas.

3. Son símbolos gramaticales, es decir, pueden ser terminales o no terminales, las últimas letras mayúsculas como X, Y, Z o las letras griegas minúsculas como , , .

4. Si A 1, A 2, …, A k, puede escribirse A 1| 2 | …| k.

5. A menos que se diga otra cosa, el lado izquierdo de la primera producción es el símbolo inicial.

Ejemplo 6.3. Una gramática más reducida de expresiones aritméticas:

1. expr expr + expr2. expr expr * expr3. expr (expr)4. expr id

6.2 Árboles de derivación.La construcción de un árbol de análisis sintáctico puede hacerse precisa si se toma una vista derivacional, en la cual las producciones se tratan como reglas de rescritura. Empezando con el símbolo inicial, cada paso de rescritura sustituye a un no terminal por el cuerpo de una de sus producciones. Esta vista derivacional corresponde a la construcción descendente de un árbol de análisis sintáctico.

Sea G=(N,T,P,S) una gramática. Si es una composición y xy(NT)*, se dice que xy se deriva directamente de xy y se escribe

xy xySi i(NT)* para i=1,...,n; y i+1 se deriva directamente de i para i=1,...,n-1, se dice que n es derivable de 1 y se escribe

1nSe llama

12...nderivación de n. Por convención, cualquier elemento de (NT)* es derivable de sí mismo.

Lenguajes y Autómatas I 2

Page 3: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

El lenguaje generado por G consiste en todos los arreglos definidos sobre T que se derivan de S, y se denota L(G).

Para la gramática del Ejemplo 6.1, las únicas derivaciones de S son

SaSb an-1Sbn-1

an-1abbn-1= anbn

Así que, L(G) es un lenguaje libre del contexto que consiste en los arreglos definidos sobre {a,b} de la formaanbn, n=1,2,...

Mediante la aplicación repetida de las producciones de la gramática del Ejemplo 6.3 pueden obtenerse expresiones más y más complicadas. Por ejemploexp expr * expr

(expr) * expr (expr) * id (expr + expr) * id (expr + id) * id (id + id) * id

Los árboles de derivación o de análisis gramatical superponen una estructura sobre las palabras de un lenguaje, que es de utilidad en las aplicaciones tales como la compilación de los lenguajes de programación. Los vértices de un árbol de derivación están etiquetados con símbolos terminales o variables de la gramática, o posiblemente con . Si un vértice interior n tiene etiqueta A y los hijos de n están etiquetados con X1, X2, …, Xk partiendo de la izquierda, entonces A

X1 X2 …Xk debe ser una producción. La Figura 6.2 muestra el árbol de análisis gramatical para la derivación de la gramática del Ejemplo 6.3. Nótese que si se leen las hojas de izquierda a derecha se obtiene la última línea de la derivación, (id + id) * id.

De manera más formal, sea G=(V, T, P, S) una GLC. Un árbol es un árbol de derivación (o de análisis gramatical) para G si:1. Cada vértice tiene una etiqueta, que es un

símbolo de VT.2. La etiqueta de la raíz es S.3. Si un vértice es interior y tiene etiqueta A,

entonces A debe estar en V.4. Si n tiene etiqueta A y vértices n1, n2, …, nk son

los hijos del vértice n, de izquierda a derecha, con etiquetas X1, X2, …, Xk, respectivamente, entonces AX1 X2 … Xk debe ser una producción de P.

5. Si el vértice n tiene etiqueta, entonces n es una hoja y es el único hijo de su padre.

Lenguajes y Autómatas I 3

expr

expr expr

expr

expr expr+

*

)( id

id idFigura 6.2. Árbol de

derivación

Page 4: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

6.3 Formas normales de Chomsky.La forma normal de Chomsky se establece por medio de teoremas que garantizan que toda gramática libre del contexto es equivalente a una gramática con restricciones sobre la forma de las producciones.

Forma normal de Chomsky o CNF (Teorema). Cualquier lenguaje libre de contexto sin , es generado por una gramática en la que todas las producciones son de la forma A BC o A a. Aquí, A, B y C son variables y a es un terminal.

Ejemplo 6.4. Sea la gramática ({S, A, B}, {a, b}, P, S) con producciones:S bA|aBA bAA|aS|aB aBB|bS|bEncuéntrese una gramática equivalente en CNF.Solución. Primero, las únicas producciones que ya se encuentran en forma apropiada son A a y B b. No existen producciones unitarias, así que puede empezarse sustituyendo los terminales de la derecha por variables, excepto en el caso de la producciones ya apropiadas. S bA se sustituye por S CbA y Cb b. De manera similar, A aS se sustituye por A CaS y Ca a; A bAA es sustituida por A CbAA; S aB se reemplaza por S CaB; B bS es reemplazado por B CbS; y, B aBB es sustituida por B CaBB. En una siguiente etapa, la producción A CbAA se sustituye A CbD1 y D1 AA; y la producción B CaBB se reemplaza por B CaD2 y D2 BB. Las producciones para la gramática CNF son:S CbA| CaB

A CaS| CbD1|aB CbS| CaD2|bD1 AAD2 BBCa aCb b

Forma normal de Greibach o GNF (teorema). Cada lenguaje libre de contexto L que no contenga , puede ser generado por una gramática para la cual cada producción es de la forma A a, en donde A es una variable, a un terminal y una cadena (posiblemente vacía) de variables.

El teorema de Greibach se basa en los dos lemas siguientes.

Lema 1. Defínase una producción A como una producción con variable A en la izquierda. Sea G = (V, T, P, S) una CFG. Sea A 1B2 una producción de P y sea B 1|2|…|r el conjunto de todas las producciones B. Hágase G1 = (V, T, P, S) la gramática obtenida de G mediante la eliminación de la producción A 1B2 de P y la adición de las producciones A 112|122|…|1r2. Entonces L(G) = L(G1)

Lema 2. Sea G = (V, T, P, S) una CFG. Sea A A1| A2|…| Ar el conjunto de las producciones A para las cuales A es el símbolo que está más a la izquierda del lado derecho. Sean A 1|2|…|s las restantes producciones de A. Hágase G1 = (V{B}, T, P, S) la CFG formada por la adición de la variable B a V y la sustitución de todas las producciones A por las producciones:

Lenguajes y Autómatas I 4

Page 5: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

Entonces L(G1) = L(G).

Ejemplo 6.5. Convertir a GNF la gramáticaG = ({A1, A2, A3}, {a, b}, P, A1)en donde P consiste en lo siguienteA1 A2A3A2 A3A1|bA3 A1A2|a

Solución. Primer paso: Como el lado derecho de las producciones para A1 y A2 comienzan con terminales o variables con número mayor, se empieza con la producción A3 A1A2 y se sustituye la cadena por A2A3A2. Nótese que A1 A2A3 es la única producción con A1 en la izquierda.El conjunto resultante de producciones es:A1 A2A3A2 A3A1|bA3 A2A3A2|aComo el lado derecho de la producción A3 A2A3A2 comienza con una variable de número menor, se sustituye la primera ocurrencia de A2 con A3A1 y b. Por tanto A3 A2A3A2 se sustituye con A3 A3A1A3A2 y A3A2. El nuevo conjunto esA1 A2A3A2 A3A1|bA3 A3A1A3A2|bA3A2|aSe transforman ahora las produccionesA3 A3A1A3A2|bA3A2|a.

Se introduce el símbolo B3 y la producción A3 A3A1A3A2 se sustituye por A3 bA3A2B3, A3 aB3, B3 A1A3A2 y B3 A1A3A2B3. El conjunto resultante esA1 A2A3A2 A3A1|bA3 bA3A2B3|aB3|bA3A2|aB3 A1A3A2|A1A3A2B3

Segundo paso: Ahora todas las producciones con A3 en la izquierda tienen lado derecho que comienzan con terminales. Estas se utilizan para sustituir a A3 en la producción A2 A3A1 y entonces las producciones con A2 en el lado izquierdo se utilizan para sustituir a A2 en la producción A1 A2A3. El resultado es el siguiente.A3 bA3A2B3 A3 bA3A2A3 aB3 A3 aA2 bA3A2B3A1 A2 bA3A2A1A2 aB3A1 A2 aA1A2 bA1 bA3A2B3A1A3 A1 bA3A2A1A3A1 aB3A1A3 A1 aA1A3A1 bA3B3 A1A3A2 B3 A1A3A2B3

Paso 3. Las dos producciones B3 se convierten a una forma apropiada, lo que trae como resultado otras 10 producciones más. Es decir, las produccionesB3 A1A3A2 y B3 A1A3A2B3quedan alteradas mediante la colocación dl lado derecho de las cinco producciones con A1 en la izquierda en lugar de la primera A1. En consecuencia B3 A1A3A2 se convierte enB3 bA3A2B3A1A3A3A2

Lenguajes y Autómatas I 5

Page 6: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

B3 aB3A1A3A3A2B3 bA3A3A2B3 bA3A2A1A3A3A2B3 aA1A3A3A2Las otras producciones para B3 se sustituyen de manera similar. El conjunto final de producciones esA3 bA3A2B3 A3 bA3A2A3 aB3 A3 aA2 bA3A2B3A1 A2 bA3A2A1A2 aB3A1 A2 aA1A2 bA1 bA3A2B3A1A3 A1 bA3A2A1A3A1 aB3A1A3 A1 aA1A3A1 bA3B3 bA3A2B3A1A3A3A2B3 B3 bA3A2B3A1A3A3A2B3 aB3A1A3A3A2B3 B3 aB3A1A3A3A2B3 bA3A3A2B3 B3 bA3A3A2B3 bA3A2A1A3A3A2B3 B3 bA3A2A1A3A3A2B3 aA1A3A3A2B3 B3 aA1A3A3A2

6.4 Diagramas de sintaxis.Un diagrama de sintaxis es una forma gráfica de expresar reglas de gramáticas. Cada regla está representada por un camino que va de la entrada a la izquierda a la salida a la derecha. Cualquier trayecto válido de entrada a salida representa una cadena generada por esa regla.

Existen varias diferencias entre los diagramas de transiciones para un analizador léxico y para un analizador sintáctico predictivo. En el caso de un analizador sintáctico, hay un diagrama por cada no terminal. Las etiquetas de las aristas son

componentes léxicos y no terminales. Una transición con un componente léxico (terminal) supone que se debe tomar dicha transición si ese componente léxico es el siguiente símbolo de entrada. Una transición con un no terminal A es una llamada al procedimiento para A.

Para construir el diagrama de transiciones de un analizador sintáctico predictivo a partir de una gramática, primero se debe eliminar la recursión por la izquierda de la gramática, y después factorizar dicha gramática por la izquierda. Luego, para cada no terminal A se hace lo siguiente:1. Crear un estado inicial y un estado final (de

retorno).2. Para cada producción A X1X2…Xn, crear un

camino desde el estado inicial al estado final, con aristas etiquetadas con X1, X2, …, Xn.

Lenguajes y Autómatas I 6

0 1 2

3 4 5 6

7 8 9

10

11

12

13

14

15

16

17

E:

T E’

E’:

+ T E’

T:

F T’

T’:

* F T’

F:

( E )

id

Figura 6.3 Diagramas de sintaxis.

Page 7: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

Ejemplo 6.6. Sea la gramática:E TE’E’ +TE’|T FT’T’ *FT’|F (E) | id

Los diagramas de sintaxis para esta gramática se muestran en la Figura 6.3. Los diagramas simplificados se muestran en la figura 6.4.

6.5 Ambigüedad.Si en cada paso de una derivación se aplica una producción a la variable que se encuentra más a la izquierda, entonces la derivación es de extrema izquierda. De manera similar, se dice que una derivación, en la que la variable que está más a la derecha se sustituye en cada paso, es de extrema derecha. Si w está en L(G) para la CFG G, entonces w tiene al menos un árbol de análisis gramatical, y correspondiendo a un árbol de análisis gramatical particular, w tiene una derivación izquierda y una derivación derecha únicas.

Por supuesto, w puede tener varias derivaciones derechas o izquierdas, ya que puede haber más de un árbol de derivación para w. Sin embargo, de cada árbol de derivación sólo se puede obtener una derivación extrema izquierda y una derivación extrema derecha.

Ejemplo 6.6. Para el árbol de derivación de la Figura 6.5 correspondiente a la gramática con producciones S aAS|a, A SbA|SS|ba, la derivación extrema izquierda es

S aAS aSbAS aabAS aabbaS aabbaaY la derivación extrema derecha es

S aAS aAa aSbAa aSbbaa aabbaa

Lenguajes y Autómatas I 7

S

a A

bS

S

A a

a b a

Figura 6.5. Árbol de derivación.

0 0 6

3 4 5

6

14

15

16

17

E:

T

E’:

+ T

F:

( E )

id

Figura 6.4 Diagramas de sintaxis simplificados.

3 4

6

+T

3 4

6

+

TT

3

+

7F

8

13

*

T:

:

Page 8: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

Una gramática libre de contexto G de la que alguna palabra tenga dos árboles de análisis gramatical se dice que es gramática ambigua. Es decir, una gramática es ambigua si alguna palabra tiene más de una derivación extrema izquierda o más de una derivación extrema derecha.

A veces, una gramática ambigua se puede rescribir para eliminar la ambigüedad. Como ejemplo, se eliminará la ambigüedad de la siguiente gramática con “else ambiguo”:

prop if expr then prop |if expr then prop else prop |otra

Aquí, otra representa cualquier otra proposición.

De acuerdo con esta gramática, la proposición condicional compuestaif E1 then S1 else if E2 then S2 else S3tiene el árbol de análisis sintáctico que se muestra en la Figura 6.6. La gramática es ambigua puesto que la cadenaif E1 then if E1 then S1 else S2tiene los dos árboles de análisis sintáctico que se muestran en la Figura 6.7.

Lenguajes y Autómatas I 8

prop

if expr

then

prop

E1

if expr

then

prop

else

prop

E2 S1 S2

prop

if expr

then

prop

else

prop

E1 S2

if then

expr

prop

E2 S1

Figura 6.7 Dos árboles de análisis sintáctico para una frase ambigua

prop

if expr

then

prop

else

prop

E1 S1

if expr

then

prop

else

prop

E2 S2 S3

Figura 6.6 Árbol de análisis sintáctico para la proposición condicional

Page 9: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

En todos los lenguajes de programación con proposiciones condicionales de esta forma, se prefiere el primer árbol de análisis sintáctico. La regla general es, “emparejar cada else con el then sin emparejar anterior más cercano”. Esta regla para eliminar ambigüedades se puede incorporar directamente a la gramática. Por ejemplo, se puede rescribir la gramática ambigua como la siguiente gramática no ambigua. La idea es que una proposición que aparezca entre un then y un else debe estar “emparejada”; es decir, no debe terminar con un then sin emparejar seguido de cualquier proposición, porque entonces el else estaría obligado a concordar con este then no emparejado. Una proposición emparejada es o una proposición if-then-else que no contenga proposiciones sin emparejar o cualquier otra clase de proposición no condicional. Así, se puede utilizar la gramáticaprop propEmp |

propNoEmppropEmp if expr then propEmp else propEmp |

otrapropNoEmp if expr then prop |

if expr then propEmp else propNoEmpEsta gramática genera el mismo conjunto de cadenas, pero permite sólo un análisis sintáctico para la cadena del ejemplo, es decir, el que asocia cada else con el then sin emparejar anterior más cercano.

Ejemplo 6.7 Sea la gramática: E E + E | E * E | (E) | id. Para el enunciado id + id * id se tienen dos derivaciones por la izquierda distintas:E E + E E E * E

id + E E + E * E id + E * E id + E * E id + id * E id + id * E

id + id * id id + id * id

Los árboles de análisis sintáctico correspondientes aparecen en la Figura 6.8.

La gramática no ambiguaE E + T | TT T * F | FF ( E ) | idGenera el mismo lenguaje, pero da a + una menor precedencia que a *, y convierte a ambos operadores en asociativos por la izquierda.

6.6 Generación de la matriz predictiva.Los analizadores sintácticos predictivos, es decir, los analizadores sintácticos de descenso recursivo que no necesitan rastreo hacia atrás, pueden construirse para una clase de gramáticas llamadas LL(1). La primera ‘L’ significa explorar la entrada de izquierda a derecha, la segunda ‘L’ significa una derivación por la izquierda, y el ‘1’ significa usar un símbolo de entrada de anticipación en cada paso, para tomar las decisiones de acción del análisis sintáctico.

Lenguajes y Autómatas I 9

E

E + Eid

E * Eid

id

E

E * E

id

E + E idi

dFigura 6.8 Dos árboles de análisis sintáctico para id+id*id

Page 10: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

Para definir formalmente las gramáticas LL(1) es necesario definir las funciones PRIMERO y SIGUIENTE.

Si es una cadena de símbolos gramaticales, se considera PRIMERO() como el conjunto de terminales que inician las cadenas derivadas de . Si * , entonces también está en PRIMERO().

Se define SIGUIENTE(A), para el no terminal A, como el conjunto de terminales a que pueden aparecer inmediatamente a la derecha de A en alguna forma de frase, es decir, el conjunto de terminales a tal que haya una derivación de a forma S * Aa para algún y . Obsérvese que en algún momento de la derivación pudieron haber existido símbolos entre A y a, pero si así fue, derivaron a y desaparecieron. Si A puede ser el símbolo situado más a la derecha en una forma de frase, entonces $ (fin de cadena) está en SIGUIENTE(A).

Para calcular PRIMERO(X) para todos los símbolos gramaticales X, se aplican las reglas siguientes hasta que no puedan añadirse más terminales o a ningún conjunto PRIMERO.1. Si X es terminal, entonces PRIMERO(X) es {X}.2. Si X es una producción, entonces añádase

a PRIMERO(X).3. Si X es no terminal y X Y1Y2…Yk es una

producción, entonces se pone a en PRIMERO(X) si, para alguna i, a está en PRIMERO(Yi) y está en todos los PRIMERO(Y1),…, PRIMERO(Yi-1); es decir, Y1…Yi-1 * . Si está en PRIMERO(Yj) para

toda j=1, 2, …, k, entonces se añade a PRIMERO(X). Por ejemplo, todo lo que está en PRIMERO(Y1) sin duda está en PRIMERO(X). Si Y1 no deriva a , entonces no se añade nada más a PRIMERO(X), pero si Y1 * , entonces se le añade PRIMERO(Y2), y así sucesivamente.

Ahora puede calcularse PRIMERO para cualquier cadena X1X2…Xn de la siguiente forma: se añade a PRIMERO(X1X2…Xn) todos los símbolos distintos de de PRIMERO(X1). Si está en PRIMERO(X1), se añaden también los símbolos distintos de de PRIMERO(X2); si está tanto en PRIMERO(X1) como en PRIMERO(X2), se añaden también los símbolos distintos de de PRIMERO(X3), y así sucesivamente. Por último, se añade a PRIMERO(X1X2…Xn) si, para toda i, PRIMERO(Xi) contiene .

Para calcular SIGUIENTE(A) para todos los no terminales A, se aplican las reglas siguientes hasta que no se pueda añadir nada más a ningún conjunto SIGUIENTE.1. Se pone $ en SIGUIENTE(S), donde S es el

símbolo inicial y $ es el delimitador derecho de la entrada.

2. Si hay una producción A B, entonces todo lo que esté en PRIMERO() excepto se pone en SIGUIENTE(B).

3. Si hay una producción A B o una producción A B, donde PRIMERO() contenga (es decir, * ), entonces todo lo que esté en SIGUIENTE(A) se pone en SIGUIENTE(B).

Ejemplo 6.8. Para la gramática del ejemplo anterior se tienePRIMERO(E) = PRIMERO(T) = PRIMERO(F) = {(, id}

Lenguajes y Autómatas I 10

Page 11: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

PRIMERO(E’) = {+, }PRIMERO(T’) = {*, }SIGUIENTE(E) = SIGUIENTE(E’) = {), $}SIGUIENTE(T) = SIGUIENTE(T’) = {+, ), $}SIGUIENTE(F) = {+, *, ), $}

La clase de gramáticas LL(1) es lo bastante robusta como para cubrir la mayoría de las construcciones de programación, aunque hay que tener cuidado al escribir una gramática adecuada para el lenguaje fuente. Por ejemplo, ninguna gramática recursiva por la izquierda o ambigua puede ser LL(1).

Una gramática G es LL(1) si y solo si cada vez que A| son dos producciones distintas de G, se aplican las siguientes condiciones:1. Para el no terminal a, tanto como derivan

cadenas que empiecen con a.2. A lo más, sólo o puede derivar la cadena

vacía.3. Si *, entonces no deriva a ninguna cadena

que empiece con un terminal en SIGUIENTE(A). De igual forma, si *, entonces no deriva a ninguna cadena que empiece con un terminal en SIGUIENTE(A).

Las primeras dos condiciones son equivalentes para la instrucción que establece que PRIMERO() y PRIMERO() son conjuntos separados. La tercera condición equivale a decir que si está en PRIMERO(), entonces PRIMERO() y SIGUIENTE(A) son conjuntos separados, y de igual forma si está en PRIMERO().

Puede construirse la tabla de un analizador sintáctico predictivo mediante el algoritmo 6.1.

Algoritmo 6.1. Construcción de una tabla de análisis sintáctico predictivo.Entrada. Una gramática G.Salida. La tabla de análisis sintáctico M.Método. Para cada producción A de la gramática hacer:

1. Para cada terminal a en PRIMERO(A), agregar A a M[A, a].

2. Si está en PRIMERO(), entonces para cada terminal b en SIGUIENTE(A), se agrega A a M[A, b]. Si está en PRIMERO() y $ se encuentra en SIGUIENTE(A), se agrega A a M[A, $] también.

Si después de los pasos anteriores no hay producción en M[A, a], entonces se establece M[A, a] a error (que por lo general se representa mediante una entrada vacía en la tabla.

Este algoritmo se basa en la siguiente idea: se elige la producción A si el siguiente símbolo de entrada a se encuentra en PRIMERO(). La única complicación ocurre cuando =, o en forma más general, *. En este caso, debe elegirse de nuevo A si el símbolo de entrada actual se encuentra en SIGUIENTE(A), o si se ha llegado a $ (fin de cadena) y $ se encuentra en SIGUIENTE(A).

Ejemplo 6.9. Considérese la gramática:E E + T | TT T * F | FF (E) | idEliminando la recursión directa por la izquierda (producciones de la forma A A) a las producciones de E y después a las de T, se obtiene

Lenguajes y Autómatas I 11

Page 12: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

E TE’E +TE’|T FT’T’ *FT’|F (E) | id

En la Figura 6.10 se muestra una tabla de análisis sintáctico predictivo para esta gramática. Los espacios en blanco son entradas de error; los otros espacios indican una producción con la cual expandir el no terminal de la cima en la pila.

NO TERMIN

SÍMBOLO DE ENTRADAid + * ( ) $

E ETE’

ETE’

E’ E’TE’

E’ E’

T TFT TFT’

T’ T’ T’*FT’

T’ T’

F Fid F(E)Figura 6.10 Tabla de análisis sintáctico M.

El problema clave durante el análisis sintáctico predictivo es determinar la producción que debe aplicarse a un no terminal. El analizador sintáctico no recursivo busca la producción que debe aplicarse en una tabla de análisis sintáctico.

Un analizador sintáctico predictivo guiado por tablas tiene un buffer de entrada, una pila, una tabla de análisis sintáctico y una cadena de salida

(Figura 6.11). El buffer de entrada contiene la cadena que se va a analizar, seguida de $, un símbolo utilizado como delimitador derecho para indicar el fin de la cadena de entrada. La pila contiene una secuencia de símbolos gramaticales con $ en la parte de abajo, que indica la base de la pila. Al principio, la pila contiene el símbolo inicial de la gramática encima de $. La tabla de análisis sintáctico es una matriz bidimensional M[A, a] donde A es un no terminal o el símbolo $. Se controla el analizador sintáctico mediante un programa que se comporta como se describe a continuación.

El programa tiene en cuenta X, el símbolo de la cima de la pila, y a, el símbolo en curso de la entrada. Estos dos símbolos determinan la acción del analizador. Existen tres posibilidades:1. Si X=a=$, el analizador sintáctico se detiene y

anuncia el éxito de la realización del análisis.2. Si X=a$, el analizador sintáctico saca a X de la

pila y mueve el apuntador de entrada al siguiente símbolo de entrada.

3. Si X es un no terminal, el programa consulta la entrada M[X,a] de la tabla de análisis sintáctico. Esta entrada será o una producción de X de la gramática o una entrada de error. Si, por ejemplo, M[X,a] = {X UVW}, el analizador sintáctico sustituye la X de la cima de la pila por WVU (con U en la cima). Como salida, se sabe que el analizador sintáctico sólo imprime la producción utilizada; ahí se podría ejecutar cualquier otro código. Si M[X,a]=error, el analizador sintáctico llama a una rutina de recuperación de error.

Lenguajes y Autómatas I 12

Page 13: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

Se puede describir el comportamiento del analizador sintáctico en función de sus configuraciones, que dan el contenido de la pila y la entrada restante (Algoritmo 6.2).

Algoritmo 6.2. Análisis sintáctico predictivo no recursivo.Entrada. Una cadena w y una tabla de análisis sintáctico M para la gramática G.Salida. Si w está en L(G), una derivación por la izquierda de w, de lo contrario, una indicación de error.Método. Al principio, el analizador sintáctico está en una configuración en la que tiene a $S en la pila con S, el símbolo inicial de G en el tope, y w$ en el buffer de entrada.

establecer ip para que apunte al primer símbolo de w;establecer X con el símbolo de la parte superior de la pila;while (X$) { /* la pila no está vacía */

if (X es a) sacar de la pila y avanzar ip;else if (X es un terminal) error();else if (M[X, a] es una entrada de error) error();else if (M[X, a] = X Y1Y2…Yk) {

enviar de salida la producción X Y1Y2…Yk;sacar de pila;meter Yk Yk-1, …, Y1 en la pila, con Y1 en la

cima;}establecer X con el símbolo de la cima de la pila;

}

Con la entrada id + id * id el analizador sintáctico predictivo realiza la secuencia de movimientos de la Figura 6.12. Estos movimientos corresponden a una derivación por la izquierda. El apuntador de entrada apunta al símbolo de la extrema izquierda de la cadena en la columna ENTRADA. Si se observa con atención las acciones de este analizador sintáctico, se nota que está buscando una derivación por la izquierda para la entrada, es decir, las producciones emitidas son las de una derivación por la izquierda. Los símbolos de entrada que ya se han examinado, seguidos de los símbolos gramaticales de la pila (de la cima al fondo), son las formas de frase izquierdas de la derivación.

PILA ENTRADA SALIDA$E id + id * id$$E’T id + id * id$ E TE’$E’T’F id + id * id$ T FT’$E’T’id id + id * id$ F id$E’T’ + id * id$$E’ + id * id$ T’ $E’T+ + id * id$ E’ + TE’$E’T id * id$$E’T’F id * id$ T FT’$E’T’id id * id$ F id

Lenguajes y Autómatas I 13

Programa para análisis sintáctico

predictivoTabla de análisis

sintáctico M

a + b $

X Y Z $

SALIDA

ENTRADA

PILA

Figura 6.11Modelo de analizador sintáctico predictivo no recursivo

Page 14: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

$E’T’ * id$$E’T’F* * id$ T’ *FT’$E’T’F id$$E’T’id id$ F id$E’T’ $$E’ $ T’ $ $ E’ Figura 6.12 Movimientos del analizador sintáctico

predictivo para id + id * id

6.7 Tipos de analizadores sintácticos.Los tipos de analizadores sintácticos se definen de acuerdo a la forma de construir el árbol sintáctico. Pueden ser generales, descendentes o ascendentes.

Universales. Los métodos universales de análisis sintáctico como el algoritmo de Cocke-Younger-Kasami y el algoritmo de Earley pueden analizar cualquier gramática. Sin embargo, estos métodos generales son demasiado ineficientes como para usarse en la producción de compiladores.

Descendentes. Parten del símbolo inicial y van efectuando derivaciones a la izquierda hasta obtener la secuencia de derivaciones que reconocen la sentencia.

Ascendentes. Parten de los terminales y van aplicando reglas de producción hacia atrás, desde el consecuente hasta el antecedente, hasta llegar al símbolo inicial.

Analizadores con retroceso. Se hace una búsqueda en profundidad con retroceso para garantizar que se encuentra la frase. Costo O(kn).

Analizadores predictivos. Se determina qué regla aplicar a partir de un análisis de los primeros tokens de la entrada.

Analizadores predictivos LL(1). Determinan qué regla de producción aplicar en cada paso en función del token que se encuentra en cada momento en la cabeza de lectura.

Analizadores predictivos LL(k). Determinan qué regla de producción aplicar en cada paso en función de los k primeros tokens que se encuentra en cada momento en la cabeza de lectura.

Analizadores sintácticos LR(k). Hacen un análisis sintáctico ascendente de acuerdo a una gramática libre del contexto. La L es por el examen de la entrada de izquierda a derecha (inglés: left-to-right), la R por construir una derivación por la derecha (inglés: rightmost derivation) en orden inverso, y la k por el número de símbolos de entrada de examen por anticipado utilizados para tomar las decisiones del análisis sintáctico. Cuando se omite, se supone que k es 1.

Existen tres técnicas para construir una tabla de análisis sintáctico LR para una gramática. El primer método, llamado SLR (S de simple) es el más fácil de implantar pero pudiera fallar para algunas gramáticas. El segundo método, llamado LR canónico, es el más poderoso y costoso. El tercer

Lenguajes y Autómatas I 14

Page 15: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

método, llamado LALR (LA de examen anticipado), está entre los otros dos en cuanto a poder y costo. Funciona con las gramáticas de la mayoría de los lenguajes de programación y, con un poco de esfuerzo, se puede implantar en forma eficiente.

6.8 Manejo de errores.Si un compilador tuviera que procesar sólo programas correctos, su diseño e implementación se simplificaría en forma considerable. No obstante, se espera que un compilador ayude al programador a localizar y rastrear los errores que, de manera inevitable, se infiltran en los programas, a pesar de los mejores esfuerzos del programador.

Una vez que se detecta un error, ¿cómo debe recuperarse el analizador sintáctico? El método más simple es que el analizador sintáctico termine con un mensaje de error informativo cuando detecte el primer error. A menudo se descubren errores adicionales si el analizador sintáctico puede restaurarse a sí mismo, a un estado en el que pueda continuar el procesamiento de la entrada, con esperanzas razonables de que un mayor procesamiento proporcione información útil para el diagnóstico. A continuación, las estrategias de recuperación de errores: modo de pánico, nivel de frase, producciones de errores y corrección global.

Recuperación en modo de pánico.Con este método, al descubrir un error el analizador sintáctico descarta los símbolos de entrada, uno a la vez, hasta encontrar un conjunto

designado de tokens de sincronización. Por lo general, los tokens de sincronización son delimitadores como el punto y coma o }, cuya función en el programa fuente es clara y sin ambigüedades. El diseñador del compilador debe seleccionar los tokens de sincronización apropiados para el lenguaje fuente. Aunque la corrección de modo de pánico a menudo omite una cantidad considerable de entrada sin verificar errores adicionales, tiene la ventaja de ser simple y, de garantizar que no entrará en un ciclo infinito.

Recuperación a nivel de frase.Al descubrir un error, un analizador sintáctico puede realizar una corrección local sobre la entrada restante; es decir, puede sustituir un prefijo de la entrada restante por alguna cadena que le permita continuar. Una corrección local común es sustituir una coma por un punto y coma, eliminar un punto y coma extraño o insertar un punto y coma faltante. La elección de la corrección local se deja al diseñador del compilador. Desde luego que debe tenerse cuidado de elegir sustituciones que no lleven hacia ciclos infinitos, como sería, por ejemplo, si siempre se inserta algo en la entrada adelante del símbolo de entrada actual.La sustitución a nivel de frase se ha utilizado en varios compiladores, que reparan los errores, ya que puede corregir cualquier cadena de entrada. Su desventaja principal es la dificultad que tiene para arreglárselas con situaciones en las que el error actual ocurre antes del punto de detección.

Lenguajes y Autómatas I 15

Page 16: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

Producciones de errores.Al anticipar los errores comunes que pueden encontrarse, puede que aumente la gramática para el lenguaje, con producciones que generen las construcciones erróneas. Un analizador sintáctico construido a partir de una gramática aumentada por estas producciones de errores detecta los errores anticipados cuando se utiliza una producción de error durante el análisis sintáctico. Así, el analizador sintáctico puede generar diagnósticos de error apropiados sobre la construcción errónea que se haya reconocido en la entrada.

Corrección global.Lo ideal sería que un compilador hiciera la menor cantidad de cambios en el procesamiento de una cadena de entrada incorrecta. Hay algoritmos para elegir una secuencia mínima de cambios, para obtener una corrección con el menor costo a nivel global. Dada una cadena de entrada incorrecta x y una gramática G, estos algoritmos buscarán un árbol de análisis sintáctico para una cadena y relacionada, de tal forma que el número de inserciones, eliminaciones y modificaciones de los tokens requeridos para transformar a x en y sea lo más pequeño posible. Por desgracia, estos métodos son en general demasiado costosos para implementarlos en términos de tiempo y espacio, por lo cual estas técnicas sólo son de interés teórico actualmente.Hay que observar que un programa casi correcto tal vez no sea lo que el programador tenía en mente. Sin embargo, la noción de la corrección con

el menor costo proporciona una norma para evaluar las técnicas de recuperación de errores, la cual se ha utilizado para buscar cadenas de sustitución óptimas para la recuperación a nivel de frase.

6.9 Generadores de analizadores sintácticos.

El generador de analizadores sintácticos LALR de nombre Yacc implementa muchos conceptos del análisis sintáctico y se emplea mucho. Yacc significa “yet another compiler-compiler” (otro compilador de compiladores más) y su primera versión es de S.C. Johnson, 1970.

Puede construirse un traductor mediante el uso de Yacc de la forma que se ilustra en la Figura 6.13. En primer lugar se prepara un archivo, por decir traducir.y, el cual contiene una especificación de Yacc del traductor. El siguiente comando del sistema

yacc traducir.ytransforma el archivo traducir.y en un programa en C llamado y.tab.c usando el método LALR. El programa en y.tab.c es una representación de un analizador sintáctico LALR escrito en C, junto con otras rutinas en C que el usuario puede haber preparado. Al compilar y.tab.c junto con la biblioteca ly que contiene el programa de análisis sintáctico LR mediante el uso del comando:

cc y.tab.c –lyse obtiene el programa objeto a.out deseado, el cual realiza la traducción especificada por el programa original en Yacc. Si se necesitan otros procedimientos, pueden compilarse o cargarse con

Lenguajes y Autómatas I 16

Page 17: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

y.tab.c, de igual forma que con cualquier programa en C.

Un programa en Yacc tiene tres partes:Declaraciones%%Reglas de traducción%%Soporte de las rutinas en C

Ejemplo 6.10 Construcción de una calculadora de escritorio simple que lee una expresión aritmética, la evalúa e imprime su valor numérico. Se define la gramática:E E + T | TT T * F | FR ( E ) | digito

El token digito es un solo dígito entre 0 y 9. En la figura 6.14 se muestra un programa en Yacc, derivado a partir de esta gramática.

%{#include <ctye.h>%}

%token DIGITO%%linea : expr ‘\n’ { printf(“%d\n”, $1); }

;expr : expr ‘+’ term { $$ = $1 + $3; }

| term;

term : term ‘*’ factor { $$ = $1 * $3; }| factor;

factor : ‘(‘ expr ‘)’ { $$ = $2; }| DIGITO;

%%yylex() {

int c;c = getchar();if (isdigit(c)) {

yyval = c – ‘0’;return DIGITO;

}return c;

}Figura 6.14 Especificación Yacc de una calculadora

de escritorio simple.

ResumenAnalizadores sintácticos. Un analizador sintáctico recibe como entrada tokens del analizador léxico, y trata los nombres de los tokens como símbolos terminales de una gramática libre de contexto. Después, el analizador construye un árbol de análisis sintáctico para su secuencia de tokens de

Lenguajes y Autómatas I 17

Figura 6.13 Creación de un traductor de E/S con Yacc.

Compilador de Yacc

Compilador de C

a.out

traducir.y

y.tab.c

entrada

y.tab.c

a.out

salida

Page 18: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

entrada; el árbol de análisis sintáctico puede construirse en sentido figurado (pasando por los pasos de derivación correspondientes) o en forma literal.

Gramáticas libres de contexto. Una gramática especifica un conjunto de símbolos terminales (entrada), otro conjunto de no terminales (símbolos que representan construcciones sintácticas) y un conjunto de producciones, cada una de las cuales proporciona una forma en la que se pueden construir las cadenas representadas por un no terminal, a partir de símbolos terminales y cadenas representados por otros no terminales. Una producción consiste en un encabezado (el no terminal a sustituir) y un cuerpo (la cadena de símbolos gramaticales de sustitución).

Derivaciones. Al proceso de empezar con el no terminal inicial de una gramática y sustituirlo en forma repetida por el cuerpo de una de sus producciones se le conoce como derivación. Si siempre se sustituye el no terminal por la izquierda (o por la derecha), entonces a la derivación se le llama por la izquierda (o respectivamente, por la derecha).

Árboles de análisis sintáctico. Un árbol de análisis sintáctico es una representación gráfica de una derivación, en la cual hay un nodo para cada no terminal que aparece en la derivación. Los hijos de un nodo son los símbolos mediante los cuales se sustituye este no terminal en la derivación. Hay una correspondencia de uno a uno entre los árboles de análisis sintáctico, las derivaciones por

la izquierda y las derivaciones por la derecha de la misma cadena de terminales.

Ambigüedad. Una gramática para la cual cierta cadena de terminales tiene dos o más árboles de análisis sintáctico distintos, o en forma equivalente, dos o más derivaciones por la izquierda, o dos o más derivaciones por la derecha, se considera ambigua. En la mayoría de los casos de interés práctico, es posible rediseñar una gramática ambigua de tal forma que se convierta en una gramática sin ambigüedad para el mismo lenguaje. No obstante, las gramáticas ambiguas con ciertos trucos aplicados nos llevan algunas veces a la producción de analizadores sintácticos más eficientes.

Análisis sintáctico descendente y ascendente. Por lo general, los analizadores sintácticos se diferencian en base a si trabajan de arriba hacia abajo (si empiezan con el símbolo inicial de la gramática y construyen el árbol de análisis sintáctico partiendo de la parte superior) o de abajo hacia arriba (si empiezan con los símbolos terminales que forman las hojas del árbol de análisis sintáctico y construyen el árbol partiendo de la parte inferior). Los analizadores sintácticos descendentes incluyen los analizadores sintácticos con descenso recursivo y LL, mientras que las formas más comunes de analizadores sintácticos ascendentes son analizadores sintácticos LR.

Analizadores sintácticos LL(1). Una gramática en la que es posible elegir la producción correcta con la cual se pueda expandir un no terminal dado, con

Lenguajes y Autómatas I 18

Page 19: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

solo analizar el siguiente símbolo de entrada, se conoce como LL(1). Estas gramáticas permiten construir una tabla de análisis sintáctico predictivo que proporcione, para cada no terminal y cada símbolo de pre-análisis, la elección de la producción correcta. La corrección de errores puede facilitarse al colocar las rutinas de error en algunas, o en todas las entradas en la tabla que no tengan una producción legítima.

Yacc. El generador de analizadores sintácticos Yacc recibe una gramática (posiblemente) ambigua junto con la información de resolución de conflictos, y construye los estados del LALR. Después produce una función que utiliza estos estados para realizar un análisis sintáctico ascendente y llama a una función asociada cada vez que se realiza una reducción.

Ejercicios…

Apéndice. Construcciones de lenguajes no independientes del contexto.No debe sorprender que algunos lenguajes no puedan ser generados por ninguna gramática. De hecho, unas cuantas construcciones sintácticas de muchos lenguajes de programación no se pueden especificar utilizando tan solo gramáticas.Ejemplo 1. Considérese el lenguaje abstracto L1 = {wcw|w está en (a|b)*}. L1 consta de todas las palabras compuestas por una cadena repetida de caracteres a y b separados por una c, como aabcaab. Puede demostrarse que este lenguaje no es independiente del contexto. Este lenguaje

resume el problema de asegurar que los identificadores se declaren antes de su uso en un programa. Es decir, la primera w de wcw representa la declaración de un identificador w. La segunda w representa su uso. En un compilador donde se requiera comprobar la declaración de identificadores antes de su uso, tal comprobación se lleva a cabo en la fase de análisis semántico.Ejemplo 2. El lenguaje L2 = {anbmcndm|n1 y m1} no es independiente del contexto. Es decir, L2 consta de cadenas en el lenguaje generado por la expresión regular a*b*c*d* tales que los números de a y c son iguales, lo mismo que los números de b y d. L2 resume el problema de comprobar si el número de parámetros formales en la declaración de un procedimiento coincide con el número de argumentos en uso de este procedimiento. Es decir, an y bm podrían representar las listas de parámetros formales en dos procedimientos con n y m argumentos, respectivamente. Entonces cn y dm representan las listas de parámetros reales en llamadas a dichos procedimientos.

Ejemplo 3. El lenguaje L3 = {anbncn|n>0}, es decir, cadenas en L(a*b*c*) con el mismo número de caracteres a, b, y c, no es independiente del contexto. Un ejemplo de un programa que incluye L3 es el siguiente. Los textos de tipografía utilizan cursivas donde los textos corrientes utilizan el subrayado. Al convertir un archivo de texto destinado a imprimirse en una impresora de líneas en texto adecuado para un dispositivo de fotocomposición, hay que sustituir las palabras subrayadas por cursivas. Una palabra subrayada es una cadena de letras seguida de un mismo número

Lenguajes y Autómatas I 19

Page 20: Lenguajes y automatas unidad 6

6. Análisis Sintáctico

de retrocesos (caracteres back space de ASCII) y de un número igual de caracteres de subrayado. Si se considera a como una letra, b como el carácter de retroceso y c como el carácter de subrayado, el lenguaje L3 representa palabras subrayadas. La conclusión es que no se puede utilizar una gramática para describir palabras subrayadas de esta forma. Por otra parte, si se representa una palabra subrayada mediante una secuencia de triples letra-retroceso-subrayado, entonces se pueden representar las palabras subrayadas con la expresión regular (abc)*.

Es interesante observar que lenguajes muy similares a L1, L2 y L3 son independientes del contexto. Por ejemplo, L’1 = {wcwR|w está en (a|b)*}, donde wR representa una w invertida, es independiente dl contexto. Se genera por la gramáticaSaSa|bSb|cEl lenguaje L’2 = {anbmcmdn|n1 y m1} es independiente del contexto, con la gramáticaSaSd|aAdAbAc|bc

Asimismo, L’’2 = {anbncmdm| n1 y m1} es independiente del contexto, con la gramáticaSABAaAb|abBcBd|cdPor último, L’3 = {anbn| n1} es independiente del contexto, con la gramáticaSaSb|ab

Es conveniente saber que L3 es el típico lenguaje no definible por ninguna expresión regular.

Lenguajes y Autómatas I 20