Xtext grammar describing cron expression not working as expected

308 views Asked by At

I am currently developing a Domain Specific Language using Xtext, and until now everything has gone well. At this very moment I am working in the definition of the grammar, specifically in a set of production rules to let the user specify cron expressions (like the ones within the crontab file in Unix).

The problem: as you can see, production rule RangeCronList is supposed to allow values like */1,3-4,JAN-DEC but it does not. However, it does allow things like */10,10-2 or */1,JAN-DEC.

The error: The error I am getting in the generated Eclipse IDE (for the non-recognized expressions) is "no viable alternative at input '...'".

The question: why these production rules do not allow to specify integers or IDs? I want users to be able to specify single integers, ids, ranges of them, and a list of these possible values.

Additional: my experience with DSLs is short, so I appreciate if you can give some advices on this grammar fragment.

grammar org.pascani.Pascani with org.eclipse.xtext.xbase.Xbase

import "http://www.eclipse.org/xtext/common/JavaVMTypes" as types

generate pascani "http://www.pascani.org/Pascani"

Model
    :   package = PackageDeclaration? 
        imports = XImportSection?
        usings =  UsingSection?
        typeDeclaration = TypeDeclaration?
    ;

PackageDeclaration returns Package
    :   'package' name = QualifiedName ';'?
    ;

UsingSection
    :   usingDeclarations += UsingDeclaration+
    ;

UsingDeclaration returns Using
    :   'using' namespace ?= 'namespace' type = [Namespace | QualifiedName] ';'?
    ;

TypeDeclaration
    :   MonitorDeclaration 
    |   NamespaceDeclaration
    ;

MonitorDeclaration returns Monitor
    :   'monitor' name = ID '{'
            typeDeclarations += MemberDeclaration*
        '}'
    ;

NamespaceDeclaration returns Namespace
    :   'namespace' name = ID '{'
            typeDeclarations += NamespaceMemberDeclaration*
        '}'
    ;

NamespaceMemberDeclaration 
    :   NamespaceDeclaration 
    |   VariableDeclaration
    ;

MemberDeclaration
    :   VariableDeclaration
    |   HandlerDeclaration
    |   EventDeclaration
    ;

VariableDeclaration
    :   jvmType = JvmTypeReference expression = XExpression ';'?    // (XAssignment | MapValue | PairValue | ArrayValue) 
    ;

HandlerDeclaration returns Handler
    :   'handler' name = ID '(' declaredFormalParameter = JvmFormalParameter ')' body = XBlockExpression
    ;

// Special data types declaration

MapValue returns Map    
    :   {Dictionary} '{' ( pairs += [Pair] (',' pairs += [Pair])* )? '}'
    ;

PairValue returns Pair
    :   key = (ID | STRING) ':' value = XExpression
    ;

ArrayValue returns Array
    :   {Array} '[' (elements += XExpression (',' elements+= XExpression)*)? ']'
    ;

// Event declarations

EventDeclaration returns Event
    :   'event' name = ID 'raised' (periodically ?= 'periodically')? 'on' emitter = EventEmitter ';'?
    ;

EventType
    :   ('invoke'|'return'|'change'|'exception')
    ;

EventEmitter
    :   eventType = EventType 'of' emitter = QualifiedName (=>specifier = (RelationalEventSpecifier | EventSpecifier))? ('using' probe = ID)?
    |   cronExpression = CronExpression
    ;

RelationalEventSpecifier
    :   '(' RelationalEventSpecifier ')'
    |   left = EventSpecifier (and ?= 'and' | or ?= 'or') right = EventSpecifier
    ;

EventSpecifier
    :   'below' 'of' EventSpecifierValue
    |   'above' 'of' EventSpecifierValue
    |   'equal' 'to' EventSpecifierValue
    ;

EventSpecifierValue
    :   value = Number percentage ?= '%'?
    |   variable = QualifiedName
    ;

CronExpression
    :   '('
            seconds = CronElement
            minutes = CronElement 
            hours = CronElement  
            days = CronElement 
            months = CronElement  
            daysOfWeek = CronElement 
            (year = CronElement)?
        ')'
    |   '@' constant = ID
    ;

CronElement
    :   TerminalCronElement | RangeCronElement | PeriodicCronElement
    ;

RangeCronElement hidden()
    :   start = IntLiteral '-' end = IntLiteral
    |   start = ID '-' end = ID
    ;

TerminalCronElement
    :   expression = (IntLiteral | ID | '*' | '?')
    ;

PeriodicCronElement hidden()
    :   expression = TerminalCronElement '/' elements = RangeCronList
    ;

RangeCronList hidden()
    :   elements += (TerminalCronElement | RangeCronElement) (',' elements += (TerminalCronElement | RangeCronElement))*
    ;

IntLiteral
    :   INT
    ;

This is the part I am interested in:

Cron Expression

Thanks.


Example (input)

package org.example.monitors

using namespace System

monitor Performance {

    event e1 raised on (0 */1,10-20 * * * *) // works fine
    event e2 raised on (0 */1 * * * *) // It's not recognized!

}
1

There are 1 answers

1
Sven Efftinge On BEST ANSWER

I don't see how whitespace is significant for parsing. If you just want to disallow whitespace you are better off invalidating it afterwards, because then you also have more control over what and how you tell the user (i.e. 'unexpected token RULE_WS' is not super useful).

The problem here is, that we need a look ahead of 2 in order to decide whether to enter the rule TerminalCronElement or RangeCronElement. Both alternatives check the token at LA(2) for possible follow ups. Unfortunately as we are in a hidden() context the token is WS, but that is not listed as a possible follow up, since the rule where the follow up tokens come from does hide whitespaces.

Your grammar works, if you rewrite the problematic part a bit like this:

Model:
   expressions+=CronExpression*;

CronExpression
   :    '('
           seconds = CronElement
           minutes = CronElement 
           hours = CronElement  
           days = CronElement 
           months = CronElement  
           daysOfWeek = CronElement 
           (year = CronElement)?
       ')'
   |     '@' constant = ID
   ;

CronElement
   :    RangeCronElement | PeriodicCronElement
   ;

RangeCronElement hidden()
   :    TerminalCronElement ({RangeCronElement.start=current}'-' end = IntLiteral)*
   ;

TerminalCronElement
   :    expression = (IntLiteral | ID | '*' | '?')
   ;

PeriodicCronElement hidden()
   :    expression = TerminalCronElement '/' elements = RangeCronList
   ;

RangeCronList hidden()
   :    elements += RangeCronElement (',' elements +=RangeCronElement)*
   ;

IntLiteral
   :    INT
   ;

(note the change in RangeCronElement) But here you'll have to invalidate things like *-3 or ?-5.

I suggest you try to do without the use of hidden().