The following chapter demonstrates how to integrate your own DSL with Java. We will do this in four stages: First, you will learn how to refer to existing Java elements from within your language. Then you will use Xbase to refer to generic types. In the third step, you will map your own DSL's concepts to Java concepts. Last but not least, you will use both Java types and your concepts within Xbase expressions and execute it.
Throughout this chapter, we will step by step improve the domain model example from the tutorial.
In the following, we are going to use the JVM types model and the Xbase language library. To make these available to your language, you have to add a couple of dependencies. In the runtime plug-in of your language, add dependencies to
Your UI plug-in needs dependencies to
Additionally, you have to register the genmodel of Xbase to the standalone setup of your MWE2 workflow and make sure the TypesGeneratorFragment (src) and the XbaseGeneratorFragment (src) are included:
bean = StandaloneSetup {
registerGenModelFile =
"platform:/resource/org.eclipse.xtext.xbase/model/Xbase.genmodel"
}
...
fragment = types.TypesGeneratorFragment {}
fragment = xbase.XbaseGeneratorFragment {}
To avoid running out of memory when regenerating, you increase the maximum heap size and the permanent generation space in the run configuration of your workflow. We recommend at least
-Xmx512m -XX:MaxPermSize=128m
in the VM Arguments section of the Arguments tab. If you are experiencing ambiguity warnings from Antlr, the usual countermeasures apply.
A common use case when developing languages is the requirement to refer to existing concepts of other languages. Xtext makes this very easy for other self defined DSLs. However, it is often very useful to have access to the available types of the Java Virtual Machine. The JVM types Ecore model enables clients to do exactly this. It is possible to create cross-references to classes, interfaces, and their fields and methods. Basically every information about the structural concepts of the Java type system is available via the JVM types. This includes annotations and their specific values and enumeration literals as well.
The implementation will be selected transparently depending on how the client code is executed. If the environment is a plain stand-alone Java or OSGi environment, the java.lang.reflect API will be used to deduce the necessary data. On the contrary, the type-model will be created from the live data of the JDT in an interactive Eclipse environment. All this happens transparently for the clients behind the scenes via different implementations that are bound to specific interfaces by means of Google Guice.
Using the JVM types model is very simple. First of all, the grammar has to import the JavaVMTypes Ecore model. Thanks to content assist this is easy to spot in the list of proposals.
import "http://www.eclipse.org/xtext/common/JavaVMTypes" as jvmTypes
The next step is to actually refer to an imported concept. Let's define a mapping to available Java types for the simple data types in the domain model language. This can be done with a simple cross-reference:
// simple cross reference to a Java type
DataType:
'datatype' name=ID
'mapped-to' javaType=[jvmTypes::JvmType|QualifiedName];
After regenerating your language, it will be allowed to define a type Date that maps to the Date like this:
datatype Date mapped-to java.util.Date
These two steps will provide a nice integration into the Eclipse JDT. There is Find References on Java methods, fields and types that will reveal results in your language files. Go To Declaration works as expected and content assist will propose the list of available types. Even the import statements will also apply for Java types.
There are several customization hooks in the runtime layer of the JVM types and on the editor side as well:
The AbstractTypeScopeProvider (src) can be used to create scopes for members with respect to the override semantics of the Java language. Of course it is possible to use this implementation to create scopes for types as well.
As the Java VM types expose a lot of information about visibility, parameter- and return types, generics, available annotations or enumeration literals, it is very easy to define constraints for the referred types.
The ITypesProposalProvider (src) can be used to provide optimized proposals based on various filter criteria. The most common selector can be used directly via createSubTypeProposals(..). The implementation is optimized and uses the JDT Index directly to minimize the effort for object instantiation. The class TypeMatchFilters (src) provides a comprehensive set of reusable filters that can be easily combined to reduce the list of proposals to a smaller number of valid entries.
While the JVM types approach from the previous chapter allows to refer to any Java element, it is quite limited when it comes to generics. Usually, a type reference in Java can have type arguments which can also include wildcards, upper and lower bounds etc. A simple cross-reference using a qualified name is not enough to express neither the syntax nor the structure of such a type reference.
Xbase offers a parser rule JvmTypeReference which supports the full syntax of a Java type reference and instantiates a JVM element of type JvmTypeReference (src). So let us start by inheriting from Xbase
grammar org.eclipse.xtext.example.Domainmodel
with org.eclipse.xtext.xbase.Xbase
As we can express all kinds of Java type references directly now, the indirection for DataTypes is no longer necessary. To eliminate it, all cross-references to Types have to be replaced by calls to the JvmTypeReferences rule. The rules DataType, Type, and QualifiedName become obsolete (the latter is already defined in Xbase), and the Type in AbstractEntity must be changed to Entity. The whole grammar now reads as
grammar org.eclipse.xtext.example.Domainmodel with
org.eclipse.xtext.xbase.Xbase
generate domainmodel "http://www.eclipse.org/xtext/example/Domainmodel"
import "http://www.eclipse.org/xtext/common/JavaVMTypes" as jvmTypes
Domainmodel:
(elements += AbstractElement)*
;
PackageDeclaration:
'package' name = QualifiedName '{'
(elements += AbstractElement)*
'}'
;
AbstractElement:
PackageDeclaration | Entity | Import
;
Import:
'import' importedNamespace = QualifiedNameWithWildcard
;
QualifiedNameWithWildcard:
QualifiedName '.*'?
;
Entity:
'entity' name = ID
('extends' superType = JvmTypeReference)?
'{'
(features += Feature)*
'}'
;
Feature:
(many ?= 'many')? name = ID ':' type = JvmTypeReference
;
As we changed the grammar, we have to regenerate the language now.
Being able to parse a Java type reference is already nice, but we also have to write them back to their string representation when we generate Java code. Into the bargain, a generic type reference with fully qualified class names can become a bit bulky. The ImportManager (src) shortens fully qualified names, keeps track of imported namespaces, avoids name collisions, and helps to serialize JvmTypeReferences (src) by means of the TypeReferenceSerializer (src). This utility encapsulates how type references may be serialized depending on the concrete context in the output.
The following snippet shows our code generator using an ImportManager (src) in conjunction with as TypeReferenceSerializer (src). We create a new instance and pass it through the generation functions, collecting types on the way. As the import section in a Java file precedes the class body, we create the body into a String variable and assemble the whole file's content in a second step.
class DomainmodelGenerator implements IGenerator {
@Inject extension IQualifiedNameProvider
@Inject extension TypeReferenceSerializer
override void doGenerate(Resource resource, IFileSystemAccess fsa) {
for(e: resource.allContentsIterable.filter(typeof(Entity))) {
fsa.generateFile(
e.fullyQualifiedName.toString.replace(".", "/") + ".java",
e.compile)
}
}
def compile(Entity it) '''
«val importManager = new ImportManager(true)»
«val body = body(importManager)»
«IF eContainer != null»
package «eContainer.fullyQualifiedName»;
«ENDIF»
«FOR i:importManager.imports»
import «i»;
«ENDFOR»
«body»
'''
def body(Entity it, ImportManager importManager) '''
public class «name» «IF superType != null»
extends «superType.shortName(importManager)» «ENDIF»{
«FOR f : features»
«f.compile(importManager)»
«ENDFOR»
}
'''
def compile(Feature it, ImportManager importManager) '''
private «type.shortName(importManager)» «name»;
public «type.shortName(importManager)»
get«name.toFirstUpper»() {
return «name»;
}
public void set«name.toFirstUpper»(
«type.shortName(importManager)» «name») {
this.«name» = «name»;
}
'''
def shortName(JvmTypeReference ref,
ImportManager importManager) {
val result = new StringBuilderBasedAppendable(importManager)
ref.serialize(ref.eContainer, result);
result.toString
}
}
In many cases, you will want your DSLs concepts to be usable as Java elements. E.g. an Entity will become a Java class and should be usable as such. In the domain model example, you can write
entity Employee extends Person {
boss: Person
...
entity Person {
friends: List<Person>
...
i.e. use entities instead of Java types or even mix Java types as List with entities such as Person. One way to achieve this is to let your concepts inherit from a corresponding JVM type, e.g. let Entity inherit from JvmGenericType (src). But this would result in a lot of accidentally inherited properties in your domain model. In Xbase there is an alternative: You can simply define how to derive a JVM model from your model. This inferred JVM model is the representation of your concepts in the typesystem of Xbase.
The main component for the inferred JVM model is the IJvmModelInferrer (src). It has a single method that takes the root model element as an argument and produces a number of JvmGenericTypes (src). As Xbase cannot guess how you would like to map your concepts to JVM elements, you have to implement this component yourself. This usually boils down to using an injected JvmTypesBuilder (src) to create a hierarchy of JVM elements. The builder (src) helps to initialize the produced types with sensible default and encapsulates the logic that associates the source elements with the derived JVM concepts. As this kind of transformation can be elegantly implemented using polymorphic dispatch functions and extension methods, it is a good choice to write the IJvmModelInferrer (src) in Xtend. It becomes even simpler if you inherit from the AbstractModelInferrer (src) which traverses the input model and dispatches to its contents until you decide which elements to handle.
For our domain model example, we implement a polymorphic dispatch function infer for Entities to transform them into a JvmGenericType (src). The produced type holds a JvmOperation (src) for each Operation and a JvmField (src) plus accessors for each Property. The resulting DomainmodelJvmModelInferrer looks like this:
class DomainModelJvmModelInferrer extends AbstractModelInferrer {
/**
* a builder API to programmatically create Jvm elements
* in readable way.
*/
@Inject extension JvmTypesBuilder
@Inject extension IQualifiedNameProvider
def dispatch void infer(Entity element,
IAcceptor<JvmDeclaredType> acceptor,
boolean isPrelinkingPhase) {
acceptor.accept(element.toClass(element.fullyQualifiedName) [
documentation = element.documentation
for (feature : element.features) {
members += feature.toField(feature.name, feature.type)
members += feature.toSetter(feature.name, feature.type)
members += feature.toGetter(feature.name, feature.type)
}
])
}
}
As Java elements and your concepts are now represented as JVM model elements, other models can now transparently link to Java or your DSL. In other words, you can use a mapped element of your DSL in the same places as the corresponding Java type.
The Xbase framework will automatically switch between the JVM element or the DSL element when needed, e.g. when following hyperlinks. The component allowing to navigate between the source model and the JVM model is called IJvmModelAssociations (src), the read-only antagonist of the IJvmModelAssociator (src) that is used by the JvmTypesBuilder (src).
By default, the inferred model is indexed, so it can be cross referenced from other models.
Xbase is an expression language that can be embedded into Xtext languages. Its syntax is close to Java, but it additionally offers type inferrence, closures, a powerful switch expression and a lot more. For details on this expression langugae, please consult the reference documentation and the Xbase tutorial. Xbase ships with an interpreter and a compiler that produces Java code. Thus, it is easy to add executable behavior to your DSLs. As Xbase integrates tightly with Java, there is usually no additional code needed to run your DSL as part of a Java application.
If you want to refer to EClassifiers from the Xbase model, you need to import it first. The same holds for the common types model:
import "http://www.eclipse.org/xtext/xbase/Xbase" as xbase
Now identify the location in your grammar, where you want references to Java types and Xbase expression to appear and call the appropriate rules of the super grammar. Have a look at the domainmodel example: An Operation's parameters are JvmFormalParamters, its return type refers to a Java type and its body is an XBlockExpression, so its parser rule reads as
Operation:
'op' name=ValidID '('
(params+=JvmFormalParameter (',' params+=JvmFormalParameter)*)? ')'
':' type=JvmTypeReference
body=XBlockExpression;
If you're unsure which entry point to choose for your expressions, consider the XBlockExpression.
To integrate Operations in our models, we have to call this rule. We copy the previous Feature to a new rule Property and let Feature become the supertype of Property and Operation:
Feature:
Property | Operation
;
Property:
name = ID ':' type = JvmTypeReference
;
Note: You will have to adapt the IJvmModelInferrer (src) to these changes, i.e. rename Feature to Property and create a JvmOperation (src) for each Operation. We leave that as an exercise :-)
If you're done with that, everything should work out of the box. Since each expression is now logically contained in an operation (src), all the scoping rules and visibility constrains can be deduced from that. The framework will take care of the fact, that the operation's parameters are reachable in the operation body or that the declared return types are validated against the actual expression types.
There is yet another aspect of the JVM model that can be explored. Since all the coarse grained concepts such as types (src) and operations (src) were already derived from the model, a generator can be used to serialize that information to Java code. There is no need to write a code generator on top of that. The JvmModelGenerator (src) knows how to generate operation bodies properly.
Sometimes it is more convenient to interpret a model that uses Xbase than to generate code from it. Xbase ships with the XbaseInterpreter (src) which makes this rather easy.
An interpreter is essentially an external visitor, that recursively processes a model based on the model element's types. By now you should be aware that polymorphic dispatch is exactly the technology needed here. In the XbaseInterpreter (src), the dispatch method is called _evaluate<SomeDescription> and takes three parameters, e.g.
protected Object _evaluateBlockExpression(XBlockExpression literal,
IEvaluationContext context,
CancelIndicator indicator)
The IEvaluationContext keeps the state of the running application, i.e. the local variables and their values. Additionally, it can be forked, thus allowing to shadow the elements of the original context. Here is an example code snippet how to call the XbaseInterpreter (src):
@Inject private XbaseInterpreter xbaseInterpreter;
@Inject private Provider<IEvaluationContext> contextProvider;
...
public Object evaluate(XExpression expression, Object thisElement) {
IEvaluationContext evaluationContext = contextProvider.get();
evaluationContext.newValue(XbaseScopeProvider.THIS, thisElement);
IEvaluationResult result = xbaseInterpreter.evaluate(expression,
evaluationContext, CancelIndicator.NullImpl);
if (result.getException() != null) {
// handle exception
}
return result.getResult();
}
This document describes the expression language library Xbase. Xbase is a partial programming language implemented in Xtext and is meant to be embedded and extended within other programming languages and domain-specific languages (DSL) written in Xtext. Xtext is a highly extendable language development framework covering all aspects of language infrastructure such as parsers, linkers, compilers, interpreters and even full-blown IDE support based on Eclipse.
Developing DSLs has become incredibly easy with Xtext. Structural languages which introduce new coarse-grained concepts, such as services, entities, value objects or state-machines can be developed in minutes. However, software systems do not consist of structures solely. At some point a system needs to have some behavior, which is usually specified using so called expressions. Expressions are the heart of every programming language and are not easy to get right. On the other hand, expressions are well understood and many programming languages share a common set and understanding of expressions. That is why most people do not add support for expressions in their DSL, but try to solve this differently. The most often used workaround is to define only the structural information in the DSL and add behavior by modifying or extending the generated code. It is not only unpleasant to write, read and maintain information which closely belongs together in two different places, abstraction levels and languages. Also, modifying the generated source code comes with a lot of additional problems. But as of today this is the preferred solution since adding support for expressions (and a corresponding execution environment) for your language is hard - even with Xtext.
Xbase serves as a language library providing a common expression language bound to the Java platform (i.e. Java Virtual Machine). It consists of an Xtext grammar, as well as reusable and adaptable implementations for the different aspects of a language infrastructure such as an AST structure, a compiler, an interpreter, a linker, and a static analyzer. In addition it comes with implementations to integrate the expression language within an Xtext-based Eclipse IDE. Default implementations for aspects like content assistance, syntax coloring, hovering, folding and navigation can be easily integrated and reused within any Xtext based language.
Conceptually and syntactically, Xbase is very close to Java statements and expressions, but with a few differences:
Xbase comes with a small set of lexer rules, which can be overridden and hence changed by users. However the default implementation is carefully chosen and it is recommended to stick with the lexical syntax described in the following.
Identifiers are used to name all constructs, such as types, methods and variables. Xbase uses the default Identifier-Syntax from Xtext - compared to Java, they are slightly simplified to match the common cases while having less ambiguities. They start with a letter a-z, A-Z or an underscore followed by more of these characters or a digit 0-9.
Identifiers may not have the same spelling as any reserved keyword. However, identifiers starting with a ^ are so called escaped identifiers. Escaped identifiers are used in cases when there is a conflict with a reserved keyword. Imagine you have introduced a keyword service in your language but want to call a Java property service at some point. In such cases you use an escaped identifier ^service to reference the Java property.
terminal ID:
'^'? ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'_'|'0'..'9')*
;
String literals can either use single quotes (') or double quotes (") as their terminals. When using double quotes all literals allowed by Java string literals are supported. In addition new line characters are allowed, that is in Xbase all string literals can span multiple lines. When using single quotes the only difference is that single quotes within the literal have to be escaped and double quotes do not.
In contrast to Java, equal string literals within the same class do not neccessarily refer to the same instance at runtime.
//TODO
" the quick brown fox
jumps over the lazy dog."
Integer literals consist of one or more digits. Only decimal literals are supported and they always result in a value of type java.lang.Integer (it might result in native type int when translated to Java, see Types). The compiler makes sure that only numbers between 0 and Integer.MAX (0x7fffffff) are used.
There is no negative integer literal, instead the expression -23 is parsed as the prefix operator - applied to an integer literal.
terminal INT returns ecore::EInt:
('0'..'9')+
;
Xbase comes with two different kinds of comments: Single-line comments and multi-line comments. The syntax is the same as the one known from Java (see § 3.7 Comments).
terminal ML_COMMENT :
'/*' -> '*/'
;
terminal SL_COMMENT :
'//' !('\n'|'\r')* ('\r'? '\n')?
;
The white space characters ' ', '\t', '\n', and '\r are allowed to occur anywhere between the other syntactic elements.
The following list of words are reserved keywords, thus reducing the set of possible identifiers:
However, in case some of the keywords have to be used as identifiers, the escape character for identifiers comes in handy.
Basically all kinds of JVM types are available and referable.
Arrays cannot be declared explicitly, but they can be passed around and if needed are transparently converted to a List of the compound type.
In other words, the return type of a Java method that returns an array of ints (int[]) can be directly assigned to a variable of type java.util.List<java.lang.Integer> (in short List<Integer>). Due to type inference you can also defer the conversion. The conversion is bi-directional so any method, that takes an array as argument can be invoked with a List instead.
A simple type reference only consists of a qualified name. A qualified name is a name made up of identifiers which are separated by a dot (like in Java).
QualifiedName:
ID ('.' ID)*
;
There is no parser rule for a simple type reference, as it is expressed as a parameterized type references without parameters.
Xbase introduces closures, and therefore an additional function type signature. On the JVM-Level a closure (or more generally any function object) is just an instance of one of the types in org.eclipse.xtext.xbase.lib.Function*, depending on the number of arguments. However, as closures are a very important language feature, a special sugared syntax for function types has been introduced. So instead of writing Function1<String,Boolean> one can write (String)=>Boolean.
Primitives cannot be used in function types.
For more information on closures see section Xbase_Expressions_Closures.
XFunctionTypeRef:
('('JvmTypeReference (',' JvmTypeReference)*')')?
'=>' JvmTypeReference;
The general syntax for type references allows to take any number of type arguments. The semantics as well as the syntax is almost the same as in Java, so please refer to the third edition of the Java Language Specification.
The only difference is that in Xbase a type reference can also be a function type. In the following the full syntax of type references is shown, including function types and type arguments.
JvmTypeReference:
JvmParameterizedTypeReference |
XFunctionTypeRef;
XFunctionTypeRef:
('(' JvmTypeReference (',' JvmTypeReference)* ')')?
'=>' JvmTypeReference;
JvmParameterizedTypeReference:
type=QualifiedName ('<' JvmTypeArgument (',' JvmTypeArgument)* '>')?;
JvmTypeArgument:
JvmReferenceTypeArgument |
JvmWildcardTypeArgument;
JvmReferenceTypeArgument :
JvmTypeReference;
JvmWildcardTypeArgument:
'?' (JvmUpperBound | JvmLowerBound)?;
JvmLowerBound :
'super' JvmTypeReference;
JvmUpperBound :
'extends' JvmTypeReference;
Xbase supports all Java primitives. The conformance rules (e.g. boxing unboxing) are also exactly like defined in the Java Language Specification.
Conformance is used in order to find out whether some expression can be used in a certain situation. For instance when assigning a value to a variable, the type of the right hand expression needs to conform to the type of the variable.
As Xbase implements the unchanegd type system of Java it also fully supports the conformance rules defined in The Java Language Specification.
Because of type inference Xbase sometimes needs to compute the most common super type of a given set of types.
For a set [T1,T2,...Tn] of types the common super type is computed by using the linear type inheritance sequence of T1 and is iterated until one type conforms to each T2,..,Tn. The linear type inheritance sequence of T1 is computed by ordering all types which are part if the type hierarchy of T1 by their specificity. A type T1 is considered more specific than T2 if T1 is a subtype of T2. Any types with equal specificity will be sorted by the maximal distance to the originating subtype. CharSequence has distance 2 to StringBuilder because the supertype AbstractStringBuilder implements the interface, too. Even if StringBuilder implements CharSequence directly, the interface gets distance 2 in the ordering because it is not the most general class in the type hierarchy that implements the interface. If the distances for two classes are the same in the hierarchy, their qualified name is used as the compare-key to ensure deterministic results.
Expressions are the main language constructs which are used to express behavior and computation of values. The concept of statements is not supported, but instead powerful expressions are used to handle situations in which the imperative nature of statements would be helpful, too. An expression always results in a value (might be the value 'null' though). In addition every resolved expressions is of a static type.
A literal denotes a fixed unchangeable value. Literals for string, integers, booleans, null and Java types are supported.
A string literal as syntactically defined in section Xbase_Syntax_StringLiteral is a valid expression and returns an instance of java.lang.String of the given value.
"Hello
World !"
An integer literal as defined in section Xbase_Syntax_IntegerLiteral creates an int. There is no signed int. If you put a minus operator in front of an int literal it is taken as a UnaryOperator with one argument (the positive int literal).
There are two boolean literals, true and false which correspond to their Java counterpart of type boolean.
The null pointer literal is, like in Java, null. It is a member of any reference type.
Type literals are specified using the keyword typeof :
Type cast behave like casts in Java, but have a slightly more readable syntax. Type casts bind stronger than any other operator but weaker than feature calls.
The conformance rules for casts are defined in the Java Language Specification.
XCastedExpression:
Expression 'as' JvmTypeReference;
There are a couple of common predefined infix operators. In contrast to Java, the operators are not limited to operations on certain types. Instead an operator-to-method mapping allows users to redefine the operators for any type just by implementing the corresponding method signature. The following defines the operators and the corresponding Java method signatures / expressions.
e1 += e2 | e1._operator_add(e2) |
e1 || e2 | e1._operator_or(e2) |
e1 && e2 | e1._operator_and(e2) |
e1 == e2 | e1._operator_equals(e2) |
e1 != e2 | e1._operator_notEquals(e2) |
e1 < e2 | e1._operator_lessThan(e2) |
e1 > e2 | e1._operator_greaterThan(e2) |
e1 <= e2 | e1._operator_lessEqualsThan(e2) |
e1 >= e2 | e1._operator_greaterEqualsThan(e2) |
e1 -> e2 | e1._operator_mappedTo(e2) |
e1 .. e2 | e1._operator_upTo(e2) |
e1 + e2 | e1._operator_plus(e2) |
e1 - e2 | e1._operator_minus(e2) |
e1 * e2 | e1._operator_multiply(e2) |
e1 / e2 | e1._operator_divide(e2) |
e1 % e2 | e1._operator_modulo(e2) |
e1 ** e2 | e1._operator_power(e2) |
! e1 | e1._operator_not() |
- e1 | e1._operator_minus() |
If the operators || and && are used in a context where the left hand operand is of type boolean, the operation is evaluated in short circuit mode, which means that the right hand operand might not be evaluated at all in the following cases:
Local variables can be reassigned using the = operator. Also properties can be set using that operator: Given the expression
myObj.myProperty = "foo"
The compiler first looks up whether there is an accessible Java Field called myProperty on the type of myObj. If there is one it translates to the following Java expression :
myObj.myProperty = "foo";
Remember in Xbase everything is an expression and has to return something. In the case of simple assignments the return value is the value returned from the corresponding Java expression, which is the assigned value.
If there is no accessible field on the left operand's type, a method called setMyProperty(OneArg) (JavaBeans setter method) is looked up. It has to take one argument of the type (or a super type) of the right hand operand. The return value will be whatever the setter method returns (which usually is null). As a result the compiler translates to :
myObj.setMyProperty("foo")
A feature call is used to invoke members of objects, such as fields and methods, but also can refer to local variables and parameters, which are made available for the current expression's scope.
The following snippet is a simplification of the real Xtext rules, which cover more than the concrete syntax.
FeatureCall :
ID |
Expression ('.' ID ('(' Expression (',' Expression)* ')')?)*
Feature calls are directly translated to their Java equivalent with the exception, that for calls to properties an equivalent rule as described in section Xbase_Expressions_PropertyAssignment applies. That is, for the following expression
myObj.myProperty
the compiler first looks for an accessible field in the type of myObj. If no such field exists it looks for a method called myProperty() before it looks for the getter methods getMyProperty(). If none of these members can be found the expression is unbound and a compiliation error is thrown.
If the current scope contains a variable named this or it, the compiler will make all its members available to the scope. That is one of
it.myProperty
this.myProperty
As this is bound to the surrounding object in Java, it can be used in finer-grained constructs such as function parameters. That is why it.myProperty has higher precedence than this.myProperty. it is also the default parameter name in closures.
Checking for null references can make code very unreadable. In many situations it is ok for an expression to return null if a receiver was null. Xbase supports the safe navigation operator ?. to make such code more readable.
Instead of writing
if ( myRef != null ) myRef.doStuff()
one can write
myRef?.doStuff()
Construction of objects is done by invoking Java constructors. The syntax is exactly as in Java.
XConstructorCall:
'new' QualifiedName
('<' JvmTypeArgument (',' JvmTypeArgument)* '>')?
('('(XExpression (',' XExpression)*)?')')?;
A closure is a literal that defines an anonymous function. A closure also captures the current scope, so that any final variables and parameters visible at construction time can be referred to in the closure's expression.
XClosure:
'[' ( JvmFormalParameter (',' JvmFormalParameter)* )?
'|' XExpression ']';
The surrounding square brackets are optional if the closure is the single argument of a method invocation. That is you can write
myList.find(e|e.name==null)
instead of
myList.find([e|e.name==null])
But in all other cases the square brackets are mandatory:
val func = [String s| s.length>3]
Closures are expressions which produce function objects. The type is a function type, consisting of the types of the parameters as well as the return type. The return type is never specified explicitly but is always inferred from the expression. The parameter types can be inferred if the closure is used in a context where this is possible.
For instance, given the following Java method signature: public T <T>getFirst(List<T> list, Function0<T,Boolean> predicate)
the type of the parameter can be inferred. Which allows users to write: arrayList( "Foo", "Bar" ).findFirst( e | e == "Bar" ) instead of arrayList( "Foo", "Bar" ).findFirst( String e | e == "Bar" )
An Xbase closure is a Java object of one of the Function interfaces shipped with the runtime library of Xbase. There is an interface for each number of parameters (current maximum is six parameters). The names of the interfaces are
In order to allow seamless integration with existing Java libraries such as the JDK or Google Guava (formerly known as Google Collect) closures are auto coerced to expected types if those types declare only one method (methods from java.lang.Object don't count).
As a result given the method java.util.Collections.sort(List<T>, Comparator<? super T>) is available as an extension method, it can be invoked like this
newArrayList( 'aaa', 'bb', 'c' ).sort(
e1, e2 | if ( e1.length > e2.length ) {
-1
} else if ( e1.length < e2.length ) {
1
} else {
0
})
If a closure has a single parameter whose type can be inferred, the declaration of the parameter can be ommitted. Use it to refer to the parameter inside the closure's body.
val (String s)=>String function = [toUpperCase]
// equivalent to [it | it.toUpperCase]
An if expression is used to choose two different values based on a predicate. While it has the syntax of Java's if statement it behaves like Java's ternary operator (predicate ? thenPart : elsePart), i.e. it is an expression that returns a value. Consequently, you can use if expressions deeply nested within expressions.
XIfExpression:
'if' '(' XExpression ')'
XExpression
('else' XExpression)?;
An expression if (p) e1 else e2 results in either the value e1 or e2 depending on whether the predicate p evaluates to true or false. The else part is optional which is a shorthand for else null. That means
if (foo) x
is the a short hand for
if (foo) x else null
The type of an if expression is calculated by the return types T1 and T2 of the two expression e1 and e2. It uses the rules defined in section Xbase_Types_CommonSuperType.
The switch expression is a bit different from Java's. First, there is no fall through which means only one case is evaluated at most. Second, the use of switch is not limited to certain values but can be used for any object reference instead. For a switch expression
switch e {
case e1 : er1
case e2 : er2
...
case en : ern
default : er
}
the main expression e is evaluated first and then each case sequentially. If the switch expression contains a variable declaration using the syntax known from section Xbase_Expressions_ForLoop, the value is bound to the given name. Expressions of type java.lang.Boolean or boolean are not allowed in a switch expression.
The guard of each case clause is evaluated until the switch value equals the result of the case's guard expression or if the case's guard expression evaluates to true. Then the right hand expression of the case evaluated and the result is returned.
If none of the guards matches the default expression is evaluated an returned. If no default expression is specified the expression evaluates to null.
Example:
switch myString {
case myString.length>5 : 'a long string.'
case 'foo' : 'It's a foo.'
default : 'It's a short non-foo string.'
}
In addition to the case guards one can add a so called Type Guard which is syntactically just a type reference preceding the than optional case keyword. The compiler will use that type for the switch expression in subsequent expressions. Example:
var Object x = ...;
switch x {
String case x.length()>0 : x.length()
List<?> : x.size()
default : -1
}
Only if the switch value passes a type guard, i.e. an instanceof operation returns true, the case's guard expression is executed using the same semantics explained in previously. If the switch expression contains an explicit declaration of a local variable or the expression references a local variable, the type guard acts like a cast, that is all references to the switch value will be of the type specified in the type guard.
The return type of a switch expression is computed using the rules defined in section Xbase_Types_CommonSuperType. The set of types from which the common super type is computed corresponds to the types of each case's result expression. In case a switch expression's type is computed using the expected type from the context, it is sufficient to return the expected type if all case branches types conform to the expected type.
switch foo {
Entity : foo.superType.name
Datatype : foo.name
default : throw new IllegalStateException
}
switch x : foo.bar.complicated('hello',42) {
case "hello42" : ...
case x.length<2 : ...
default : ....
}
XSwitchExpression:
'switch' (ID ':')? XExpression '{'
XCasePart+
('default' ':' XExpression))?
'}';
XCasePart:
JvmTypeReference? ('case' XExpression)? ':' XExpression );
}
Variable declarations are only allowed within blocks. They are visible in any subsequent expressions in the block. Although overriding or shadowing variables from outer scopes is allowed, it is usually only used to overload the variable name 'this', in order to subsequently access an object's features in an unqualified manner.
A variable declaration starting with the keyword val denotes a so called value, which is essentially a final (i.e. unsettable) variable. In rare cases, one needs to update the value of a reference. In such situations the variable needs to be declared with the keyword var, which stands for 'variable'.
A typical example for using var is a counter in a loop.
{
val max = 100
var i = 0
while (i > max) {
println("Hi there!")
i = i +1
}
}
Variables declared outside a closure using the var keyword are not accessible from within a closure.
XVariableDeclaration:
('val' | 'var') JvmTypeReference? ID '=' XExpression;
The return type of a variable declaration expression is always void. The type of the variable itself can either be explicitly declared or be inferred from the right hand side expression. Here is an example for an explicitly declared type: var List<String> msg = new ArrayList<String>(); In such cases, the right hand expression's type must conform to the type on the left hand side.
Alternatively the type can be left out and will be inferred from the initialization expression: var msg = new ArrayList<String>(); // -> type ArrayList<String>
The block expression allows to have imperative code sequences. It consists of a sequence of expressions, and returns the value of the last expression. The return type of a block is also the type of the last expression. Empty blocks return null. Variable declarations are only allowed within blocks and cannot be used as a block's last expression.
A block expression is surrounded by curly braces and contains at least one expression. It can optionally be terminated by a semicolon.
{
doSideEffect("foo")
result
}
{
var x = greeting();
if (x.equals("Hello ")) {
x+"World!";
} else {
x;
}
}
XBlockExpression:
'{'
(XExpressionInsideBlock ';'?)*
'}';
The for loop for (T1 variable : iterableOfT1) expression is used to execute a certain expression for each element of an array of an instance of java.lang.Iterable. The local variable is final, hence canot be updated.
The return type of a for loop is void. The type of the local variable can be left out. In that case it is inferred from the type of the array or java.lang.Iterable returned by the iterable expression.
for (String s : myStrings) {
doSideEffect(s);
}
for (s : myStrings)
doSideEffect(s)
XForExpression:
'for' '(' JvmFormalParameter ':' XExpression ')'
XExpression
;
A while loop while (predicate) expression is used to execute a certain expression unless the predicate is evaluated to false. The return type of a while loop is void.
XWhileExpression:
'while' '(' predicate=XExpression ')'
body=XExpression;
while (true) {
doSideEffect("foo");
}
while ( ( i = i + 1 ) < max )
doSideEffect( "foo" )
A do-while loop do expression while (predicate) is used to execute a certain expression unless the predicate is evaluated to false. The difference to the while loop is that the execution starts by executing the block once before evaluating the predicate for the first time. The return type of a do-while loop is void.
XDoWhileExpression:
'do'
body=XExpression
'while' '(' predicate=XExpression ')';
do {
doSideEffect("foo");
} while (true)
Although an explicit return is often not necessary, it is supported. In a closure for instance a return expression is always implied if the expression itself is not of type void. Anyway you can make it explicit:
listOfStrings.map(e| {
if (e==null)
return "NULL"
e.toUpperCase
})
Like in Java it is possible to throw java.lang.Throwable. The syntax is exactly the same as in Java.
{
...
if (myList.isEmpty)
throw new IllegalArgumentException("the list must not be empty")
...
}
The try-catch-finally expression is used to handle exceptional situations. You are not forced to declare checked exceptions, if you don't catch checked exceptions they are rethrown in a wrapping runtime exception. Other than that the syntax again is like the one known from Java.
try {
throw new RuntimeException()
} catch (NullPointerException e) {
// handle e
} finally {
// do stuff
}
Languages extending Xbase might want to contribute to the feature scope. Besides that one can of course change the whole implementation as it seems fit there is a special hook, which can be used to add so called extension methods to existing types.
Xbase itself comes with a standard library of such extension methods adding support for various operators for the common types, such as java.lang.String, java.util.List, etc.
These extension methods are declared in separate Java classes. There are various ways how extension methods can be added. The simplest is, that the language designer predefines, which extension methods are available. This means, that language users cannot add additional library functions using this mechanism.
Another alternative is to have them looked up by a certain naming convention. Also for more general languages it is possible to let users add extension methods using imports or the like. This approach can be seen in the language Xtend2, where extension methods are lexically imported through static imports and/or dependency injection.
The precedence of extension methods is always lower than real member methods, that is you cannot override member features. Also the extension members are not invoked polymorphic. If you have two extension methods on the scope (foo(Object) and foo(String)) the expression (foo as Object).foo would bind and invoke foo(Object).
If the last argument of a method call is a closure, it can be appended to the method call. Thus,
foo(42) [String s | s.toUpperCase]
will call a Java method with the signature
void foo(int, Function1<String, String>)
Used in combination with the implicit parameter name in closures you can write extension libraries to create and initialize graphs of objects in a concise builder syntax like in Groovy. Consider you have a set of library methods
HtmlNode html(Function1<HtmlNode, Void> initializer)
HeadNode head(HtmlNode parent, Function1<HeadNode, Void> initializer)
...
that create DOM elements for HTML pages inside their respective parent elements. You can then create a DOM using the following Xbase code:
html( [ html |
head(html, [
// initialize head
] )
] )
Appending the closure parameters and prepending the parent parameters using extension syntax yields
html() [ html |
html.head() [
// initialize head
]
]
Using implicit parameter names and skipping empty parentheses you can simplify this to
html [
head [
// initialize head
]
]