Grammars
Contents
- Overview
- Parsing and Synthesizing
- Sequences of Elements
- Specializing Grammars
- Synthesizing Syntax
- Simple Language Constructs
- Binding and Looping Constructs
- Case Analysis
- XBNF Grammar
Overview
Computer based languages are used as communication media; either system to system or human to system. In either case, a computer must be able to recognise phrases expressed in the language. When phrases are presented to a computer they must be interpreted in some way. Typically, phrases may be interpreted directly, as they are recognized, or some internal representation for the phrases may be synthesized (abstract syntax) and subsequently passed on to an internal module for processing.There are a variety of formats by which language may be conveyed to a computer; a common mechanism is by text. A standard way of defining how to recognize and process text-based languages is by supplying a grammar. A grammar is a collection of rules that define the legal sequences of text strings in a language. In addition to recognising language phrases, a grammar may include actions that are to be performed as sub-phrases are processed.
In order to use grammars to process language phrases we must supply the grammar to a program called a {\em parser}. The medium used to express the grammar is usually text. BNF (and te extended version EBNF) is a text-based collection of grammar formation rules. BNF defines how to express a grammar and what the grammar means in terms of processing languages.
This chapter describes the XMF version of EBNF, called XBNF. XBNF integrates EBNF with XOCL (and any language defined by XBNF). XMF provides an XBNF parser that is supplied with an XBNF grammar and will then parse and synthesize XMF-defined laguages.
Grammars (and therefore languages) in XMF are associated with classes. Typically an XBNF grammar defines how to recognize legal sequences of characters in a language and to syntheisize an instance of the associated class (and populate the instance with instances of the class's attribute types).
To top.
Parsing and Synthesizing
This section describes the basics of using XBNF to define simple grammars that recognize languages and synthesize XMF data values. A grammar is an instance of the XMF class Parser::BNF::Grammar. A grammar consists of a collection of clauses each of which is a rule that defines a non-terminal of the grammar. A non-terminal is a name (by convention a non-terminal name starts with an upper case letter).A clause has the following form:
NAME ::= RULE .
The grammar is constructed incrementally, the clauses are defined in the context of an @Grammar:
@Grammar
CLAUSES
end
Calc ::= Mult '=' 'end'.
A Mult is a multiplicative expression, possibly involving the operators * and /. We use a standard method of defining operator precedence in the grammar, where addition operators bind tighter than multiplicative operators. This is achieved using two different non-terminals:
Mult ::= n1 = Add (
'*' n2 = Mult { n1 * n2 }
| '/' n2 = Mult { n1 / n2 }
| { n1 }
).
Each non-terminal synthesizes a value when it successfully recognizes some input. When one non-terminal calls another (for example Add is called by Mult) the value synthesized by the called non-terminal may be optionally named in the definition of the calling non-terminal. Once named, the syntheized value may be referenced in the rest of the rule. Naming occurs a number of times in the definition of Mult where the names n1 and n2 are used to refer to syntheisized numbers.
A clause may include optional parts separated by |. In general, if a clause contains an optional component of the form A | B where A and B are arbitrary components, then a parse is successful when either A or B is successful.
The clause for Mult contains three optional components that occur after the initial Add is successful (hence the need for parentheses around the optional components to force the Add to occur). Once the Add is successful then the parse is successful when one of the following occurs:
- a * is encountered followed by a Mult.
- a + is encountered followed by a Mult.
- no further input is processed (this option must occur last since it subsumes the previous two options - options are tried in turn).
The clause for Mult contains a number of actions that are used to synthesize values (in this case evaluate numeric expressions). The clause for Add is the same as that for Mult except that the clause recognizes and syntheises addition expressions:
Add ::= n1 = Int (
'+' n2 = Add { n1 + n2 }
| '-' n2 = Add { n1 - n2 }
| { n1 }
).
@Grammar
Calc ::= Mult '=' 'end'.
Mult ::= n1 = Add (
'*' n2 = Mult { n1 * n2 }
| '/' n2 = Mult { n1 / n2 }
| { n1 }
).
Add ::= n1 = Int (
'+' n2 = Add { n1 + n2 }
| '-' n2 = Add { n1 - n2 }
| { n1 }
).
end
Sequences of Elements
A grammar rule may want to specify that, at a given position in the parse, a sequence of character strings can occur. For example, this occurs when parsing XML elements, where a composite element can have any number of children elements each of which is another element.XBNF provides the postfix operators * and + that apply to a clause component X and define that the parse may recognize sequences of occurrences of X. In the case of *, there may be 0 or more occurrences of X and in the case of + there may be 1 or more occurrences of X. If X synthesizes a value each time it is used in a parse then the use of * and + synthesizes sequences of values, each value in the sequence is a value syntheiszed by X.
The following gramar shows an example of the use of *. XML elements are trees; each node in the tree has a label. The following grammar recognizes XML (without attributes and text elements) trees and synthesizes an XMF sequence representation for the XML tree. The grammar shows the use of the builtin non-terminal Name that parses XMF names and synthesizes the name as a string:
@Grammar
Element ::=
SingleElement
| CompositeElement.
SingleElement ::=
'<' tag = Name '/>'
{ Seq{tag} }.
CompositeElement ::=
'<' tag = Name '>'
children = Element*
'<' Name '/>'
{ Seq{tag | children} }.
end
Specializing Grammars
Grammars may be specialized by adding extra clauses or extending existing clauses. A grammar has an extends clause that is followed by comma separatedreferences to parent grammars. The newly defined grammar is the merge of the parent grammars and the newly defined clauses. Any clauses in the parents or in the body of the new definition that have the same names are merged into a single clause using the | operator.For example, suppose we want to extend the grammar for XML given in the previoussection with the ability to recognize text elements. A text element is supplied as a string within string-quotes. XBNF provides a built-in non-terminal Str for strings. The new grammar is defined as follows:
@Grammar extends XML
Element ::= Str.
end
@Grammar
Element ::=
SingleElement
| CompositeElement
| Str.
SingleElement ::=
'<' tag = Name '/>'
{ Seq{tag} }.
CompositeElement ::=
'<' tag = Name '>'
children = Element*
'<' Name '/>'
{ Seq{tag | children} }.
end
Synthesizing Syntax
The examples given above show how a stream of characters can be processed by a grammar where the actions of the grammar perform tasks. The tasks have involved arithmetic expressions and the contruction of sequences. In general, a grammar is used by a parser to process the input characters to perform a task that involves fairly complex processing. The task can be completely specified in an XBNF grammar, however it is better to split the task into (at least) two distinct chunks: the parsing part that constructs a data structure from the input and then a processing part that interprets the data structure in some way. If the data structure closely resembles the structure of the input then the parsing actions do not need to do much: just chop up the input (concrete syntax) and create appropriate instances of the data structures (abstract syntax).Suppose we want to modify the calculator language defined above to produce the arithmetic expression rather than execute the expressions directly. Assuming that the OCL package is imported:
@Grammar
Calc ::= Mult '=' 'end'.
Mult ::= n1 = Add (
'*' n2 = Mult { BinExp(n1,"*",n2) }
| '/' n2 = Mult { BinExp(n1,"/",n2) }
| { n1 }
).
Add ::= n1 = IntExp (
'+' n2 = Add { BinExp(n1,"+",n2) }
| '-' n2 = Add { BinExp(n1,"-",n2) }
| { n1 }
).
IntExp :: n = Int { IntExp(n) }.
end
@Grammar
Calc ::= Mult '=' 'end'.
Mult ::= n1 = Add (
'*' n2 = Mult { [| <n1> * <n2> |] }
| '/' n2 = Mult { [| <n1> / <n2> |] }
| { n1 }
).
Add ::= n1 = IntExp (
'+' n2 = Add { [| <n1> + <n2> |] }
| '-' n2 = Add { [| <n1> - <n2> |] }
| { n1 }
).
IntExp ::= n = Int { n.lift() }.
end
To top.
Simple Language Constructs
A language driven approach to modelling involves working with and developing high-level languages that allow us to focus on the the what, rather than the how of applications. Languages can be defined in terms of abstract syntax models or concrete syntax models. Grammars support the definition of concrete syntax models and are used extensively throughout the rest of this book.A grammar for a language construct synthesizes instances of an abstract syntax model. So long as the model has an execution engine then the synthesized data can be executed. The syntax model and engine may be provided, such as XOCL, or may be one that has been defined specially for the application; the latter case is referred to as a domain specific language.
This section provides many examples of new language constructs that are defined using grammars. All of the syntax constructs are provided as part of the XMF system and implemented as extensions to the basic XOCL language. In most cases the grammars synthesize instances of the XOCL model and can therefore be viewed as syntactic sugar. All of the language constructs are general purpose and should not really be viewed as domain specific; however the techniques can be used when defining domain specific constructs.
To top.
When
When is a simple language construct that behaves as sugar for an if-expression:@When guard do
action
end
@Grammar extends OCL::OCL.grammar
When ::= guard = Exp 'do' action = Exp {
[| if <guard>
then <action>
else "GUARD FAILS"
end
|]
}.
end
Cond
Sometimes, if-expresions can become deeply nested and difficult to read. A cond-expression is equivalent to a nested if-then-else, but promotes all of the if-then parts to the top level:@Cond
test1 do exp1 end
test2 do exp2 end
...
testn do expn end
else exp
end
@Grammar extends OCL::OCL.grammar
Cond ::= clauses = Clause* otherwise = Otherwise 'end' {
Cond(clauses,otherwise)
}.
Clause ::= guard = Exp 'do' body = Exp 'end' {
CondClause(guard,body)
}.
Otherwise ::= 'else' Exp |
{ [| self.error("No else clause.") |] }.
end
@Operation desugar():Performable
clauses->reverse->iterate(clause exp = otherwise |
[| if <clause.guard()>
then <clause.body()>
else <exp>
end
|])
end
Classes and Packages
Classes and packages are name spaces: they both contain named elements. Although name spaces can contain any type of named element, packages and classes are examples of name spaces that know about particular types of named element. Packages know about sub-packages and classes. Classes know about attributes, operations and constraints.To allow extensibility, name space definitions contain sequences of element definitions. An element is added to a name space through the add operation. Packages and classes are examples of name spaces that hijack the add operation in order to manage particular types of known named elements. The rest of this section defined package and class definition language features.
A second feature of name spaces is mutual-reference. Definitions within a name space may refer to each other and the references must be resolved. For example, this occurs when attribute types refer to classes in the same package and when classes inherit from each other in a package definition. One way to achieve resolution is via two-passes. The named elements are added to the name space but all references are left unresolved. A second pass then replaces all references with direct references ot the apropriate named elements. This section shows how this two-pass system can be implemented.
Firstly, consider a package definition:
@Package P
@Class A
@Attribute b : B end
end
@Class B extends A
@Attribute a : A end
end
end
Package("P")
.add(@Class A ... end)
.add(@Class B ... end)
@Grammar extends OCL::OCL.grammar
Package ::= n = Name defs = Exp* 'end' {
defs->iterate(def package = [| Package(<n.lift()>) |] |
[| <package>.add(<def>) |])
}.
end
For example, attributes are defined by the following grammar (simplified so that types are just names):
@Grammar
Attribute::= n = Name ':' t = Name 'end' {
[| Attribute(<n.lift()>).type := <t.lift()> |]
}.
end
@Operation init()
if type.isKindOf(String)
then self.type := owner.resolveRef(type)
end;
super()
end
@Grammar extends OCL::OCL.grammar
Class ::= n = Name ps = Parents defs = Exp* 'end' {
ps->iterate(p classDef =
defs->iterate(def classDef = [| Class(<n.lift>) |]
| [| <classDef>.add(<def>) |])
| [| <classDef>.addParent(<p.lift>) |])
}.
Parents ::=
'extends' n = Name ns = (',' Name)* { Seq{n|ns} }
| { Seq{"Object"}.
end
The context rule is as follows:
Context ::= 'context' nameSpace = Exp def = Exp {
[| let namedElement = <def>;
nameSpace = <nameSpace>
in nameSpace.add(namedElement);
namedElement.init()
end |]
}.
@Attribute <NAME> : <TYPE> [ = <INITEXP> ] [ <MODIFIERS> ] end
An attribute within a class definition may supply modifiers. The modifiers cause the slot to have properties and create operations within the class. Attribute modifiers have the following form:
'(' <ATTMOD> (',' <ATTMOD>)* ')'
- ? defines an accesor operation for the attribute. For an attribute named n, an operation named n() is created that returns the value of the slot in an instance of the class.
- ! defines an updater operation for the attribute. For an attribute named n, an operation named setN(value) is created that sets the value of the slot named n to value in an instance of the class.
- + defines an extender operation for the attribute providing that the type is a collection. For an attribute named n, an operation named addToN(value) is created that extends the collection in the slot of an instance of the class. If the type of the attribute is a sequence type then the new value is added to the end of the sequence.
- - defines a reducer operation for the attribute providing that the type is a collection. For an attribute named n, an operation named deleteFromN(value) is created that extends the collection in the slot of an instance of the class.
- ^ makes the slot a container providing that the type inherits from XCore::Contained. In this case values in the slot will be contained objects with a slot named owner. Using the updater and extender operations to update the slot defined by this attribute will modify the owner slot when the value is added. It is often desirable to maintain linkage between a contained value and its owner, this attribute modifier manages this relationship automatically providing that the slot is always modified through the updater and extender.
@Grammar extends OCL::OCL.grammar
Attribute ::=
name = AttName // Name may be a string.
meta = MetaClass // Defaults to XCore::Attribute
':' type = AttType // An expression.
init = AttInit // Optional init value.
mods = AttMods // Optional modifiers.
'end' { Attribute(name,mult,type,init,mods,meta) }.
AttInit ::=
// Init values may be dynamic or static. A
// dynamic init value is evaluated each time
// a slot is created. A static init value is
// evaluated once when the attribute is
// created and used for all slots.
'=' Exp
| '=' 'static' e = Exp { [| static(<e>) |] }
| { null }.
AttMods ::=
mods = { AttributeModifiers() }
[ '(' AttModifier^(mods) (',' AttModifier^(mods))* ')' ]
{ mods }.
AttModifier(mods) ::=
mod = AttMod { mods.defineModifier(mod) }.
AttMod ::=
'?' { "?" }
| '!' { "!" }
| '+' { "+" }
| '-' { "-" }
| '^' { "^" }.
AttType ::= n = AttTypeName AttTypeTail^(n).
AttTypeName ::=
n = Name ns = ('::' Name)* { Path(Var(n),ns) }.
AttTypeTail(n) ::=
'(' args = CommaSepExps ')' { Apply(n,args) }
| { n }.
AttName ::= Name | Str.
MetaClass ::= 'metaclass' Exp | { null }.
end
Operations
An operation has a basic form of:@Operation [ <NAME> ] <ARGS> <BODY> end
- Operations have properties (allowing them to behave like objects with slots). The properties are optional and specified between the operation namd and its arguments.
- Arguments have optional types, specified as : <EXP> following the argument name.
- Argument names may be specified as XOCL patterns. The pattern matches the supplied argument value.
- Arguments are positional except in the case of an optional multiargs specification as the last argument position. A multiargs specification follows the special designator '.'. This is a mechanism that allows operations to take any number of arguments. Arguments are matched positionally and it is an error to supply less than the number of declared positional arguments to an operation. If more than the expected positional arguments are supplied and if the operation has a multiargs designator then all the remaining supplied argument values are supplied as a sequence and matched to the multiargs pattern.
context Root
@Enum Colour(Red,Green,Amber) end
context Root
@Enum Position(North,South,East,West) end
context Root
@Class TrafficLight
@Attribute colour : Colour (?,!) end
@Attribute position : Position (?,!) end
@Constructor(colour,position) ! end
end
context Root
@Class RoadCrossing
@Attribute light1 : TrafficLight
= TrafficLight(Colour::Red,Position::North)
end
@Attribute light2 : TrafficLight
= TrafficLight(Colour::Red,Position::South)
end
@Attribute light3 : TrafficLight
= TrafficLight(Colour::Green,Position::East)
end
@Attribute light4 : TrafficLight
= TrafficLight(Colour::Green,Position::West)
end
@Operation cycle()
// Cycle the traffic lights...
end
end
This is a good example of the use of meta-classes to implement new types. The following class definition declares a class Enum; its definition shows a number of useful features when declaring new meta-classes. Firstly the definition:
context XCore
@Class Enum extends Class
@Attribute names : Seq(String) (?) end
@Constructor(name,names) !
@For name in names do
self.addName(name)
end
end
@Operation default()
self.getElement(names->head)
end
@Operation defaultParents():Set(Classifier)
Set{NamedElement}
end
@Operation add(n)
if n.isKindOf(String)
then self.addName(n)
else super(n)
end
end
@Operation addName(name:String)
self.names := names->including(name);
self.add(self(name))
end
end
In addition to the names being recorded as the value of the slot names, each name gives rise to an instance of the enumerated type. Each instance of the enumerated type is a new value that has a name, therefore Colour must inherit from NamedElement. When a meta-class is defined it may redefine the operation inherited from Class called defaultParents. This must return a set of classifiers that instances of the meta-class will inherit from by default. In the case of Enum, any instance of Enum is declared to inherit from NamedElement, therefore the following creates two named elements that are instances of the class Colour: Colour(``Red'') and Colour(``Green'').
The operation Enum::addName is called for each of the names declared for the enumerated type. An instance of Enum is a class therefore it is a name-space and can contain named elements. the operation addName created an instance of the enumerated type self(name) and then adds it to itself. Each name declared for the enumerated type gives rise to a new instance which is named and is guaranteed to be different (when compared with =) to any other value.
A grammar for Enum completes the definition of enumerated types. Notice that, since an enumerated type is a class it can contain attributes and operations in its body:
@Grammar extends OCL::OCL.grammar
Enum ::= n = Name ns = Names es = Exp* 'end' {
exps->iterate(e x = [| Enum(<name.lift()>,<names.lift()>) |] |
[| <x>.add(<e>) |])
}.
Names ::= '(' n = EnumName ns = (',' EnumName)* ')' {
Seq{n | ns}
}.
EnumName ::=
Name
| Str.
end
Binding and Looping Constructs
New syntax constructs are generally used to abstract away from implementation details. They can hide the plumbing that is required to implement a given construct and make the meaning of the construct clear by adding keywords and grouping structure such as parentheses. Sometimes a syntax construct is defined so that it is simply more succinct than using the required expressions required to construct the required data values. For example, it may be more appealing to use an @Record ... end structure than creating the underlying records and fields explicitly. In these cases the new syntax construct is just hiding the details of data construction.Another category of syntax construct hides away the details of control flow and the details of how variables get bound. Typically, control based syntax constructs have to do more work because of the complexity in establishing variables in the correct scope and getting the control flow right. For example, there are lots of variations on looping constructs that use a controlled variable and hide away the details of testing for exit and rebinding the variable each time round the loop.
This section describes a number of control-flow and binding syntax constructs.
To top.
AbstractOp
Letrec
For
A @For-loop has a syntax construct defined by XOCL::For. When you define a @For-loop, the system creates an instance of XOCL::For and then desugars it. The @For-loop is a good example of a fairly extensive syntax transformation since the construct has many different forms controlled by a number of keywords. This section defines the syntax of a @For-loop and provides the definition of one of the syntax transformations.The class XOCL::For has the following attribites:
@Attribute names : Seq(String) end // The controlled variables.
@Attribute directive : String end // The type of for loop.
@Attribute guard : Performable end // A guard on the bound element (null = true).
@Attribute colls : Seq(Performable) end // The collections.
@Attribute body : Performable end // The body performed in the scope of 'name'.
@Attribute isExp : Boolean end // Returns a value.
@Grammar extends OCL::OCL.grammar
For ::=
// A for-loop may bind a sequence of names...
names = ForNames
// The directive defines how to interpret the
// collections...
dir = ForDirective
// A collection for each of the names...
colls = ForColls
// An optional boolean expression that
// must be true in order to process
// the names...
guard = ('when' Exp | { null })
// We are either performing commands or
// producing a sequence of values...
isExp = ForType
// The body - either a command or an
// expression..
body = Exp
'end'
{ For(names,dir,colls,guard,body,isExp) }.
ForColls ::= exp = Exp exps = (',' Exp)* { Seq{exp | exps} }.
ForDirective ::=
// The directive controls how the collections
// are processed...
// A walker produces instances of named types...
'classifiedBy' { "classifiedBy" }
// Select elements from the collections...
| 'in' { "in" }
// Reverse the collections...
| 'inReverse' { "inReverse" }
// Get keys from a table...
| 'inTableKeys' { "inTableKeys" }
// Get values from a table...
| 'inTableValues' { "inTableValues" }
// Call ->asSeq...
| 'inSeq' { "inSeq" }.
ForNames ::=
name = AName
names = (',' AName)*
{ Seq{name | names} }.
ForType ::=
'do' { false }
| 'produce' { true }.
end
context XOCL::For
@Operation desugarInAction():Performable
[| let <self.collBindings()>
in let isFirst = true
in @While not <self.emptyCollCheck()> do
let <self.bindNames()>
in <self.takeTails()>;
let isLast = <self.emptyCollCheck()>
in <self.protect(body)>;
isFirst := false
end
end
end
end
end
|]
end
context XOCL::For
@Operation bindNames():Seq(ValueBinding)
// Bind each of the controlled variables to the head of the
// corresponding collection.
0.to(names->size-1)->collect(i |
ValueBinding(names->at(i),[| <Var("forColl" + i)> ->head |]))
end
context XOCL::For
@Operation collBindings():Seq(ValueBinding)
// Returns a sequence of bindings that guarantee the collections
// are all sequences...
0.to(colls->size-1)->collect(i |
ValueBinding("forColl" + i,[| <colls->at(i)> ->asSeq |]))
end
context XOCL::For
@Operation emptyCollCheck():Performable
// Check that any of the collection variables is empty...
1.to(colls->size-1)->iterate(i test = [| <Var("forColl0")> ->isEmpty |] |
[| <test> or <Var("forColl" + i)> ->isEmpty |])
end
context XOCL::For
@Operation protect(exp:Performable)
// If the guard is defined then check it before performing
// the expression. Otherwise perform the expression. If
// we are an expression then add the result of the expression
// to the for results; these will be returned at the end of
// the evaluation.
if guard = null
then
if isExp
then [| forResults := Seq{<exp> | forResults} |]
else exp
end
else
if isExp
then [| if <guard>
then forResults := Seq{<exp> | forResults}
end |]
else [| if <guard>
then <exp>
end |]
end
end
end
context XOCL::For
@Operation takeTails():Performable
1.to(colls->size-1)->iterate(i updates = [| <Var("forColl0")> := <Var("forColl0")> ->tail |] |
[| <updates>; <Var("forColl" + i)> := <Var("forColl" + i)> ->tail |])
end
Find
Count
Case Analysis
TypeCase
Case
XBNF Grammar
@Grammar extends OCL::OCL.grammar
XBNF_Action ::=
// An action is either an expression in { and }
// which synthesizes a value or is a predicate that
// must be true for the parse to proceed...
'{' exp = Exp '}'
{ PreAction(exp) }
| '?' boolExp = Exp
{ PrePredicate(boolExp) }.
XBNF_Atom ::=
// An atom is the basic unit of a clause...
XBNF_Action
| XBNF_Literal
| XBNF_Call
| '(' XBNF_Disjunction ')'.
XBNF_Binding ::=
// A clause binding performs a grammar action and
// associates the value produced with a named
// local...
name = Name '=' atom = XBNF_Sequence {
And(atom,Bind(name))
}.
XBNF_Call ::=
// Call a clause. The arguments are optional...
name = Name args = XBNF_CallArgs { Call(name,args) }.
XBNF_CallArgs ::=
// Arguments supplied to a clause are optional.
// The args must be preceded by a ^ to distinguish
// the args from a pair of calls with 0 args...
'^' '(' n = Name ns = (',' Name)* ')' { Seq{n|ns} }
| { Seq{} }.
XBNF_Clause ::=
name = Name args = XBNF_ClauseArgs '::=' body = XBNF_Disjunction '.'
{ Clause(name,args,body) }.
XBNF_ClauseArgs ::=
'(' n = Name ns = (',' Name)* ')' { Seq{n|ns} }
| { Seq{} }.
XBNF_Conjunction ::=
// Conjunction is just a sequence of 1 or more
// clause elements...
elements = XBNF_Element+ {
elements->tail->iterate(e conj = elements->head |
And(conj,e))
}.
XBNF_Disjunction ::=
// A disjunction is a sequence of elements
// separated by | ...
element = XBNF_Conjunction (
'|' rest = XBNF_Disjunction { Or(element,rest) } | { element }).
XBNF_Element ::=
XBNF_Optional
| XBNF_Binding
| XBNF_Sequence.
Grammar ::=
parents = XBNF_GrammarParents
imports = XBNF_GrammarImports
clauses = XBNF_Clause*
'end'
{ Grammar(parents,clauses->asSet,"",imports) }.
XBNF_GrammarImports ::=
// The imports of a grammar affect the grammars that are
// available via @...
'import' class = Exp classes = (',' Exp)* { Seq{class | classes} }
| { Seq{} }.
XBNF_GrammarParents ::=
// A grammar may inherit from 0 or more parent grammars.
// The parent clauses are added to the child...
'extends' parent = Exp parents = (',' Exp)* { parents->asSet->including(parent) }
| { Set{} }.
XBNF_Literal ::=
// The following literals are built-in non-terminals of a
// grammar. The action uses getElement to reference the
// classes (and therefore the constructors) because a grammar
// cannot reference a variable with the same name as a terminal
// in an action...
// Get the next character...
'Char' { (Parser::BNF.getElement("Char"))() }
// Get the next line...
| 'Line' { (Parser::BNF.getElement("Line"))() }
// Get a string...
| 'Str' { (Parser::BNF.getElement("Str"))() }
// Get a terminal (in ' and ')...
| 'Terminal' { (Parser::BNF.getElement("Term"))() }
// Return the current token...
| 'Token' { (Parser::BNF.getElement("Tok"))() }
// Get an integer...
| 'Int' { (Parser::BNF.getElement("Int"))() }
// Get a float...
| 'Float' { (Parser::BNF.getElement("Float"))() }
// Get a name...
| 'Name' { (Parser::BNF.getElement("Name"))() }
// Expect end-of-file...
| 'EOF' { (Parser::BNF.getElement("EOF"))() }
// Throw away all choice points created since starting
// the current clause...
| '!' { (Parser::BNF.getElement("Cut"))() }
// Dispatch to the grammar on the most recently
// synthesized value which should be a sequence of
// names the represent a path to a classifier with
// respect to the currently imported name-spaces...
| '@' { (Parser::BNF.getElement("At"))() }
// Add a name-space to the currently imported
// name-spaces...
| 'ImportAt' { (Parser::BNF.getElement("ImportAt"))() }
// Get the current state of the parsing engine...
| 'pState' { (Parser::BNF.getElement("PState"))() }
// Get the current line position...
| 'LinePos' { (Parser::BNF.getElement("LinePos"))() }
// Define a new terminal in the form NewToken(NAME,ID)...
| XBNF_NewToken
// Get a terminal name...
| terminal = Terminal { (Parser::BNF.getElement("Terminal"))(terminal) }.
XBNF_NewToken ::=
// A new token is defined as a name and an integer id.
// The tokenizer used to parse the grammar is responsible
// for returning a token with the type set to the id...
'NewToken' '(' n = Name ',' t = Int ')' {
(Parser::BNF.getElement("NewToken"))(n,t)
}.
XBNF_Optional ::=
// An optional clause element is tried and ignored if it fails...
'[' opt = XBNF_Disjunction ']'
{ Opt(opt) }.
XBNF_Path ::= name = Name names = ('::' Name)* { Seq{name | names} }.
XBNF_TypeCheck ::=
// An element that checks the type of the synthesized value...
element = XBNF_Atom (':' type = XBNF_Path { And(element,TypeCheck(type)) }
| { element }).
XBNF_Sequence ::=
// An element an be followed by a * or a + for 0
// or more and 1 or more repetitions...
element = XBNF_TypeCheck (
'*' { StarCons(element) }
| '+' { PlusCons(element) }
| { element }
).
end