Apéndice III. Opcionalidad en ANTLR4

Lectura Adicional Recomendada

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

Cuando a un símbolo se le aplica el operador '?', si se accede a dicho símbolo en una acción Java, ¿qué valor se obtiene?

s: a? { ... $a ... };   // ¿Qué contiene $a?

En este apéndice se verán los valores obtenidos en el acceso a símbolos a los que se haya aplicado dicho operador y un análisis de las opciones que se tienen para tratar con ellos en las acciones Java.

Valor del operador '?'

Cuando se accede a un símbolo con '?', el valor obtenido dependerá de que dicho símbolo sea un token o un no-terminal.

Nota

📌 Este documento trata el operador '?' cuando se aplique a un sólo símbolo. Ver Acciones en los Patrones para ver la implementación de este operador cuando esté definiendo una lista 0+cs — l: (e (s e)*)?

Valor en Tokens

Supóngase que se aplica el '?' a un token. Por tanto, sería válido que dicho token no apareciera a la entrada. En este caso, si se accede al token ausente desde una acción Java, se obtendría:

  • null si se accede con $<token> o $<token>.text.
    a: IDENT? INT { System.out.println($IDENT); }	// null si no hay IDENT
    
    a: IDENT? INT { System.out.println($IDENT.text); }  // null si no hay IDENT
    
  • Una excepción NullPointerException si se accede con $<ident>.getText().
    a: IDENT? INT { System.out.println($IDENT.getText()); }	// NullPointerException
    

Como puede observarse, es sorprendente que la notación $token.text, que es un atajo de $token.getText(), produzca un resultado distinto.

Valor en No-Terminales

Si a un hijo no-terminal se le ha aplicado '?' y no aparece a la entrada, no se llega a llamar a su función asociada y, por tanto, no se obtiene su objeto contexto. Por tanto, al acceder a dicho contexto desde una acción Java, se obtendrá null.

a: b? INT { System.out.println($b.ctx); }  // null si no hay `b` a la entrada

En ese caso, no se debería intentar acceder a ninguno de los atributos de dicho contexto (en concreto, al atributo de retorno). Por ejemplo, no se debería hacer lo siguiente, ya que produciría un NullPointerException:

a: b? INT { System.out.println($b.val); }  // Excepción si no está b!!!

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

Esto quiere decir que, antes de acceder al atributo de retorno $b.val, hay que asegurarse de que en la entrada hubo un símbolo b y, por tanto, se llegó a llamar a b(); es decir, que el contexto de b no sea null.

a: b? INT { System.out.println($b.ctx != null); }
    // true si hubo `b` en la entrada

Recuérdese que, como se vio en acceso básico en no-terminales, aquí no se puede usar $b directamente (sin añadir el .ctx), cosa que sí permitía con los tokens:

a: b? INT { System.out.println($b != null); };
    // ANTLR no reconoce `$b` y da error

Opciones de Implementación

Una vez analizadas todos los posibles valores que puede devolver un símbolo al que se le haya aplicado el operador '?', se verá a continuación cómo deben ser en la práctica las acciones que tengan que tratar con estos símbolos.

Supóngase un lenguaje con un print en el que sea opcional poner la expresión que normalmente le sigue (en ese caso, supongamos que sólo imprimiría un salto de línea).

sentence returns[Sentence ast]
	: 'print' expr? ';' { $ast = new Print(...); }
	;

A la hora de crear el nodo Print, si la expresión no aparece en la entrada, se quiere meter null en dicho nodo para representar la ausencia de hijo.

Aparentemente, una solución podría ser:

sentence returns[Sentence ast]
	: 'print' expr? ';' { $ast = new Print($expr.ast); } // Posible excepción!!!
	;

El problema de la versión anterior es que, como se ha visto en el apartado anterior, si la expresión no está en la entrada, el atributo $expr vale null y al intentar acceder a $expr.ast se producirá una NullPointerException.

Por tanto, en lugar de lo anterior, se tienen las siguientes soluciones:

Se deja a libertad del alumno decidir cuál de ellas utilizar.

Nota

📌 En todas las soluciones siguientes se asume que, si no está el hijo, se quiere guardar null en el padre. Si se quisiera representar la ausencia de este hijo de otra manera (por ejemplo, con un NullObject), en dichas soluciones sólo habría que sustituir el null por el new de dicha clase.

Código de Detección

La primera solución es utilizar código que detecte si el hijo ha aparecido o no y, en función de ello, meter en el nodo el valor adecuado como hijo.

sentence returns[Sentence ast]
	: 'print' expr? ';' { $ast = new Print($expr.ctx != null ? $expr.ast : null); }
	;

Recuérdese que ANTLR4 no permite usar $expr directamente; para acceder al contexto devuelvo por un hijo hay que utilizar $expr.ctx.

Regla Auxiliar EBNF

La segunda solución sería crear una regla auxiliar exprOpt a la que se mueva el operador '?'.






 


sentence returns[Sentence ast]
	: 'print' exprOpt ';' { $ast = new Print($exprOpt.ast); }
	;

exprOpt returns[Expression ast]
	: (expr { $ast = $expr.ast; })?     // El '?' está aquí!!
	;

De esta manera, dado que siempre se invocará la función exprOpt, siempre se obtendrá un contexto en el retorno de dicha función (y, por tanto, $exprOpt nunca será null).

Dentro de exprOpt, si en la entrada aparece la expresión opcional se ejecuta la acción y se asigna valor a $ast. En caso contrario, $ast quedará con su valor de inicialización null. Pero, en cualquier caso, en la regla padre sentence, la expresión $exprOpt nunca producirá una excepción ya que siempre apuntará a un objeto contexto (aunque el valor del atributo ast de dicho contexto sea null).

Sin embargo, esta solución sólo sería válida en los casos en los que se quiera usar null en lugar del hijo. Si se quisiera devolver un objeto de otra clase (por ejemplo, un NullObject), esta solución no podría aplicarse, ya que juega con la inicialización por defecto de $ast a null.

Podría crearse, en la definición de la variable de retorno de la clausula returns, el objeto a devolver en el caso de que no se ejecute la asignación a $ast, pero dicho objeto se crearía siempre, incluso en los casos en los que apareciera el hijo y se asignara otro objeto a $ast.

Regla Auxiliar BNF

La tercera solución sería renunciar al operador EBNF '?' y utilizar una regla auxiliar con alternativa a vacío como se hubiera hecho en BNF.







 


sentence returns[Sentence ast]
	: 'print' exprOpt ';' { $ast = new Print($exprOpt.ast); }
	;

exprOpt returns[Expression ast]
	: expr	{ $ast = $expr.ast; }
	|		{ $ast = null; }
	;

En la acción asociada a la regla vacía se podría dejar cualquier valor que se quisiera usar para indicar la asencia de la entrada. En este caso, se ha asignado null. Sin embargo, para devolver dicho valor en $ast, no sería necesaria la acción ya que es el valor de inicialización que ya tiene $ast.



 


exprOpt returns[Expression ast] // $ast se inicializa a null
	: expr	{ $ast = $expr.ast; }
	|
	;

Desdoblar Combinaciones

La última solución consiste en renunciar de nuevo al operador '?' de EBNF y, en vez de añadir una regla auxiliar, duplicar en el padre directamente las distintas variantes que puede tener en función de que su hijo aparezca o no:

sentence returns[Sentence ast]
	: 'print' expr ';'	{ $ast = new Print($expr.ast); }
	| 'print' ';'		{ $ast = new Print(null); }
	;

Lo anterior tiene el inconveniente de que, si en la regla hay varios hijos con operador '?', el número de combinaciones que hay que desplegar crecerá rápidamente (habrá 2n reglas, donde n es el número de hijos opcionales). Sin embargo, en otros casos puede ser la solución más fácil de leer.