Apéndice II. Contexto de las Reglas de ANTLR

Lectura Adicional Recomendada

🔎 Este apéndice no forma parte del material a evaluar

El contexto de una regla es el objeto Java que tiene cada regla de ANTLR para guardar su información. Se utiliza, entre otras cosas, para que una regla le pase información a otra.

Durante los capítulos anteriores, aunque no se ha nombrado expresamente, este objeto se ha estado usando constantemente (se han estado usando sus atributos sin nombrarlo). Para el uso de ANTLR básico no es necesario saber más de este objeto. Sin embargo, para el resto de los apéndices que vienen a continuación, que ahondan más en ANTLR, es necesario saber de la existencia de este objeto ya que se usará en todos ellos.

El Contexto de una Regla

Supóngase una regla de nombre abc:

abc: ...

Para dicha regla, ANTLR crearía dos cosas:

  1. Se crearía una clase llamada AbcContext, que será el contexto de esta regla, la cual tendrá atributos para guardar toda la información producida durante la ejecución de la regla abc (ahí estará, por ejemplo $ast). Es decir, se crea una clase Java por cada regla para guardar la información específica de dicha regla.
  2. Se crearía un método abc() en la clase GrammarParser (la clase en la que ANTLR genera el parser). Este método tendrá el código de reconocimiento de la regla (los match y demás llamadas) y, al acabar, devolverá un objeto de la clase AbcContext anterior (su contexto).

Por ejemplo, para el no-terminal expr

expr: ... ;

se crearía lo siguiente:

 




 










class ExprContext extends ParserRuleContext {
    ...
}

class GrammarParser {
    public ExprContext expr()  {
        ExprContext ctx = new ExprContext(...);
        ...
        <código para reconocer la regla...>
        ...

        return ctx;
    }
    ...
}

La clase ParserRuleContext, de la que deriva ExprContext, es generada por ANTLR y es el padre común de todos los contextos de las distintas reglas. Es decir, cuando se quiera tratar con un contexto sin importar de qué regla sea, este es el tipo que hay que utilizar (es como el Object de los contextos). Por ejemplo, si se quiere tener en una List con contextos de varias reglas, la lista debería ser de tipo List<ParserRuleContext>.

El contexto de una regla contendrá (entre otras cosas):

  • El atributo donde se dejará el valor de retorno de la regla (el habitual $ast).
  • Los objetos asociados a todos los símbolos del consecuente (parte derecha) de la regla: los tokens encontrados (si son terminales) o los objetos contexto devueltos (si son no-terminales).

Por ejemplo, una regla como la siguiente:

abc returns[String val]
	: IDENT b;

Generaría (simplificando) un contexto llamado AbcContext:

class AbcContext extends ParserRuleContext {
    public String val;      // Retorno
    public Token IDENT;     // Token IDENT que se encuentre en la entrada
    public BContext b;      // Contexto que devuelva la llamada a b()
}

Acceso a Atributos de la Regla

Para acceder a los atributos del contexto de la regla actual desde la propia regla basta con poner directamente $atributo:

abc returns[String val]
	: IDENT b { $val = "adios"; };

Si se quiere una referencia al objeto contexto (al objeto AbcContext), hay que poner $ctx.

abc returns[String val]
	: IDENT b { System.out.println($ctx); }; // Es un AbcContext

De hecho, cuando se accede a los atributos, ANTLR internamente está poniendo ctx antes de cada uno. Por tanto, las dos reglas siguientes son idénticas:

abc returns[String val]
	: IDENT b { $ctx.val = "adios"; };
abc returns[String val]
	: IDENT b { $val = "adios"; };  // ANTLR pone los "ctx." por nosotros

Acceso a Atributos de los Hijos

Ahora se tratará, en vez de cómo acceder a los atributos de la propia regla, el cómo acceder a los atributos de los símbolos que están en la parte derecha de la regla (los símbolos hijos de la regla).

La forma de acceder (y el valor obtenido), depende del tipo de símbolo. A continuación se verá:

Acceso a Terminales (tokens)

Supóngase la siguiente regla:

 

a: IDENT { /* ¿cómo se accede aquí al token IDENT? */ };

En este apartado se verá cómo acceder al token IDENT desde la acción Java del final de la regla.

Acceso Básico

La notación $<hijo>, cuando el hijo es un terminal como en este caso, devuelve un objeto de la clase Token generada por ANTLR que contiene toda la información encontrada en la entrada (lexema, tipo de token, línea y columna).

a: IDENT { System.out.println($IDENT); } // Objeto Token

El método más usado de dicha clase será getText() para obtener el lexema del token. Por ello, ANTLR permite el atajo de poner $símbolo.text y él lo sustituirá por $símbolo.getText().

a: IDENT { System.out.println($IDENT.text); } // Equivale a `$IDENT.getText()`

Repeticiones de un mismo Token

Si un token aparece varias veces en la regla, al usar su nombre sólo se accederá al último de ellos.

a: IDENT IDENT { System.out.println($IDENT); } // Segundo IDENT

Para acceder a cualquier otro token, hay dos opciones: índices y etiquetas.

Opción 1. Índices

Utilizando notación de índices, se puede acceder a todos los Tokens de los terminales del mismo nombre (empiezan en 0).

a: IDENT IDENT { System.out.println($IDENT(0)); } // Token del primer IDENT

En este caso, para acceder al lexema del token, ANTLR no permite usar el atajo $IDENT(0).text, por lo que para obtener éste hay que poner la expresión completa $IDENT(0).getText().

Opción 2. Etiquetas

La alternativa a usar índices es usar etiquetas que identifiquen el terminal al que nos referimos:

a: id1=IDENT id2=IDENT { System.out.println($id1.text); }

En este caso, se puede volver a usar el atajo $id1.text para obtener el lexema.

Terminales Anónimos

Para acceder a terminales anónimos (definidos entre comillas simples), hay que usar etiquetas. En este caso, se utiliza la etiqueta tk:

a: tk=('print'|'out') { System.out.println($tk.text); }  // Saldrá "print" o "out"

Acceso a No-terminales

Se verán ahora cómo acceder a los hijos cuando estos son no-terminales. Supóngase ahora las siguientes reglas:

 




a: b { /* ¿cómo se accede aquí a la información devuelta por 'b'? */ };

b returns[String val]
    : IDENT { $val = $IDENT.text; };

Ahora se verá cómo acceder al valor de retorno de b (variable val) desde la acción de la regla a.

Acceso Básico en No-terminales

Cuando el símbolo b es un no-terminal, en el parser que genera ANTLR, la llamada a b() devuelve su contexto. Para acceder a dicho objeto, siendo coherente con cómo se accede a los tokens, podría parecer que debería poder accederse mediante $b. Sin embargo, esto no es así (por algún motivo que ANTLR no especifica):

a: b { System.out.println($b); }	// Error. ANTLR no lo permite

En su lugar, para acceder a dicho contexto, hay que poner $b.ctx.

a: b { System.out.println($b.ctx); }	// BParserRuleContext

A la hora de acceder a un atributo de dicho contexto, se podrá poner $b.ctx.atributo o bien, lo que será más habitual, poner ya directamente el atributo sin poner ctx (ANTLR ya añadirá el prefijo ctx a dicho atributo).

// Caso más habitual
a: b { System.out.println($b.val); };    // Equivale a `$b.ctx.val`

b returns[String val]
    : IDENT { $val = $IDENT.text; };

En resumen:

  • Para acceder a un atributo de un hijo (que será el caso habitual):
    • Es válido tanto $b.<atributo> como $b.ctx.<atributo>.
  • Para el caso poco común de que se necesite acceder al objeto contexto completo de un hijo (lo que devolvió la llamada a su método):
    • Es válido $b.ctx.
    • No es válido $b.

Varias apariciones del mismo No-Terminal

Si en la parte derecha de una regla aparece varias veces el mismo no-terminal, mediante su nombre sólo se accede al contexto del último de ellos:

a: b b { System.out.println($b.val); }; // La segunda b

b returns[String val]
	: IDENT { $val = $IDENT.text; };

Para acceder al valor de retorno del primer b, hay dos opciones: índices y etiquetas.

Opción 1. Acceso con Índices

Utilizando notación de índices se pueden acceder a los contextos que devolvieron cada uno de los terminales del mismo nombre (empezando en 0).

a: b b { System.out.println($ctx.b(0)); }; // BContext de la primera `b`

Nótese que, a diferencia de con los terminales, es obligatorio poner $ctx antes del nombre del símbolo (por algún motivo que no explica, ANTLR4 no permite poner $b(0)).

Lo más habitual, más que acceder al contexto devuelto, será acceder directamente al atributo con el valor de retorno del hijo:

a: b b { System.out.println($ctx.b(0).val); }; // String

b returns[String val]
	: IDENT { $val = $IDENT.text; };

Debido a lo engorroso de esta opción, se recomienda no utilizarla y usar directamente etiquetas (la opción 2).

Además, esta primera opción tiene bugs que han sido reportados al autor de la herramienta, pero que aún no han sido corregidos. Para más info sobre los bugs detectados:

Opción 2. Acceso con Etiquetas

La opción 2 consiste en usar etiquetas con los no-terminales (al igual que se comentó con los terminales). Esta es la opción recomendada.

a: id1=b id2=b { System.out.println($id1.val); };