persistence@glassfish.java.net

EJBQL update: grammar change

From: Michael Bouschen <Michael.Bouschen_at_Sun.COM>
Date: Wed, 26 Oct 2005 20:05:23 +0200

Hi Tom,

attached you find an updated version of EJBQLParser.g. I added new
grammar rules for
- multiple expressions in the SELECT clause
- constructor expression (NEW keyword)
- EXISTS
- ALL, ANY, SOME
- CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP
- SIZE

Please note, all of the above features are not fully implemented, so the
parser will throw an exception "Not yet implemented: SIZE function" if
any of these are used in an EJBQL query.

Please let me know if there are any issues with the change. Thanks!

Regards Michael





/*
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the "License"). You may not use this file except
 * in compliance with the License.
 *
 * You can obtain a copy of the license at
 * glassfish/bootstrap/legal/CDDLv1.0.txt or
 * https://glassfish.dev.java.net/public/CDDLv1.0.html.
 * See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * HEADER in each file and include the License file at
 * glassfish/bootstrap/legal/CDDLv1.0.txt. If applicable,
 * add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your
 * own identifying information: Portions Copyright [yyyy]
 * [name of copyright owner]
 */
// Added 20/12/2000 JED. Define the package for the class
header {
        package oracle.toplink.essentials.internal.parsing.ejbql.antlr273;

    import oracle.toplink.essentials.exceptions.EJBQLException;
}

/** */
class EJBQLParser extends Parser("oracle.toplink.essentials.internal.parsing.ejbql.EJBQLParser");
options {
        exportVocab=EJBQL;
        k = 3; // This is the number of tokens to look ahead to
        buildAST = true;
}

tokens {
        ABS="abs";
    ALL="all";
        AND="and";
    ANY="any";
        AS="as";
        ASC="asc";
        AVG="avg";
        BETWEEN="between";
    BOTH="both";
        BY="by";
        CONCAT="concat";
        COUNT="count";
    CURRENT_DATE="current_date";
    CURRENT_TIME="current_time";
    CURRENT_TIMESTAMP="current_timestamp";
        DESC="desc";
    DELETE="delete";
        DISTINCT="distinct";
        EMPTY="empty";
        ESCAPE="escape";
    EXISTS="exists";
        FALSE="false";
    FETCH="fetch";
        FROM="from";
    GROUP="group";
    HAVING="having";
        IN="in";
    INNER="inner";
        IS="is";
    JOIN="join";
    LEADING="leading";
    LEFT="left";
        LENGTH="length";
        LIKE="like";
        LOCATE="locate";
    LOWER="lower";
        MAX="max";
        MEMBER="member";
        MIN="min";
        MOD="mod";
    NEW="new";
        NOT="not";
        NULL="null";
        OBJECT="object";
        OF="of";
        OR="or";
        ORDER="order";
    OUTER="outer";
        SELECT="select";
        SET="set";
    SIZE="size";
        SQRT="sqrt";
    SOME="some";
        SUBSTRING="substring";
        SUM="sum";
    TRAILING="trailing";
    TRIM="trim";
        TRUE="true";
        UNKNOWN="unknown";
        UPDATE="update";
    UPPER="upper";
        WHERE="where";
}

{
    protected void validateAbstractSchemaName(Token token)
        throws RecognitionException {
        String text = token.getText();
        if (!isValidJavaIdentifier(token.getText())) {
            throw new NoViableAltException(token, getFilename());
        }
    }

    protected boolean isValidJavaIdentifier(String text) {
        if ((text == null) || text.equals(""))
            return false;

        // check first char
        if (!Character.isJavaIdentifierStart(text.charAt(0)))
            return false;

        // check remaining characters
        for (int i = 1; i < text.length(); i++) {
            if (!Character.isJavaIdentifierPart(text.charAt(i))) {
                return false;
            }
        }
        
        return true;
    }
}

document
        : selectStatement
        | updateStatement
    | deleteStatement
        ;

selectStatement
        : (selectClause {finishedSelect();})
          (fromClause {finishedFrom();})
          (whereClause {finishedWhere();})?
      (groupByClause {finishedGroupBy();})?
      (havingClause {finishedHaving();})?
          (orderByClause {finishedOrderBy();})?
          EOF {finishedAll();}
        ;

//================================================

updateStatement
        : (updateClause {finishedUpdate();})
          (setClause {finishedSet();})
          (whereClause {finishedWhere();})?
          EOF {finishedAll();}
        ;

updateClause
        : UPDATE {matchedUpdate();}
          (abstractSchemaName ((AS)? abstractSchemaIdentifier)?)
        ;

setClause
        : (set setAssignmentClause)
          (COMMA {startEqualsAssignment();} setAssignmentClause)*
         ;

set
        : SET {matchedSet();}
        ;

setAssignmentClause
        : ((leftMostIdentifier dot)? identifier equalsAssignment newValue {finishedEqualsAssignment();})
        ;

newValue
    : expressionOperand
        ;

//================================================

deleteStatement
    : deleteClause { finishedDelete(); }
      (whereClause {finishedWhere();})?
      EOF {finishedAll();}
    ;

deleteClause
    : DELETE { matchedDelete(); } FROM
      abstractSchemaName ((AS)? abstractSchemaIdentifier)?
    ;

//================================================

selectClause
        : SELECT {matchedSelect();} (distinct)?
      selectExpression
      ( COMMA
        {
            throw EJBQLException.notYetImplemented(
               "multiple expressions in SELECT clause");
        }
        selectExpression
      )*
    ;

distinct
        : DISTINCT {matchedDistinct();}
        ;

selectExpression
    : singleValuedPathExpression
    | aggregateFunction {finishedAggregate();}
    | identifier
    | OBJECT LEFT_ROUND_BRACKET identifier RIGHT_ROUND_BRACKET
    | constructorExpression
        ;

aggregateFunction
        : AVG {matchedAvg();} aggregateFunctionArg
    | MAX {matchedMax();} aggregateFunctionArg
    | MIN {matchedMin();} aggregateFunctionArg
    | SUM {matchedSum();} aggregateFunctionArg
        | COUNT {matchedCount();} countFunctionArg
        ;

aggregateFunctionArg
    : LEFT_ROUND_BRACKET (distinct)? stateFieldPathExpression RIGHT_ROUND_BRACKET
    ;

countFunctionArg
    : LEFT_ROUND_BRACKET (distinct)? ( identifier | pathExpression ) RIGHT_ROUND_BRACKET
    ;

constructorExpression
    : NEW
      {
          throw EJBQLException.notYetImplemented(
              "constructor expression");
      }
        constructorName
        LEFT_ROUND_BRACKET
        ( constructorItem ( COMMA constructorItem )* )?
        RIGHT_ROUND_BRACKET
    ;

constructorName
    : baseIdentifier ( DOT baseIdentifier )*
    ;

constructorItem
    : singleValuedPathExpression
    | aggregateFunction {finishedAggregate();}
    ;

fromClause
        : from identificationVariableDeclaration
      (COMMA (identificationVariableDeclaration |
               collectionMemberDeclaration) )*
        ;
from
        : FROM {matchedFrom();}
        ;

identificationVariableDeclaration
        : rangeVariableDeclaration (join)*
        ;

rangeVariableDeclaration
        : abstractSchemaName (AS)? abstractSchemaIdentifier
        ;

// Non-terminal abstractSchemaName first matches any token to allow abstract
// schema names that are keywords (such as order, etc.).
// Method validateAbstractSchemaName throws an exception if the text of the
// token is not a valid Java identifier.
abstractSchemaName
        : ident:.
      {
          validateAbstractSchemaName(ident);
          matchedAbstractSchemaName();
      }
        ;

abstractSchemaIdentifier
        : baseIdentifier {matchedAbstractSchemaIdentifier();}
        ;

join
{ boolean outerJoin; }
    : outerJoin = joinSpec
      ( associationPathExpression (AS)? baseIdentifier
        {
            matchedJoinIdentifier();
            finishedJoin(outerJoin);
        }
      | FETCH associationPathExpression
        { finishedFetchJoin(outerJoin); }
      )
    ;

joinSpec returns [boolean outer]
{ outer = false; }
    : (LEFT (OUTER)? { outer = true; } | INNER )? JOIN
    ;

collectionMemberDeclaration
        : IN LEFT_ROUND_BRACKET collectionValuedPathExpression RIGHT_ROUND_BRACKET
      (AS)? baseIdentifier
      {
          matchedCollectionMemberIdentifier();
          finishedInClauseInFrom();
      }
        ;

collectionValuedPathExpression
    : pathExpression
    ;

associationPathExpression
    : pathExpression
    ;

singleValuedPathExpression
        : pathExpression
        ;

stateFieldPathExpression
    : pathExpression
    ;

pathExpression
        : leftMostIdentifier dot (identifier dot)* identifier
        ;

dot
        : DOT {matchedDot();}
        ;

identifier
        : baseIdentifier {matchedIdentifier();}
        ;

leftMostIdentifier
        : baseIdentifier {matchedLeftMostIdentifier();}
        ;

baseIdentifier
        : TEXTCHAR
        ;

whereClause
        : WHERE {matchedWhere();} conditionalExpression
        ;

conditionalExpression
        : conditionalTerm (OR{matchedOr();} conditionalTerm {finishedOr();})*
        ;

conditionalTerm
        : {conditionalTermFound();} conditionalFactor (AND{matchedAnd();} conditionalFactor {finishedAnd();})*
        ;

conditionalFactor
        : (NOT {matchedNot();})? ( conditionalPrimary | existsExpression )
        ;

conditionalPrimary
        : (LEFT_ROUND_BRACKET conditionalExpression) =>
                (LEFT_ROUND_BRACKET {matchedLeftRoundBracket();} conditionalExpression RIGHT_ROUND_BRACKET {matchedRightRoundBracket();})
        | simpleConditionalExpression
        ;

simpleConditionalExpression
        : arithmeticExpression simpleConditionalExpressionRemainder
        ;

simpleConditionalExpressionRemainder
        : comparisonExpression
        | ((NOT)? BETWEEN) => (betweenExpression)
        | ((NOT)? LIKE) => (likeExpression)
        | ((NOT)? IN) => (inExpression)
        | (isNot NULL) => (nullComparisonExpression)
        | emptyCollectionComparisonExpression
        | collectionMemberExpression
        ;

betweenExpression
        : (NOT {matchedNot();})? BETWEEN {matchedBetween();} stringExpression AND {matchedAndAfterBetween();} stringExpression {finishedBetweenAnd();}
        ;

inExpression
        : (NOT {matchedNot();})? (IN {matchedIn();})
                (LEFT_ROUND_BRACKET
                        (literalString | literalNumeric | inputParameter | namedInputParameter)
                                (COMMA (literalString | literalNumeric | inputParameter | namedInputParameter))*
                RIGHT_ROUND_BRACKET)
                {finishedIn();}
        ;

likeExpression
        : (NOT {matchedNot();})?
                (LIKE {matchedLike();}
                        (literalString | inputParameter | namedInputParameter)
                        (ESCAPE {matchedEscape();} literalString {finishedEscape();})?)
                {finishedLike();}
        ;

nullComparisonExpression
        : isNot NULL {matchedNull();} {finishedNull();}
        ;

emptyCollectionComparisonExpression
        : isNot EMPTY {matchedEmpty();} {finishedEmpty();}
        ;

collectionMemberExpression
        : (NOT {matchedNot();})? (MEMBER (OF)?) {matchedMemberOf();} collectionValuedPathExpression {finishedMemberOf();}
        ;

existsExpression
    : EXISTS
      {
          throw EJBQLException.notYetImplemented("EXISTS expression");
      }
      LEFT_ROUND_BRACKET subquery RIGHT_ROUND_BRACKET
    ;

comparisonExpression
        : (equalTo | notEqualTo | greaterThan | greaterThanEqualTo | lessThan | lessThanEqualTo)
      ( arithmeticExpression | anyOrAllExpression )
      {finishedComparisonExpression();}
        ;

arithmeticExpression
    : simpleArithmeticExpression
    | LEFT_ROUND_BRACKET subquery RIGHT_ROUND_BRACKET
    ;

simpleArithmeticExpression : arithmeticTerm ((plus | minus) arithmeticTerm)* ;

arithmeticTerm : arithmeticFactor ((multiply | divide) arithmeticFactor)* {finishedMultiplyOrDivide();} ;

arithmeticFactor : (plus | minus)? arithmeticPrimary ;

arithmeticPrimary
        : expressionOperand
        | (LEFT_ROUND_BRACKET {matchedLeftRoundBracket();} arithmeticExpression RIGHT_ROUND_BRACKET {matchedRightRoundBracket();})
        ;

expressionOperand
        : leftMostIdentifier
        | stateFieldPathExpression
        | functionsReturningNumerics
        | functionsReturningDatetime
        | functionsReturningStrings
        | inputParameter
        | namedInputParameter
        | literalNumeric
        | literalString
        | literalBoolean
        ;

anyOrAllExpression
    : ( ALL | ANY | SOME )
        {
            throw EJBQLException.notYetImplemented("ALL, ANY, SOME expression");
        }
        LEFT_ROUND_BRACKET subquery RIGHT_ROUND_BRACKET
    ;

isNot
        : IS (NOT {matchedNot();})?
        ;

stringExpression
        : stringPrimary
    | LEFT_ROUND_BRACKET subquery RIGHT_ROUND_BRACKET
        ;

stringPrimary
        : literalString
        | functionsReturningStrings
        | inputParameter
        | namedInputParameter
    | stateFieldPathExpression
        ;

//Literals and Low level stuff
literal
        : literalNumeric
        | literalBoolean
        | literalString
        ;

literalNumeric
        : NUM_INT {matchedInteger();}
        | NUM_FLOAT {matchedFloat();}
        ;

literalBoolean
        : TRUE {matchedTRUE();}
        | FALSE {matchedFALSE();}
        ;

literalString
        : STRING_LITERAL_DOUBLE_QUOTED {matchedDoubleQuotedString();}
        | STRING_LITERAL_SINGLE_QUOTED {matchedSingleQuotedString();}
        ;

inputParameter
        : (QUESTIONMARK NUM_INT) {matchedInputParameter();}
        ;

namedInputParameter
        : (COLON TEXTCHAR) {matchedNamedInputParameter();}
        ;

functionsReturningNumerics
        : abs
        | length
        | mod
        | sqrt
        | locate
    | size
        ;

functionsReturningDatetime
    : CURRENT_DATE
      {
          throw EJBQLException.notYetImplemented("CURRENT_DATE function");
      }
    | CURRENT_TIME
      {
          throw EJBQLException.notYetImplemented("CURRENT_TIME function");
      }
    | CURRENT_TIMESTAMP
      {
          throw EJBQLException.notYetImplemented("CURRENT_TIMESTAMP function");
      }
    ;

functionsReturningStrings
        : concat
        | substring
    | trim
    | upper
    | lower
        ;

plus
        : PLUS {matchedPlus();}
        ;

minus
        : MINUS {matchedMinus();}
        ;

multiply
        : MULTIPLY {matchedMultiply();}
        ;

divide
        : DIVIDE {matchedDivide();}
        ;

equalTo
        : EQUALS {matchedEqualsComparison();}
        ;

equalsAssignment
        : EQUALS {matchedEqualsAssignment();}
        ;

greaterThan
        : GREATER_THAN {matchedGreaterThan();}
        ;

greaterThanEqualTo
        : GREATER_THAN_EQUAL_TO {matchedGreaterThanEqualTo();}
        ;

lessThan
        : LESS_THAN {matchedLessThan();}
        ;

lessThanEqualTo
        : LESS_THAN_EQUAL_TO {matchedLessThanEqualTo();}
        ;

notEqualTo
        : (NOT_EQUAL_TO) {matchedNotEqualTo();}
        ;

// Functions returning strings
concat
        : CONCAT {matchedConcat();}
                LEFT_ROUND_BRACKET
                        (singleValuedPathExpression | literalString) COMMA {matchedCommaAfterConcat();}
                        (singleValuedPathExpression | literalString)
                RIGHT_ROUND_BRACKET {finishedConcat();}
                
        ;

substring
        : SUBSTRING {matchedSubstring();}
                LEFT_ROUND_BRACKET
                        (singleValuedPathExpression | inputParameter | namedInputParameter | literalString) {finishedSubstringVariable();}
                        COMMA arithmeticExpression
                        COMMA arithmeticExpression
                RIGHT_ROUND_BRACKET
                {finishedSubstring();}
        ;

trim
    : TRIM {matchedTrim();}
        LEFT_ROUND_BRACKET
        ( ( trimDef )=> trimDef )? stringPrimary {finishedTrimVariable();}
        RIGHT_ROUND_BRACKET
      {finishedTrim();}
    ;

trimDef
    : ( trimSpec )? ( trimChar {finishedTrimChar();} )? FROM
    ;

trimSpec
    : LEADING {matchedLeading();}
    | TRAILING {matchedTrailing();}
    | BOTH {matchedBoth();}
    ;

trimChar
    : literalString
    | inputParameter
    ;

upper
    : UPPER { matchedUpper(); }
      LEFT_ROUND_BRACKET stringPrimary { finishedUpperVariable(); } RIGHT_ROUND_BRACKET
      { finishedUpper(); }
    ;

lower
    : LOWER { matchedLower(); }
      LEFT_ROUND_BRACKET stringPrimary { finishedLowerVariable(); } RIGHT_ROUND_BRACKET
      { finishedLower(); }
    ;


// Functions returning numerics
abs
        : ABS {matchedAbs();}
                LEFT_ROUND_BRACKET
                (singleValuedPathExpression | inputParameter | namedInputParameter ) {finishedAbsVariable();}
                RIGHT_ROUND_BRACKET
          {finishedAbs();}
        ;

length
        : LENGTH {matchedLength();}
                LEFT_ROUND_BRACKET
                singleValuedPathExpression {finishedLengthVariable();}
                RIGHT_ROUND_BRACKET
                {finishedLength();}
        ;

locate
        : LOCATE {matchedLocate();}
                LEFT_ROUND_BRACKET
                literalString {finishedLocateLiteral();}
                COMMA singleValuedPathExpression {finishedLocateVariable();}
                RIGHT_ROUND_BRACKET
                {finishedLocate();}
        ;

size
    : SIZE
        {
            throw EJBQLException.notYetImplemented("SIZE function");
        }
        LEFT_ROUND_BRACKET collectionValuedPathExpression RIGHT_ROUND_BRACKET
    ;

mod
        : MOD {matchedMod();}
                LEFT_ROUND_BRACKET
                        arithmeticExpression {finishedFirstArithmeticExpressionInMOD(); }
                        COMMA arithmeticExpression {finishedSecondArithmeticExpressionInMOD();}
                RIGHT_ROUND_BRACKET
        ;

sqrt
        : SQRT {matchedSqrt();}
                LEFT_ROUND_BRACKET
                (singleValuedPathExpression | inputParameter | namedInputParameter) {finishedSqrtVariable();}
                RIGHT_ROUND_BRACKET
        ;

subquery
    : simpleSelectClause
      subqueryFromClause
          (whereClause {finishedWhere();})?
      (groupByClause {finishedGroupBy();})?
      (havingClause {finishedHaving();})?
    ;

simpleSelectClause
    : SELECT {matchedSelect();} (distinct)? simpleSelectExpression
    ;

simpleSelectExpression
    : singleValuedPathExpression
    | aggregateFunction
    | baseIdentifier
    ;

subqueryFromClause
    : from subselectIdentificationVariableDeclaration
        ( COMMA subselectIdentificationVariableDeclaration )*
    ;

subselectIdentificationVariableDeclaration
    : identificationVariableDeclaration
    | associationPathExpression (AS)? baseIdentifier
    | collectionMemberDeclaration
    ;

orderByClause
        : ORDER BY {matchedOrderBy();}
                orderByItem (COMMA orderByItem)*
        ;

orderByItem
        : stateFieldPathExpression {matchedOrderByItem();}
                (ASC {matchedAscDirection();} | DESC {matchedDescDirection();})?
                {finishedOrderByItem();}
    ;

groupByClause
    : GROUP BY { matchedGroupBy(); }
        groupByItem (COMMA groupByItem)*
        ;

groupByItem
    : stateFieldPathExpression
                { finishedGroupByItem(); }
    ;

havingClause
    : HAVING { matchedHaving(); }
        conditionalExpression
    ;

/** */
class EJBQLLexer extends Lexer;
options {
        caseSensitive=false;
        caseSensitiveLiterals=false;
        k = 4;
        exportVocab=EJBQL;
        charVocabulary = '\3'..'\377';
}

// hexadecimal digit (again, note it's protected!)
protected
HEX_DIGIT
        : ('0'..'9'|'a'..'f')
        ;

WS : (' ' | '\t' | '\n' | '\r')+
        { $setType(Token.SKIP); } ;

LEFT_ROUND_BRACKET
        : '('
        ;

RIGHT_ROUND_BRACKET
        : ')'
        ;

COMMA
        : ','
        ;

TEXTCHAR
        : ('a'..'z' | '_' | '$') ('a'..'z' | '_' | '$' | '0'..'9')*
        ;

// a numeric literal
NUM_INT
        {boolean isDecimal=false;}
        : '.' {_ttype = DOT;}
                        (('0'..'9')+ (EXPONENT)? (FLOAT_SUFFIX)? { _ttype = NUM_FLOAT; })?
        | ( '0' {isDecimal = true;} // special case for just '0'
                        ( ('x')
                                ( // hex
                                        // the 'e'|'E' and float suffix stuff look
                                        // like hex digits, hence the (...)+ doesn't
                                        // know when to stop: ambig. ANTLR resolves
                                        // it correctly by matching immediately. It
                                        // is therefor ok to hush warning.
                                        options {
                                                warnWhenFollowAmbig=false;
                                        }
                                : HEX_DIGIT
                                )+
                        | ('0'..'7')+ // octal
                        )?
                | ('1'..'9') ('0'..'9')* {isDecimal=true;} // non-zero decimal
                )
                ( ('l')
                
                // only check to see if it's a float if looks like decimal so far
                | {isDecimal}?
                        ( '.' ('0'..'9')* (EXPONENT)? (FLOAT_SUFFIX)?
                        | EXPONENT (FLOAT_SUFFIX)?
                        | FLOAT_SUFFIX
                        )
                        { _ttype = NUM_FLOAT; }
                )?
        ;

// a couple protected methods to assist in matching floating point numbers
protected
EXPONENT
        : ('e') ('+'|'-')? ('0'..'9')+
        ;


protected
FLOAT_SUFFIX
        : 'f'|'d'
        ;

EQUALS
        : '='
        ;

GREATER_THAN
        : '>'
        ;

GREATER_THAN_EQUAL_TO
        : ">="
        ;

LESS_THAN
        : '<'
        ;

LESS_THAN_EQUAL_TO
        : "<="
        ;

NOT_EQUAL_TO
        : "<>"
        ;

REMOTE_INTERFACE_REFERENCE_OPERATOR
        : "=>"
        ;

MULTIPLY
        : "*"
        ;

DIVIDE
        : "/"
        ;

PLUS
        : "+"
        ;

MINUS
        : "-"
        ;

QUESTIONMARK
        : "?"
        ;

COLON
        : ":"
        ;

// Added Jan 9, 2001 JED
// string literals
STRING_LITERAL_DOUBLE_QUOTED
        : '"' (ESC|~('"'|'\\'))* '"'
        ;

STRING_LITERAL_SINGLE_QUOTED
        : ('\'' '\\' '\'') => ('\'' '\\' '\'')
        | '\'' (ESC|~('\''|'\\') | ("''"))* '\''
        ;

// Added Jan 9, 2001 JED
// escape sequence -- note that this is protected; it can only be called
// from another lexer rule -- it will not ever directly return a token to
// the parser
// There are various ambiguities hushed in this rule. The optional
// '0'...'9' digit matches should be matched here rather than letting
// them go back to STRING_LITERAL to be matched. ANTLR does the
// right thing by matching immediately; hence, it's ok to shut off
// the FOLLOW ambig warnings.
protected
ESC
        : '\\'
                ( '_'
                | 'n'
                | 'r'
                | 't'
                | 'b'
                | 'f'
                | '"'
                | '\''
                | '\\'
                | ('u')+ HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT
                | ('0'..'3')
                        (
                                options {
                                        warnWhenFollowAmbig = false;
                                }
                        : ('0'..'7')
                                (
                                        options {
                                                warnWhenFollowAmbig = false;
                                        }
                                : '0'..'7'
                                )?
                        )?
                | ('4'..'7')
                        (
                                options {
                                        warnWhenFollowAmbig = false;
                                }
                        : ('0'..'9')
                        )?
                )
        ;