Syntax Specification
Antlr4 files may be found for EL at in the openEHR Antlr4 Git repository.
The Antlr4 grammar for the EL syntax is shown below.
//
// description: Antlr4 grammar for openEHR Expression Language baed on BMM meta-model.
// author: Thomas Beale <thomas.beale@openehr.org>
// contributors:Pieter Bos <pieter.bos@nedap.com>
// support: openEHR Specifications PR tracker <https://openehr.atlassian.net/projects/SPECPR/issues>
// copyright: Copyright (c) 2016- openEHR Foundation <http://www.openEHR.org>
// license: Apache 2.0 License <http://www.apache.org/licenses/LICENSE-2.0.html>
//
parser grammar ElParser;
options { tokenVocab=ElLexer; }
import Cadl2Parser;
// ========================== Type names ==========================
typeId: simpleTypeId | genericTypeId ;
simpleTypeId: simpleTypeName typeValueConstraint? ;
simpleTypeName: UC_ID ;
typeValueConstraint: SYM_LEFT_GUILLEMET namespaceId SYM_RIGHT_GUILLEMET ;
// a namespace id has at least one '.'
namespaceId: namespaceSegmentId ( '.' namespaceSegmentId )+ ;
namespaceSegmentId: LC_ID | UC_ID | WEB_ID ;
genericTypeId: simpleTypeName '<' typeId ( ',' typeId )* '>' ;
// ========================== EL Expressions ==========================
//
// Stratified expression grammar
//
// Note on spurious "FULL AMBIGUITY" warnings: parsing real .bmml sources through this grammar
// reports numerous ANTLR FULL AMBIGUITY warnings (always exact: false), centred on the
// loop-continue-vs-exit decision of the `( op operand )*` repetitions below - most visibly
// elExprAnd's and elExprEquality's. These are confirmed benign, not a grammar defect:
//
// - elExpression (and everything under it) is reached from dozens of unrelated call sites
// across the combined Bmml+Cadl2+El grammar (bmmConstantDecl, bmmPropertyDecl,
// bmmVariableDecl, bmmAssignment, dlConditionBranch, elArgsList, bmmClassAssertion,
// elExprForAll/ThereExists bodies, etc.), each with a different follow context. When ANTLR
// escalates one of these loop-exit decisions to full-context (LL) analysis, its default
// (non-exact) mode deliberately stops looking as soon as it can guarantee the correct
// prediction, without proving there is no ambiguity for some other, unreached follow
// context - hence exact is always false here. See ParserATNSimulator's own comment on this
// trade-off ("we just can't say for sure there is an ambiguity without looking further").
// - ANTLR always resolves a reported ambiguity by picking the lowest-numbered alternative,
// and for a `(...)* ` loop that is always "take another iteration" (the greedy, intended
// reading) - never "exit early".
// - Verified directly: dumping the parse tree for inputs like `r.source = self` and
// `a = b and c = d` (both of which trigger this warning) shows the operators are built
// correctly regardless - e.g. `elExprEquality(elExprComparison(r.source), =,
// elExprComparison(self))` - not truncated or misparsed.
//
// Net effect: noisy but harmless. Eliminating the warning would require restructuring away
// from a widely shared elExpression subtree (e.g. back to native left-recursion, which is
// what an earlier grammar iteration used to dodge a *different*, genuine ambiguity - see git
// history) - not something to take on just to silence a cosmetic warning.
//
elExpression:
elExprTernary
| elTuple
;
elExprTernary: elExprImplies ( '?' elSimpleTerminal ':' elSimpleTerminal )? ;
elExprImplies: elExprOr ( SYM_IMPLIES elExprOr )* ;
elExprOr: elExprAnd ( elOrBinop elExprAnd )* ;
elExprAnd: elExprEquality ( SYM_AND elExprEquality )* ;
elExprEquality: elExprComparison ( elEqualityBinop elExprComparison )* ;
elExprComparison: elExprAddSub ( elComparisonBinop elExprAddSub )* ;
elExprAddSub: elExprMultDiv ( elAddSubBinop elExprMultDiv )* ;
elExprMultDiv: elExprExp ( elMultDivBinop elExprExp )* ;
// Uses RHS recursion to achieve right-associativity
elExprExp: elExprNot ( '^' elExprExp )? ;
elExprNot: SYM_NOT? elExprPostfixUnary ;
//
// Placeholder, if/when postfix markers needed
//
elExprPostfixUnary: elExprTerminal ;
elExprTerminal:
elExprVoidComparison
| elExprParen
| elAtom
| elExprMatches
| elExprForAll
| elExprThereExists
| dlDecisionTable
;
elExprParen: '(' elExpression ')' ;
elExprMatches: elValueGenerator SYM_MATCHES '{' primitiveObjectMatcher '}' ;
elExprForAll: SYM_FOR_ALL elVariableId ':' elValueGenerator '¦' elExpression ;
elExprThereExists: SYM_THERE_EXISTS elVariableId ':' elValueGenerator '¦' elExpression ;
//
// Equivalent of aaa != Void, or similar. We use instead the form ∃aaa
//
elExprVoidComparison: SYM_THERE_EXISTS elValueGenerator ;
//
// --------- Various operators ----------
//
elOrBinop:
SYM_OR
| SYM_XOR
;
elEqualityBinop:
SYM_EQ
| SYM_NE
;
elComparisonBinop:
SYM_GT
| SYM_LT
| SYM_LE
| SYM_GE
;
elAddSubBinop:
'+'
| '-'
;
elMultDivBinop:
'/'
| '*'
| '%'
;
//
// ----------- Semantic and structural atoms -------------
//
elAtom:
booleanValue
| elArithmeticValue
| stringValue
| characterValue
| termCodeValue
| primitiveStructure
| elValueGenerator
;
elArithmeticValue:
integerValue
| realValue
| dateValue
| dateTimeValue
| timeValue
| durationValue
;
//
// -------------------------- tuples -----------------------------
//
elTuple: '[' elExpression ( ',' elExpression )+ ']';
//
// -------------------------- value-generating expressions -----------------------------
//
//
// A narrower terminal, used where only a leaf value (no operators) is syntactically valid,
// e.g. decision-table branches and ternary results.
//
elSimpleTerminal:
booleanValue
| elArithmeticValue
| stringValue
| termCodeValue
| primitiveStructure
| elValueGenerator
;
//
// TODO: Can't syntactically distinguish between a local variable
// and a property or constant reference.
//
elValueGenerator:
SYM_SELF
| elBareRef
| elScopedFeatureRef
;
//
// An unscoped reference of some kind
// Will map to EL_WRITABLE_VARIABLE or EL_PROPERTY_REF (unscoped)
//
elBareRef:
elInstantiableRef
| elFunctionCall
| elConstantId
;
//
// Instantiable feature refs; may be the target of an assignment
//
elInstantiableRef:
SYM_RESULT
| elBoundVariableId
| elVariableId
;
//
// Scoped feature references. Has to have at least one scoping element, either a
// class name and then any number of bareRefs. Will map to any ElValueGenerator (scoped)
//
elScopedFeatureRef: elScoper elBareRef ;
elScoper: ( typeId '.' )? ( elBareRef '.' )+ ;
//
// A variable bound to a data source, lexical form '$xxxx'
// TODO: analyse how a boundVariableId can be created as a built-in feature
//
elBoundVariableId: BOUND_VARIABLE_ID ;
//
// A 'variable' reference could be to a property; parameter; local variable. The parser cannot know
// which one, it depends on the context e.g. being in an invariant or being in a routine pre-condition
// or statement.
//
elVariableId: elLcId ;
elConstantId: UC_ID ;
//
// Function calls: Build a BmmFunctionCall object
//
elFunctionCall: elLcId '(' elArgsList? ')' ;
elArgsList: elExpression ( ',' elExpression )* ;
//
// -------------------------- decision tables -----------------------------
//
// Ugly, but we need to allow certain keywords as routine names; the following
// is a hard-wired list of names that will have been matched as keyowods, rather than
// LC_ID
elLcId:
LC_ID
| SYM_FOR_ALL
| SYM_THERE_EXISTS
| SYM_MATCHES
| SYM_ASSERT
| SYM_CARDINALITY
| SYM_EXISTENCE
| SYM_OCCURRENCES
;
//
// The ternary form (dlBinaryChoice / 'cond ? a : b') is not part of this rule - see the note
// on elAtom above; it is inlined directly into elExpr as #elExprTernary.
//
dlDecisionTable:
dlCaseTable
| dlConditionTable
;
dlCaseTable:
dlSimpleCaseTable
| dlGeneralCaseTable
;
//
// condition chains (if/then statement equivalent)
// when
// =========================================================
// er_positive and
// her2_negative and
// not ki67.in_range (#high) -> #luminal_A,
// ---------------------------------------------------------
// er_positive and
// her2_negative and
// ki67.in_range (#high) -> #luminal_B_HER2_negative,
// ---------------------------------------------------------
// * -> #none
// =========================================================
//
dlConditionTable: SYM_WHEN BLOCK_DELIM ( dlConditionBranch ',' )+ ( dlConditionBranch | dlConditionDefaultBranch ) BLOCK_DELIM ;
dlConditionBranch: elExpression SYM_ARROW elExpression ;
dlConditionDefaultBranch: SYM_ASTERISK SYM_ARROW elExpression ;
//
// Case tables, e.g.:
// Result := case qCSI_score in
// ============================
// 0: expr0,
// ----------------------------
// |1..2|: expr1,
// ----------------------------
// |3..5|: expr2,
// ----------------------------
// |6..8|: expr3,
// ----------------------------
// |≥ 9|: expr4
// ============================
// ;
//
dlGeneralCaseTable: SYM_CASE elExpression SYM_IN BLOCK_DELIM ( dlGeneralCaseBranch ',' )+ ( dlGeneralCaseBranch | dlGeneralCaseDefaultBranch ) BLOCK_DELIM ;
dlGeneralCaseBranch: primitiveObject ':' elExpression ;
dlGeneralCaseDefaultBranch: SYM_ASTERISK ':' elExpression ;
//
// Simple value-based (typed) Case tables, e.g.:
// case gfr_range in
// =================
// |>20|: 1,
// |10..20|: 0.75,
// |<10|: 0.5
// =================
// ;
//
dlSimpleCaseTable: SYM_CASE elSimpleTerminal SYM_IN BLOCK_DELIM ( dlSimpleCaseBranch ',' )+ ( dlSimpleCaseBranch | dlSimpleCaseDefaultBranch ) BLOCK_DELIM ;
dlSimpleCaseBranch: primitiveObject ':' elSimpleTerminal ;
dlSimpleCaseDefaultBranch: SYM_ASTERISK ':' elSimpleTerminal ;
The Antlr4 lexer for the EL syntax is shown below.
//
// description: Antlr4 grammar for openEHR Expression Language baed on BMM meta-model.
// author: Thomas Beale <thomas.beale@openehr.org>
// contributors:Pieter Bos <pieter.bos@nedap.com>
// support: openEHR Specifications PR tracker <https://openehr.atlassian.net/projects/SPECPR/issues>
// copyright: Copyright (c) 2016- openEHR Foundation <http://www.openEHR.org>
// license: Apache 2.0 License <http://www.apache.org/licenses/LICENSE-2.0.html>
//
lexer grammar ElLexer;
import Cadl2Lexer, SymbolsLexer, GeneralIdsLexer;
// The COMMENT channel receives all regular comments.
channels {
COMMENT
}
// ------------------ lines and comments ------------------
// inline comment, i.e. right side of text
CMT_LINE : '||' ~[\r\n]* -> channel(COMMENT) ;
// These are used in decision tables, but are really comments
BLOCK_DELIM : '====' '='* EOL ;
BLOCK_ROW : '----' '-'* EOL -> channel(COMMENT) ;
EOL : '\r'? '\n' -> channel(HIDDEN) ;
WS : [ \t\r] -> channel(HIDDEN) ;
// --------- keywords ----------
SYM_RESULT : 'Result' ;
SYM_SELF : 'Self' ;
SYM_IN : 'in' ;
SYM_WHEN : 'when' ;
SYM_CASE : 'case' ;
// --------- symbols ----------
SYM_ASSIGNMENT: ':=' ;
SYM_INTERROGATION: '?' ;
SYM_LEFT_GUILLEMET: '«' ;
SYM_RIGHT_GUILLEMET: '»' ;
SYM_BROKEN_BAR: '¦' ;
SYM_DOUBLE_MINUS: '--' ;
SYM_DOUBLE_PLUS: '++' ;
SYM_THEN : 'then' | 'THEN' ;
SYM_AND : 'and' | 'AND' | '∧' ;
SYM_OR : 'or' | 'OR' | '∨' ;
SYM_XOR : 'xor' | 'XOR' | '⊻' ;
SYM_NOT : 'not' | 'NOT' | '~' | '¬' ;
SYM_IMPLIES : 'implies' | '⇒' | '→' ;
SYM_IFF : '⇔' | '↔' ;
SYM_FOR_ALL : 'for_all' | '∀' ;
SYM_THERE_EXISTS: 'there_exists' | '∃' ;
SYM_MATCHES : 'matches' | 'is_in' | '∈' ;
SYM_ASSERT : 'assert' ;
BOUND_VARIABLE_ID: '$' LC_ID ;
// ---------- local code that are not ADL codes -------
// e.g. [heart_rate]
LOCAL_TERM_CODE_REF: '[' ALPHANUM_US_CHAR+ ']' ;