Top

Using Xbase Expressions

Xbase is an expression language that can be embedded in Xtext languages. Its syntax is close to Java, but it additionally offers type inferrence, closures, a rich switch statement and a lot more. For details on the Xbase langugae itself, please consult the Xbase documentation and the Xbase tutorial. Xbase ships with an interpreter and a compiler to 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.

Making Your Grammar Refer To Xbase

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:
    visibility=Visibility? '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 root XExpression.

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 :-)

Populating the Scopes

If you're not familiar with Xtext's concept of scopes yet, it would be a good idea to learn about scopes before you go on reading this section.

The XbaseScopeProvider (src) already builds a complex hierarchy of scopes that is necessary to link your expressions. There are a few points you may want to customize.

Within the body of an operation, we want to have several variables on the scope. The variable this should point to the JVM type of the entity, such that all its features are callable. In addition, the operation's parameters should be referrable by their name. The XbaseScopeProvider (src) has a template method createLocalVarScope for exactly this purpose. The following code shows the Xtend implementation of the scope provider for our domain model language:

class DomainmodelScopeProvider extends XbaseScopeProvider {

    @Inject extension IJvmModelAssociations associations
    
    override IScope createLocalVarScope(EObject context, 
            EReference reference, IScope parent,
            boolean includeCurrentBlock, int idx) {
        if (context instanceof Entity) {
            val type = (context as Entity).jvmType
            return new SimpleScope(parent, newImmutableList(
                EObjectDescription::create(XbaseScopeProvider::THIS, 
                type)))
        }
        if(context instanceof Operation){
                val descriptions = (context as Operation)
                    .params.map(e | e.createIEObjectDescription())
                return MapBasedScope::createScope(
                        super.createLocalVarScope(context, reference, 
                            parent, includeCurrentBlock, idx), 
                        descriptions);    
        }
        return super.createLocalVarScope(context, reference, parent, 
            includeCurrentBlock, idx)
    }
    
    def createIEObjectDescription(JvmFormalParameter jvmFormalParameter)
    {
        EObjectDescription::^create(
            QualifiedName::^create(jvmFormalParameter.name), 
            jvmFormalParameter, null);
    }

    def JvmType getJvmType(Entity entity) {
        entity.jvmElements.filter(typeof(JvmType)).head
    }
    
    override JvmDeclaredType getContextType(EObject call) {
        if (call == null)
            return null
        val containerClass = getContainerOfType(call, typeof(Entity));
        if (containerClass != null)
            return getJvmType(containerClass) as JvmDeclaredType
        else
            return super.getContextType(call)
    }
}

In addition to the variable binding and some helper functions, we defined the function getContextType to tell Xbase, which features are visible (callable) in the context of a the given element. In out case, these are all features of the inferred type, including the private ones.

Type Checking at the Boundaries

The scope provider from the last section allows Xbase to do static type analysis on the operation bodies, as all callable properties now have types. The missing part is to make sure that the declared return type conforms to the actual types of the method body's possible return values.

Type analysis in Xbase boils down to compare expected types with the actual types returned by an expression. For example, the condition of an XIfExpression (src) is expected to be a boolean, so any expression used as condition should conform to the boolean type. The component the defines these types and expectations is the ITypeProvider (src). It has several responsibilities

getType() returns the type of an expression
getExpectedType() returns the expected type of an expression in a given context
getTypeForIdentifiable() returns the expected type of an element that is referenced
getCommonReturnType() returns the common supertype of all types used in return expressions inside this expression
getThrownExceptions() returns the types of all declared exceptions thrown inside this expression

An additional flag rawType signals a raw type without resolved type parameters is enough. This is needed to avoid cycles in the type inference while linking. The XbaseTypeProvider (src) implements polymorphic dispatch methods for the first three of these. In the domain model example, we expect the return type of the body of an Operation to conform to the declared return type. We can do so by specializing the default XbaseTypeProvider (src) as

@Singleton
public class DomainmodelTypeProvider extends XbaseTypeProvider {

    protected JvmTypeReference _expectedType(Operation operation, 
            EReference reference, int index, boolean rawType) {
        if(reference == DomainmodelPackage.Literals.OPERATION__BODY) {
            return operation.getType();
        }                
        return null
    }
}

and of course binding this new implementation in the languge's module. By default, Xbase only checks type conformance inside expressions. To address the validation at the expression's boundary, we have to add a validation rule:

public class DomainmodelJavaValidator 
        extends AbstractDomainmodelJavaValidator {

    @Inject
    private XbaseTypeConformanceComputer typeConformanceComputer;
    
    @Inject
    private DomainmodelTypeProvider typeProvider;
    
    @Check
    public void checkTypeConformanceOfOperation(Operation op){
        JvmTypeReference expectedType = typeProvider
            .getExpectedType(op.getBody());
        JvmTypeReference commonReturnType = typeProvider
            .getCommonReturnType(op.getBody(), true);
        if(!typeConformanceComputer.isConformant(expectedType, 
            commonReturnType))
            error("Type does not conform to expected type!",
                DomainmodelPackage.Literals.OPERATION__BODY);
    }
}

Generating Java Code using the Xbase Compiler

The XbaseCompiler (src) compiles Xbase expressions to Java code. You will now learn how to integrate it in the code generator of your DSL.

The following snippet shows how the code generator can call the compiler:

class DomainmodelGenerator implements IGenerator {
    
    @Inject DomainmodelCompiler domainmodelCompiler
    
    ...
    
    def dispatch compile(Operation o, ImportManager importManager) '''
        public «o.type.shortName(importManager)» «o.name»(«
            o.parameterList(importManager)») {
            «domainmodelCompiler.compile(o, importManager)» 
        }
    '
''
    
    def parameterList(Operation o, ImportManager importManager) {
        o.params.map(p| p.parameterType.shortName(importManager) 
                        + ' ' + p.name).join(''', 
            '
''
        )
    }
}

Now let us customize the compiler. The XbaseCompiler (src) writes its code output into an IAppendable (src). This component keeps track of the indentation, of local variables, and of the generated code. Use an StringBuilderBasedAppendable (src) to generate the code of an XbaseExpression into a StringBuilder. Before calling the compiler, we add the operation's parameters to the local variables.

Remember that in the scope provider we bound the variable this to the inferred type of the current Entity. The XbaseCompiler (src) will by default assign the value of all expressions to a new Java variable. If we just call this, we don't need that, so we override isVariableDeclarationRequired to return false in this case. Similarly, whenever the compiler needs to put a variable name for the inferred Java type, it should use this. This can be achieved by overriding getVarName(). In a similar way, we have to override the IdentifiableSimpleNameProvider (src) to return this for the type. It is likely that we will reduce the last two tasks to one in the future.

Here's the complete code for the customized compiler:

public class DomainmodelCompiler extends XbaseCompiler {

    public String compile(Operation operation, 
                          ImportManager importManager) {
        StringBuilderBasedAppendable appendable = 
            new StringBuilderBasedAppendable(importManager);
        for(JvmFormalParameter param: operation.getParams()) {
            appendable.declareVariable(param, param.getName());
        }
        return compile(operation.getBody(), appendable, 
            operation.getType()).toString();
    }

    @Override
    protected boolean isVariableDeclarationRequired(XExpression expr, 
                                                    IAppendable b) {
        if (expr instanceof XAbstractFeatureCall 
            && ((XAbstractFeatureCall)expr).getFeature() 
                instanceof JvmGenericType) {
            return false;
        }
        return super.isVariableDeclarationRequired(expr,b);
    }

    @Override
    protected String getVarName(Object ex, IAppendable appendable) {
        if(ex instanceof JvmGenericType) {
            return "this";
        }
        return super.getVarName(ex, appendable);
    }
}

class DomainmodelIdentifiableSimpleNameProvider extends 
    IdentifiableSimpleNameProvider {
    
    def dispatch getSimpleName(JvmType element) {
        return "this";
    }
    
    def dispatch getSimpleName(JvmIdentifiableElement element) {
        return super.getSimpleName(element);
    }
}

Using the Xbase Interpreter

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<ElementTypeWithoutX> and takes two 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();
}