Capítulo 4. Análisis sintáctico

Lenguaje independiente del contexto.

"Parser". Gobierna todo el proceso. Realiza el resto de la reducción al axioma de la gramática para comprobar que la instrucción en cuestión es correcta. Considera como símbolos terminales las unidades sintácticas devueltas por el analizador morfológico.

Dos tipos principales de análisis:

Que sea una gramática de tipo 2 (lenguaje independiente del contexto) no nos asegura que el autómata a pila sea determinista. Estos sólo representan un subconjunto de los lenguajes i.c.. A lo largo de las páginas siguientes iremos imponiendo diversas restricciones a las gramáticas. Las restricciones de un método no tienen porqué ser las mismas que las de otro (complementariedad).

Análisis "top-down"

Se parte del axioma y se va realizando la reducción. La palabra x (normalmente una instrucción) se llama meta u objetivo del análisis. La primera fase del análisis consiste en encontrar, entre las reglas cuya parte izquierda es el axioma, la que conduce a x.

Análisis "top-down" con vuelta atrás lenta

Sea el axioma S y las reglas cuya parte izquierda es el axioma:

  S ::= X1 X2 ... Xn | Y1 Y2 ... Ym | ...

Sea x la palabra a reconocer. El objetivo del análisis es encontrar una derivación tal que

  S ->* x

Probaremos primero la regla

  S ::= X1 X2 ... Xn

Debemos descomponer x en la forma

  x = x1 x2 ... xn
y tratar de encontrar las derivaciones
  X1 ->* x1
  X2 ->* x2
     ...
  Xn ->* xn

Cada una de las derivaciones anteriores es una submeta. Pueden ocurrir los siguientes casos:

Análisis "top-down" con vuelta atrás rápida

Consiste en ordenar las distintas partes derechas de las reglas que comparten la misma parte izquierda de manera que la regla "buena" se analice antes.

Ejemplo:

  E ::= T+E | T
  T ::= F*T | F
  F ::= i

Analizamos la cadena i*i. Probamos primero E::=T+E y obtenemos el árbol

                E
         -------|-------
         T      +      E
       -----
       F * T
       |   |
       i   F
           |
           i

En cuanto comprobemos que el signo + no pertenece a la cadena objetivo, no es necesario volver a las fases precedentes del análisis tratando de obtener otra alternativa, sino que pasamos directamente a intentar E::=T.

         E
         |
         T
       -----
       F * T
       |   |
       i   F
           |
           i

Una buena regla para ordenar la gramática es poner primero las partes derechas más largas. Si una parte derecha es la cadena vacía, debe ser la última. Si se llega a ella, automáticamente hay éxito en la submeta.

Forma normal de Greibach

Ver "Teoría de Lenguajes, gramáticas y autómatas".

Lenguajes LL(1)

Son gramáticas en forma normal de Greibach en las que no existen dos reglas con la misma parte izquierda, cuya parte derecha empiece por el mismo símbolo terminal.

Conversión: si existen reglas de la forma:

   U ::= aV | aW
   V ::= bX | cY
   W ::= dZ | eT
tratamos de sustituir las dos reglas de parte izquierda U por
   U ::= aK
   K ::= bX | cY | dZ | eT
y aplicamos la misma sustitución a las nuevas reglas formadas.

La gramática se llama LL(1) de Knuth porque basta estudiar en cada momento un carácter de la cadena objetivo para saber qué regla se debe aplicar.

Análisis "top-down" selectivo

Se llama también "sin vuelta atrás" o "descenso recursivo". Se basa en poner la gramática en la forma normal de Greibach (LL(1)) y marchar directamente por el único camino permisible. Si en algún momento no se encuentra regla que aplicar, se puede generar directamente un mensaje de error.

Ejemplo: sea la gramática

  E := T + E | T - E | T
  T := F * T | F / T | F
  F ::= i | (E)

En forma normal de Greibach queda:

  E ::= iPTME | (ECPTME | iDTME | (ECDTME | iME | (ECME |
        iPTSE | (ECPTSE | iDTSE | (ECDTSE | iSE | (ECSE |
        iPT   | (ECPT   | iDT   | (ECDT   | i   | (EC
  T ::= iPT   | (ECPT   | iDT   | (ECDT   | i   | (EC
  F ::= i | (EC
  M ::= +
  S ::= -
  P ::= *
  D ::= /
  C ::= )

En forma LL(1) queda:

  E ::= iV  | (ECV
  V ::= *TX | /TX  | +E | -E | ^
  X ::= +E  | -E   | ^
  T ::= iU  | (ECU
  U ::= *T  | /T   | ^
  F ::= i   | (EC
  C ::= )

Paso automático de la gramática LL(1) al analizador

Dada una gramática LL(1), formada por un conjunto de reglas de la forma

  U ::= x X1 X2 ... Xn | y Y1 Y2 ... Ym | ... | z Z1 Z2 ... Zp

Generamos la siguiente función C:

  int U (char *cadena, int i)
  {
    if (i<0) return i;  /* Pasa errores anteriores */
    switch (cadena[i]) {
      case x:
        i++;
        i = X1 (cadena, i);
        i = X2 (cadena, i);
        . . .
        i = Xn (cadena, i);
        break;
      case y:
        i++;
        i = Y1 (cadena, i);
        i = Y2 (cadena, i);
        . . .
        i = Ym (cadena, i);
        break;
      ...
      case z:
        i++;
        i = Z1 (cadena, i);
        i = Z2 (cadena, i);
        . . .
        i = Zp (cadena, i);
        break;
      /* Fin de cadena va a default */
      default: return -n; /* Genera error n */
        }
    return i;
  }

Si las reglas tienen la forma

  U ::= x X1 X2 ... Xn | ... | z Z1 Z2 ... Zp | ^

Generamos la siguiente función C:

  int U (char *cadena, int i)
  {
    if (i<0) return i;  /* Pasa errores anteriores */
    switch (cadena[i]) {
      case x:
        i++;
        i = X1 (cadena, i);
        i = X2 (cadena, i);
        . . .
        i = Xn (cadena, i);
        break;
      ...
      case z:
        i++;
        i = Z1 (cadena, i);
        i = Z2 (cadena, i);
        . . .
        i = Zp (cadena, i);
        break;
        }
    return i;
  }

Ejemplo: la gramática anterior se reduce a:

  int E (char *cadena, int i)
  {
    if (i<0) return i;
    switch (cadena[i]) {
      case 'i':
        i++;
        i = V (cadena, i);
        break;
      case '(':
        i++;
        i = E (cadena, i);
        i = C (cadena, i);
        i = V (cadena, i);
        break;
      default: return -1;
        }
    return i;
  }

  int V (char *cadena, int i)
  {
    if (i<0) return i;
    switch (cadena[i]) {
      case '*':
      case '/':
        i++;
        i = T (cadena, i);
        i = X (cadena, i);
        break;
      case '+':
      case '-':
        i++;
        i = E (cadena, i);
        break;
        }
    return i;
  }

  int X (char *cadena, int i)
  {
    if (i<0) return i;
    switch (cadena[i]) {
      case '+':
      case '-':
        i++;
        i = E (cadena, i);
        break;
        }
    return i;
  }

  int T (char *cadena, int i)
  {
    if (i<0) return i;
    switch (cadena[i]) {
      case 'i':
        i++;
        i = U (cadena, i);
        break;
      case '(':
        i++;
        i = E (cadena, i);
        i = C (cadena, i);
        i = U (cadena, i);
        break;
      default: return -2;
        }
    return i;
  }

  int U (char *cadena, int i)
  {
    if (i<0) return i;
    switch (cadena[i]) {
      case '*':
      case '/':
        i++;
        i = T (cadena, i);
        break;
        }
    return i;
  }

  int F (char *cadena, int i)
  {
    if (i<0) return i;
    switch (cadena[i]) {
      case 'i':
        i++;
        break;
      case '(':
        i++;
        i = E (cadena, i);
        i = C (cadena, i);
        break;
      default: return -3;
        }
    return i;
  }

  int C (char *cadena, int i)
  {
    if (i<0) return i;
    switch (cadena[i]) {
      case ')':
        i++;
        break;
      default: return -4;
        }
    return i;
  }

La cadena x se analiza invocando

   axioma (x, 0);

En nuestro ejemplo:

   E (x, 0);

Si devuelve strlen(x), la cadena x es correcta. En caso contrario, devolverá un número negativo que indica el error detectado.

Ejemplo: análisis de "i+i*i".

  E ("i+i*i", 0) = V ("i+i*i", 1) =
                 = E ("i+i*i", 2) =
                 = V ("i+i*i", 3) =
                 = X ("i+i*i", T ("i+i*i", 4)) =
                 = X ("i+i*i", U ("i+i*i", 5)) =
                 = X ("i+i*i", 5) =
                 = 5

Ejemplo: análisis de "i+i*".

  E ("i+i*", 0) = V ("i+i*", 1) =
                = E ("i+i*", 2) =
                = V ("i+i*", 3) =
                = X ("i+i*", T ("i+i*", 4)) =
                = -2

Análisis "bottom-up"

Parte del objetivo y trata de reconstruir las producciones para llegar a reducir al axioma.

Ejemplo: sea la gramática

  E ::= E + T | E - T | T
  T ::= T * F | T / F | F
  F ::= i | (E)
y la expresión (reducida por el analizador morfológico) i+i*i.

Colocamos un puntero sobre la cadena a analizar.

   meta: E

   i + i * i
   ^

La única regla que empieza por "i" es F::=i. La aplicamos:

   meta: E

   F
   |
   i + i * i
   ^

La cadena a obtener (subobjetivo), vista desde arriba, es F+i*i. La única regla aplicable (que empiece por F) es T::=F. La aplicamos:

   meta: E

   T
   |
   F
   |
   i + i * i
   ^

La cadena a obtener (subobjetivo), vista desde arriba, es T+i*i. Hay dos reglas cuya parte derecha empieza por T, E::=T y T::=T*F. Probamos la de parte derecha más larga (la segunda): regla aplicable (que empiece por F) es T::=F. La aplicamos:

   meta: E

   T
   |----
   T * F
   |
   F
   |
   i + i * i
     ^
y avanzamos el puntero para comprobar. La submeta a encontrar es *F y la subcadena a generar es +i*i. Como comienzan por distinto símbolo terminal, no es posible hacerlo. Retrocedemos a la situación anterior y aplicamos la otra regla, E::=T.
   meta: E

   E
   |
   T
   |
   F
   |
   i + i * i
   ^

El objetivo actual es E+i*i. Ha aparecido el axioma, pero queda cadena por reducir. Sólo hay una regla cuya parte derecha empiece por E, E::=E+T. La aplicamos:

   meta: E

   E
   |----
   E + T
   |
   T
   |
   F
   |
   i + i * i
     ^

Ahora tenemos el subobjetivo +T que debe producir +i*i. Los dos símbolos terminales iniciales coinciden. Los identificamos y avanzamos el puntero.

   meta: E

   E
   |----
   E | T
   | | Submeta: T
   T |
   | |
   F |
   | |
   i + i * i
       ^

Partiendo de i*i hay que comprobar la submeta T. La única regla que empieza por "i" es F::=i. La aplicamos:

   meta: E

   E
   |----
   E | T
   | | Submeta: T
   T |
   | |
   F | F
   | | |
   i + i * i
       ^

La única regla que empieza por F es T::=F. La aplicamos:

   meta: E

   E
   |----
   E | T
   | | Submeta: T
   T | T
   | | |
   F | F
   | | |
   i + i * i
       ^

Hemos encontrado la submeta T. Pero aún queda cadena. Miramos si hay alguna regla cuya parte derecha empiece por T. Hay dos, E::=T y T::=T*F. Probamos la de parte derecha más larga (la segunda).

   meta: E

   E
   |----
   E | T
   | | Submeta: T
   | | T
   | | |----
   T | T * F
   | | |
   F | F
   | | |
   i + i * i
         ^

Avanzamos el puntero y comprobamos la subcadena *F con la subcadena *i. Los dos símbolos terminales iniciales coinciden. Los identificamos y avanzamos:

   meta: E

   E
   |----
   E | T
   | | Submeta: T
   | | T
   | | |----
   T | T | F
   | | | | submeta: F
   F | F |
   | | | |
   i + i * i
           ^

Partiendo de i hay que comprobar la submeta F. La única regla que empieza por "i" es F::=i. La aplicamos:

   meta: E

   E
   |----
   E | T
   | | Submeta: T
   | | T
   | | |----
   T | T | F
   | | | | Submeta: F
   F | F | F
   | | | | |
   i + i * i
           ^

Todas las metas y submetas coinciden. Luego ésta es la reducción al axioma buscada. El árbol de análisis queda:

   E
   |----
   E | T
   | | |----
   T | T | F
   | | | | |
   F | F | |
   | | | | |
   i + i * i

Gramáticas de precedencia simple

Introducir aquí la teoría de formas sentenciales, frases y asideros, y la de relaciones.

Relaciones de precedencia

Dada una gramática limpia G de axioma Z, definimos las siguientes relaciones de precedencia entre los símbolos del vocabulario A = AN U AT. Para todo R,S en A:

   R =. S <=> existe U::=uRSv  en  P
   R <. S <=> existe U::=uRTv  en  P,
              tal que T F+ S en P.
   R >. S <=> existe U::=uTWv  en  P,
              tal que T L+ R, W F* S en P.
   R <.= S <=> R <. S ó R =. S
   R >.= S <=> R >. S ó R =. S

Teorema: R =. S <=> RS aparece en el asidero de alguna forma sentencial.

Prueba:

Teorema: R >. S <=> existe una forma sentencial uRSv donde R es el símbolo final de un asidero.

Prueba:

Teorema: R <. S <=> existe una forma sentencial uRSv donde S es la cabeza de un asidero.

Se demuestra de forma análoga.

Gramática de precedencia simple

Definición: G es una G.P.S. o G.P.(1,1) si:

  1. Sólo existe, como mucho, una relación de precedencia entre dos símbolos cualesquiera del vocabulario.
  2. No existen dos producciones con la misma parte derecha.

Se llama G.P.(1,1) porque sólo se usa un símbolo a cada lado de un posible asidero para decidir si lo es o no.

Problema: si no se cumple esto, se puede manipular la gramática para intentar que se cumpla. La recursividad a izquierdas o a derechas da problemas (salen dos relaciones de precedencia con los símbolos que van antes o después del recursivo). Para evitarlo, se puede estratificar la gramática, añadiendo símbolos nuevos, pero si hay muchas reglas es muy pesado.

Supondremos que toda forma sentencial queda rodeada por dos símbolos especiales de principio y fin de cadena: |- -|, que no están en A y tales que, para todo S en A, |- <. S y S >. -|.

Teorema: Una gramática de precedencia simple no es ambigua. Además, el asidero de una forma sentencial S1 ... Sn es la subcadena Si ... Sj, situada más a la izquierda tal que:

     Si-1 <. Si =. Si+1 =. ... =. Sj-1 =. Sj >. Sj+1

Prueba:

Como el asidero es único (por construcción) y sólo puede ser parte derecha de una regla (por ser G.P.S.) sólo se puede aplicar una regla para reducir. Luego la gramática no es ambigua.

Construcción de las relaciones

  1. La relación =. se construye por simple observación de las partes derechas de las reglas (definición de =.).
  2. La relación <. se construye así:
      <. = (=.) +.x (F+)
    
    donde +.x es el producto booleano de matrices. (Por definición del producto de relaciones y la definición de <. y =.).
  3. La relación >. se construye así:
      >. = (L+)' +.x (=.) +.x (F*)
    
    donde ' es la transposición de matrices. La demostración queda como ejercicio.

Algoritmo de análisis

  1. Se almacenan las producciones de la gramática en una tabla.
  2. Se construye la matriz de precedencia MP de dimensiones N.N (N=cardinal de A), tal que
      MP(i,j) = 0 si no existe relación entre Si y Sj
              = 1 si Si <. Sj
              = 2 si Si =. Sj
              = 3 si Si >. Sj
    
  3. Se inicializa una pila con el símbolo |- y se añade el símbolo -| al final de la cadena de entrada.
  4. Se compara el símbolo de lo alto de la pila con el siguiente símbolo de entrada.
  5. Si no hay relación, error sintáctico: cadena rechazada.
  6. Si hay la relación <. o la relación =., se introduce el símbolo de entrada en la pila y se avanza el puntero sobre la entrada. Volver al paso 4.
  7. Si la relación es >., el asidero termina en lo alto de la pila.
  8. Se recupera el asidero de la pila, sacando símbolos hasta que el símbolo en lo alto de la pila está en relación <. con el último sacado.
  9. Se compara el asidero con las partes derechas de las reglas.
  10. Si no está, error sintáctico: cadena rechazada.
  11. Si está, se introduce la parte izquierda de la regla en la pila.
  12. Si en la pila sólo queda |- seguido del axioma y la cadena de entrada ha quedado reducida al símbolo -|, la cadena ha sido reconocida.
  13. Volver al paso 4.

Ejemplo:

  S ::= aSb | c

Matrices de las relaciones:

  =. : 0 0 1 0    F  : 0 1 0 1   L : 0 0 1 1
       1 0 0 0         0 0 0 0       0 0 0 0
       0 0 0 0         0 0 0 0       0 0 0 0
       0 0 0 0         0 0 0 0       0 0 0 0
  F+ = F
  L+ = L
  F* : 1 1 0 1
       0 1 0 0
       0 0 1 0
       0 0 0 1

Operando, sale:

  <. : 0 0 0 0    >. : 0 0 0 0
       0 1 0 1         0 0 0 0
       0 0 0 0         0 0 1 0
       0 0 0 0         0 0 1 0

Con lo que la matriz de precedencia queda:

       S a b c -|
      ----------
    S |    =   >
    a |= <   < >
    b |    >   >
    c |    >   >
    |-|< < < <

Analicemos la instrucción: aacbb

        Pila            Entrada
        ------------    ------------
        |-            < aacbb-|
        |-<a          < acbb-|
        |-<a<a        < cbb-|
        |-<a<a<c      > bb-|
        |-<a<a=S      = bb-|
        |-<a<a=S=b    > b-|
        |-<a=S        = b-|
        |-<a=S=b      > -|
        |-<S            -|

Luego la cadena es aceptada.

Analicemos la instrucción: aabb

        Pila            Entrada
        ------------    ------------
        |-            < aabb-|
        |-<a          < abb-|
        |-<a<a          bb-|     (no hay relación)

Luego la cadena es rechazada.

Analicemos la instrucción: acbb

        Pila            Entrada
        ------------    ------------
        |-            < acbb-|
        |-<a          < cbb-|
        |-<a<c        > bb-|
        |-<a=S        = bb-|
        |-<a=S=b      > b-|
        |-<S          = b-|
        |-<S=b        > -|       (no hay regla)

Luego la cadena es rechazada.

Funciones de precedencia

La matriz de precedencias ocupa NxN. A veces se puede construir dos funciones de precedencia que ocupan sólo 2xN. (Linealización de la matriz). Estas funciones, de existir, no son únicas (hay infinitas).

Para el ejemplo anterior valen:

        |-  S  a  b  c  -|
    f   0  2  2  3  3
    g      2  3  2  3  0
Si se pueden construir f y g, se verifica que

Existen matrices no linearizables. Ejemplo:

    A B
 A  = >
 B  = =
pues debería ser
   f(A) = g(A)
   f(A) > g(B)
   f(B) = g(A)
   f(B) = g(B)

Es decir:

   f(A) > g(B) = f(B) = g(A) = f(A) => f(A) > f(A)

¡Contradicción!

Construcción de las funciones, cuando no hay inconsistencias:

Prueba:

Ejemplo: grafo para obtener las funciones de precedencia del ejemplo anterior:

    fS <-- --> gS
         | |
    fa <-|-|-- ga
     ^<--|  |
     |---|--|
        ||----->
    fb -|----> gb
        |      ^
       -|------|
    fc ||----- gc

Mecanización del cálculo anterior: un grafo equivale a la matriz booleana de la relación xRy<=>hay un arco del nodo x al nodo y. Representamos el grafo por la matriz B 2Nx2N:

    (0)     (>.=)
    (<.=)'  (0)

Construimos B*. f(Si)=número de unos en la fila i. g(Si)=número de unos en la fila N+i.

En el ejemplo, B es la matriz

    0 0 0 0 0 0 1 0
    0 0 0 0 1 0 0 0
    0 0 0 0 0 0 1 0
    0 0 0 0 0 0 1 0
    0 1 0 0 0 0 0 0
    0 1 0 0 0 0 0 0
    1 0 0 0 0 0 0 0
    0 1 0 0 0 0 0 0

B* es la matriz

    1 0 0 0 0 0 1 0
    0 1 0 0 1 0 0 0
    1 0 1 0 0 0 1 0
    1 0 0 1 0 0 1 0
    0 1 0 0 1 0 0 0
    0 1 0 0 1 1 0 0
    1 0 0 0 0 0 1 0
    0 1 0 0 1 0 0 1
y las dos funciones f, g, salen como se dijo antes. Basta añadir los símbolos de principio y fin de cadena (con valor 0).

El uso de las funciones supone pérdida de información. Por ejemplo:

        Pila            Entrada
        ------------    ------------
        |-            < aabb-|
        |-<a          < abb-|
        |-<a<a        = bb-|
        |-<a<a=b      > b-|      (no hay regla)

Ha habido que hacer un paso más.

Gramáticas de operadores

Tienen la siguiente restricción: en su conjunto de producciones no existe una regla de la forma:

   U ::= xVWy
donde V,W en AN. En estas gramáticas, los símbolos terminales se llaman operadores, los no terminales operandos.

Relaciones de precedencia de operadores

Dada una gramática limpia de operadores, definimos las siguientes relaciones de precedencia entre los operadores: Para todo R,S en AT:

   R =. S <=> existe U::=uRSv  en  P
            o existe U::=uRVSv  en  P, V en AN.
   R <. S <=> existe U::=uRTv  en  P, T en AN,
              tal que T =>+ Sx
                    o T =>+ VSx, V en AN.
   R >. S <=> existe U::=uTSv  en  P, T en AN,
              tal que T =>+ xR
                    o T =>+ xRV, V en AN.
   R <.= S <=> R <. S ó R =. S
   R >.= S <=> R >. S ó R =. S

Gramática de precedencia de operadores

Definición: G es una G.P.O. si:

  1. G es una gramática de operadores.
  2. Sólo existe, como mucho, una relación de precedencia entre dos operadores cualesquiera.

Construcción de las relaciones

  1. La relación =. se construye por simple observación de las partes derechas de las reglas (definición de =.).
  2. Definimos la relación FO ("First for Operators") así:

    U FO S <=> U::=Sx ó U::=VSx en P, U,V en AN, S en AT.

  3. Definimos la relación LO ("Last for Operators") así:

    U LO S <=> U::=xS ó U::=xSV en P, U,V en AN, S en AT.

    Las dos relaciones anteriores se definen como matrices del mismo tamaño que las relaciones F y L, ya conocidas.

  4. La relación <. se construye así:
      <. = (=.S) +.x (F*) +.x (FO)
    
    donde +.x es el producto booleano de matrices y =.S es la relación =., tal como se definió para las gramáticas de precedencia simple.
  5. La relación >. se construye así:
      >. = ((L*) +.x (LO))' +.x (=.S)
    
    donde ' es la transposición de matrices.

Algoritmo de análisis

El equivalente del asidero se llama frase primaria. Es una frase que contiene al menos un símbolo terminal, pero no una frase menor que ella. Un teorema nos asegura que para toda frase primaria Si...Sj se cumple que

     Si-1 <. Si =. Si+1 =. ... =. Sj-1 =. Sj >. Sj+1

Para reducir hacia el axioma tomamos la frase primaria situada más a la izquierda en la forma sentencial.

Ejemplo:

  E ::= E+T | T
  T ::= T*F | F
  F ::= (E) | i | -F

La matriz de relaciones es:

       + * ( ) -
   +   > < < > <
   *   > > < > <
   (   < < < = <
   )   > >   >
   -   > > < > <

El algoritmo es como el de las gramáticas de precedencia simple, pero se ignoran los símbolos no terminales.

Vamos a ver un algoritmo reducido que se emplea para el análisis y el cálculo de expresiones:

El algoritmo utiliza dos pilas: una de operadores, otra de operandos. La ventaja principal de estas gramáticas es que la matriz de las relaciones es mucho más pequeña.

  1. Se almacenan las producciones de la gramática en una tabla.
  2. Se construye la matriz de precedencia de operadores, MP de dimensiones N.N (N=cardinal de AT), tal que
      MP(i,j) = 0 si no existe relación entre Si y Sj
              = 1 si Si <. Sj
              = 2 si Si =. Sj
              = 3 si Si >. Sj
    
  3. Se inicializa la pila de operadores con el símbolo |- y se añade el símbolo -| al final de la cadena de entrada. La pila de operandos está inicialmente vacía.
  4. Si el próximo símbolo de entrada es un identificador, se introduce en la pila de operandos. En caso contrario, se compara la precedencia del operador que esta en lo alto de la pila de operadores con el próximo operador.
  5. Si no hay relación, error sintáctico: cadena rechazada.
  6. Si los símbolos son ( y ), eliminar ambos.
  7. Si hay la relación <. o la relación =., se introduce el símbolo de entrada en la pila de operadores y se avanza el puntero sobre la entrada. Volver al paso 4.
  8. Si la relación es >., se llama la rutina semántica asociada al símbolo situado en lo alto de la pila, eliminándolo de ésta. Se eliminan también los operandos asociados y se introduce en la pila de operandos el resultado de la ejecución de la rutina semántica.
  9. Si en la pila de operadores sólo queda |-, la cadena de entrada ha quedado reducida al símbolo -|, y la pila de operandos contiene un solo valor, la cadena ha quedado reconocida y el resultado de la expresión está en la pila de operandos.
  10. Volver al paso 4.

Ejemplo: análisis de a*(b+c).

           a*(b+c)-|
 ||-                    |
         < *(b+c)-|
 ||-                   a-|
         < (b+c)-|
 ||-*                  a-|
           b+c)-|
 ||-*(                 a-|
         < +c)-|
 ||-*(                ba-|
           c)-|
 ||-*(+               ba-|
         > )-|
 ||-*(+              cba-|
           )-|
 ||-*(                Pa-|     P=b+c
         > -|
 ||-*                 Pa-|
           -|
 ||-                   Q-|     Q=a*P

Este algoritmo tiene problemas, como no localizar los errores en instrucciones como "a b +", "+b-c", etc. Se puede añadir una prueba para comprobar que no hay dos operandos seguidos, ni un operando seguido por función monádica, o se puede pasar a un algoritmo semejante al de las gramáticas de precedencia simple.

Gramáticas LR(k)

Informalmente, una gramática LR(k) (Knuth, 1965) permite analizar de forma determinista, de izquierda a derecha, una cadena de entrada, observando a lo sumo los siguientes elementos:

Los analizadores basados en gramáticas LR(k) presentan características comunes con los analizadores "top-down" y "bottom-up".

Configuración

Es una producción con un marcador, que indica hasta donde hemos llegado en el análisis de la parte derecha. Por ejemplo, sea la gramática parcial

1:  <Bloque> ::= begin <Decs> ; <Ejecs> end
2:  <Decs>   ::= <Dec>
3:            |  <Decs> ; <Dec>
4:  <Ejecs>  ::= <Ejec>
5:            |  <Ejec> ; <Ejecs>

Son configuraciones válidas las siguientes:

    (1,0)
    (2,1)
    (3,3)
donde (a,b) indica que estamos analizando la posición b (en origen 0) de la parte derecha de la regla de producción número a.

Estado

Se representa mediante un conjunto de configuraciones. Indica las configuraciones posibles en la situación actual del análisis. Su número es finito.

Supongamos que estamos en la configuración representada por (a,b), y que el símbolo siguiente a la posición b en la parte derecha de la regla a es el símbolo no terminal U. Sean c,...,d las reglas cuya parte izquierda es U. Entonces, de la configuración (a,b) se podrá pasar a las configuraciones (c,0),...,(d,0). Si aplicamos el mismo procedimiento indefinidamente, hasta que no aparezcan configuraciones nuevas, tendremos la "clausura" de la configuración (a,b): el conjunto de estados que han ido apareciendo, incluido el (a,b). Si partimos de un conjunto de configuraciones (un estado), la clausura correspondiente será igual a la unión de las clausuras de las configuraciones del estado.

Estado inicial

Para preparar el análisis LR(k) de una gramática hacemos inicialmente dos cosas:

Operación corrimiento

Dado un estado, se puede pasar al estado sucesor mediante la operación "corrimiento", que consiste en avanzar un símbolo sobre la cadena de entrada, pasando además del estado actual a otro nuevo, que se obtiene así:

Obsérvese que en la cadena de entrada podemos encontrar símbolos terminales o no terminales, como veremos a continuación.

Operación reducción

Cuando una configuración representa el final de la parte derecha de una regla, se puede aplicar esa regla, añadiendo su parte izquierda a la izquierda de la cadena de entrada, eliminando de la lista de estados por los que se ha pasado los que corresponden a la parte derecha de la regla aplicada, y volviendo al estado anterior al comienzo del análisis de dicha regla.

Tabla del análisis

La tabla del análisis tiene tantas filas como estados accesibles a partir del inicial, y tantas columnas como símbolos del vocabulario.

Para construir la tabla del análisis, partimos del estado inicial y calculamos los estados accesibles a partir de él, los estados accesibles a partir de éstos, etc., hasta que no salgan estado nuevos, utilizando únicamente la operación corrimiento. A continuación, rellenamos algunas casillas con la operación reducción, utilizando para ello una regla que veremos después.

Además, pondremos "Fin" en la casilla cuya fila es el estado inicial y cuya columna es el axioma. Esto sólo podremos hacerlo en algunas gramáticas, en aquéllas en las que el axioma no puede aparecer en ninguna forma sentencial. Más adelante veremos cómo se actúa cuando esto no es cierto.

Ejemplo

Sea el ejemplo de la gramática que hemos visto antes. Consideraremos terminales a los símbolos <Dec> y <Ejec>.

Nuestra tabla inicial es:

        Bloque Decs Ejecs Dec Ejec begin  ;  end  -|
  S0      Fin
donde S0=clausura {(0,0)} = {(0,0),(1,0)}.

En el estado inicial, ignoraremos la configuración (0,0), cuya casilla ya está rellena. La otra, (1,0), es anterior a un símbolo terminal: "begin". Luego el único estado accesible desde el inicial es la clausura de {(1,1)} = {(1,1),(2,0),(3,0)} = S1. La tabla queda:

        Bloque Decs Ejecs Dec Ejec begin  ;  end  -|
  S0      Fin                        S1
  S1

A partir de S1 podemos encontrar en la cadena de entrada los símbolos <Dec> y <Decs>. <Dec> nos pasa a la configuración (2,1), <Decs> a la {(1,2),(3,1)}. Tendremos, pues, los estados S2=clausura {(2,1)}={(2,1)} y S3=clausura {(1,2),(3,1)}={(1,2),(3,1)}. La tabla queda:

        Bloque Decs Ejecs Dec Ejec begin  ;  end  -|
  S0      Fin                        S1
  S1            S3         S2
  S2
  S3

Desde S2 ya no se puede seguir: es fin de regla. Desde S3 sí. En ambos casos, el símbolo siguiente sólo puede ser ";". Las configuraciones siguientes son: {(1,3),(3,2)}. El estado siguiente será su clausura: S4={(1,3),(3,2),(4,0),(5,0)}. La tabla queda:

        Bloque Decs Ejecs Dec Ejec begin  ;  end  -|
  S0      Fin                        S1
  S1            S3         S2
  S2
  S3                                      S4
  S4

Partiendo de S4, podemos encontrar los símbolos <Ejecs>, <Dec> y <Ejec>. Tendremos, por tanto, tres transiciones posibles. La primera nos lleva a {(1,4)}, cuya clausura es él mismo. Lo llamaremos S5. La segunda nos lleva a {(3,3)}, cuya clausura es él mismo. Lo llamaremos S6. La tercera nos lleva a {(4,1),(5,1)}, cuya clausura es él mismo. Lo llamaremos S7. La tabla queda:

        Bloque Decs Ejecs Dec Ejec begin  ;  end  -|
  S0      Fin                        S1
  S1            S3         S2
  S2
  S3                                      S4
  S4                  S5   S6  S7
  S5
  S6
  S7

Desde S5 sólo podemos encontrar el símbolo "end", que nos lleva a {(1,5)}, cuya clausura es él mismo. Lo llamamos S8. Desde S6 no podemos llegar a ningún sitio: es fin de regla. Desde S7 podemos encontar el símbolo ";" y llegar a {(5,2)}, cuya clausura es {(5,2),(4,0),(5,0)}, un estado nuevo. Lo llamamos S9. La tabla queda:

        Bloque Decs Ejecs Dec Ejec begin  ;  end  -|
  S0      Fin                        S1
  S1            S3         S2
  S2
  S3                                      S4
  S4                  S5   S6  S7
  S5                                          S8
  S6
  S7                                      S9
  S8
  S9

Desde S8 no podemos seguir: es fin de regla. Desde S9 podemos encontrar <Ejecs> o <Ejec>. El primero nos lleva a {(5,3)}, cuya clausura es él mismo. Lo llamamos S10. El segundo nos lleva a {(4,1),(5,1)}, es decir, a S7. La tabla queda:

        Bloque Decs Ejecs Dec Ejec begin  ;  end  -|
  S0      Fin                        S1
  S1            S3         S2
  S2
  S3                                      S4
  S4                  S5   S6  S7
  S5                                          S8
  S6
  S7                                      S9
  S8
  S9                  S10      S7
  S10

Desde S10 no podemos seguir: es fin de regla. Por tanto, ya no hay más estados. La tabla está completa, salvo por la operación reducción.

Resumiendo: los estados encontrados son:

    S0   {(0,0),(1,0)}
    S1   {(1,1),(2,0),(3,0)}
   *S2   {(2,1)}
    S3   {(1,2),(3,1)}
    S4   {(1,3),(3,2),(4,0),(5,0)}
    S5   {(1,4)}
   *S6   {(3,3)}
   *S7   {(4,1),(5,1)}
   *S8   {(1,5)}
    S9   {(5,2),(4,0),(5,0)}
   *S10  {(5,3)}
donde los estados que contienen una configuración final de regla aparecen señalados con un asterisco.

Reglas para la operación reducción

Dependiendo de la regla utilizada para rellenar algunas casillas de la tabla con la operación reducción, tendremos distintos tipos de familias de analizadores LR(k).

Analizadores LR(0)

La regla es: "si un estado contiene configuraciones de final de regla, reduciremos esa regla en todas las casillas correspondentes a un símbolo terminal".

La aplicación de la regla anterior puede provocar conflictos de dos tipos:

La aplicación de esta regla al ejemplo anterior nos daría la siguiente tabla:

        Bloque Decs Ejecs Dec Ejec begin  ;  end  -|
  S0      Fin                        S1
  S1            S3         S2
  S2                       R2  R2    R2   R2  R2  R2
  S3                                      S4
  S4                  S5   S6  S7
  S5                                          S8
  S6                       R3  R3    R3   R3  R3  R3
  S7                       R4  R4    R4 R4/S9 R4  R4
  S8                       R1  R1    R1   R1  R1  R1
  S9                  S10      S7
  S10                      R5  R5    R5   R5  R5  R5

Tenemos, pues un conflicto corrimiento-reducción. Por tanto, esta gramática no es LR(0).

Si al aplicar la regla anterior no hay ningún conflicto, decimos que la gramática está en forma LR(0). Por ejemplo, sea la gramática:

 1:  E ::= E + T
 2:  E ::= T
 3:  T ::= i
 4:  T ::= ( E )

Añadimos la regla 0:

 0:  S ::= E -|

Aplicando la técnica anterior, obtenemos la siguiente tabla de análisis:

            E    T     i    +   (     )  -|
  S0       S1   S8    S4       S5
  S1                       S2           Fin
  S2            S3    S4       S5
  S3                  R1   R1  R1    R1  R1
  S4                  R3   R3  R3    R3  R3
  S5       S6   S8    S4       S5
  S6                       S2        S7
  S7                  R4   R4  R4    R4  R4
  S8                  R2   R2  R2    R2  R2

Obsérvese que, en este caso, el axioma sí puede aparecer como parte de una forma sentencial, por lo que hemos tenido que introducir un estado más (S1), tratando la configuración (0,0) como otra cualquiera. La situación de aceptación, señalada por Fin, tiene lugar en la configuración (0,1), que aparece en dicho estado nuevo cuando se localiza el símbolo de fin de cadena.

Esta gramática deja de ser LR(0) si se introduce la precedencia de operadores. También surgen conflictos si existen reglas que generan la cadena vacía, especialmente cuando hay otras reglas con la misma parte izquierda.

Analizadores SLR(1)

La regla es: "si un estado contiene configuraciones de final de regla, reduciremos esa regla en todas las casillas correspondentes a un símbolo terminal que aparezca detrás de la parte izquierda de esa regla (directamente o después de alguna derivación) en la parte derecha de otra".

La aplicación de esta regla al ejemplo anterior nos daría la siguiente tabla:

        Bloque Decs Ejecs Dec Ejec begin  ;  end  -|
  S0      Fin                        S1
  S1            S3         S2
  S2                                      R2
  S3                                      S4
  S4                  S5   S6  S7
  S5                                          S8
  S6                                      R3
  S7                                      S9  R4
  S8                                              R1
  S9                  S10      S7
  S10                                         R5

Si al aplicar la regla anterior no hay ningún conflicto, decimos que la gramática está en forma SLR(1). Para cualquier lenguaje independiente del contexto determinista existe una gramática SLR(1) que lo representa.

En las gramáticas LR(1), cada configuración se incrementa con información sobre los símbolos de entrada que pueden venir a continuación. Esto complica los estados y las transiciones, pero en general no es necesario. Se demuestra que todo lenguaje representable mediante una gramática LR(1) también puede representarse mediante una gramática SLR(1).

Relaciones entre lenguajes

             Independientes del contexto (Chomsky-tipo 2)
                         |
             Ind. contexto deterministas = SLR(1) = LR(1)
       -------------------------------------
     LL(k)                            Precedencia simple
       |                                   |
     LL(1)                         Precedencia operadores

Análisis

Para realizar el análisis, aplicaremos el siguiente algoritmo:

  1. Se construye la tabla del análisis, como se ha indicado.
  2. Se añade a la cadena de entrada el símbolo final.
  3. Se construye una pila de estados, inicializada al estado inicial.
  4. En cada paso, el estado actual en que se encuentra el análisis es el que está en lo alto de la pila. Se indexa la tabla del análisis, por filas por el estado actual, por columnas por el primer símbolo de la cadena de entrada, para hallar la acción a realizar.
  5. Si es un corrimiento, eliminamos el primer símbolo de la cadena de entrada, introducimos el nuevo estado en lo alto de la pila y volvemos al paso 4.
  6. Si es una reducción, eliminamos de la pila tantos estados como símbolos haya en la parte derecha de la regla a aplicar, añadimos la parte izquierda de la regla a la izquierda de la cadena de entrada y volvemos al paso 4.
  7. Si es Fin, la cadena de entrada estará constituida por el axioma de la gramática, seguido por el símbolo final de cadena. La cadena inicial ha sido aceptada (es correcta).
  8. Si está en blanco, la cadena inicial de entrada es errónea.

Utilizaremos la tabla y la gramática de nuestro ejemplo para analizar la cadena de entrada:

   begin <Dec> ; <Dec> ; <Ejec> ; <Ejec> end

Partimos del estado inicial S0 = {(0,0),(1,0)}. La sucesión de estados por los que pasamos es:


  Estados  Cadena de entrada                            Acción
  -------  -------------------------------------------- ------
  S0       begin <Dec> ; <Dec> ; <Ejec> ; <Ejec> end -| S1

  S1       <Dec> ; <Dec> ; <Ejec> ; <Ejec> end -|       S2
  S0

  S2       ; <Dec> ; <Ejec> ; <Ejec> end -|             R2
  S1
  S0

  S1       <Decs> ; <Dec> ; <Ejec> ; <Ejec> end -|      S3
  S0

  S3       ; <Dec> ; <Ejec> ; <Ejec> end -|             S4
  S1
  S0

  S4       <Dec> ; <Ejec> ; <Ejec> end -|               S6
  S3
  S1
  S0

  S6       ; <Ejec> ; <Ejec> end -|                     R3
  S4
  S3
  S1
  S0

  S1       <Decs> ; <Ejec> ; <Ejec> end -|              S3
  S0

  S3       ; <Ejec> ; <Ejec> end -|                     S4
  S1
  S0

  S4       <Ejec> ; <Ejec> end -|                       S7
  S3
  S1
  S0

  S7       ; <Ejec> end -|                              S9
  S4
  S3
  S1
  S0

  S9       <Ejec> end -|                                S7
  S7
  S4
  S3
  S1
  S0

  S7       end -|                                       R4
  S9
  S7
  S4
  S3
  S1
  S0

  S9       <Ejecs> end -|                               S10
  S7
  S4
  S3
  S1
  S0

  S10      end -|                                       R5
  S9
  S7
  S4
  S3
  S1
  S0

  S4       <Ejecs> end -|                               S5
  S3
  S1
  S0

  S5       end -|                                       S8
  S4
  S3
  S1
  S0

  S8       -|                                           R1
  S5
  S4
  S3
  S1
  S0

  S0       <Bloque> -|                                  Fin