On a previous post, I started to blog about how Xtext 2.1 made it extremely easy to have a powerful integration with Java thanks to Xbase, a new expression language library which allows to integrate (Java-like) expressions in your DSL. I had also blogged about using JVM types from your DSL.
In this post, IÔÇÖd like to continue to inspect how to use only a small part of Xbase and still have the control on the generation part: in particular (for other projects) I would like to retain the control on the generation for my model, while relying on the Xbase generation for the Xbase parts. Thus, in this post IÔÇÖll describe:
- how to integrate Xbase variables (XVariableDeclaration) and expressions (XExpression) in your DSL
- how to extend Xbase scoping for making the variables visible in the expressions
- write a generator for your DSL and reuse the XbaseCompiler for the code of XVariableDeclaration and XExpressions
Following the spirit of the previous post, I wonÔÇÖt use the Domainmodel example, but something really simple, like the Greeting example, i.e., the very basic DSL you will get when creating an Xtext project inside Eclipse. In particular, you might want to go through the previous posts before reading this post (since we reuse some of the concepts seen there):
- the one about JVM types (concerning how to use the ImportManager for generating correct code for accessing Java types and the corresponding import statements)
- and the one about Xbase expressions (concerning how to use the XbaseCompiler for generating code corresponding to Xbase expressions).
So, first of all, create a new Xtext project; IÔÇÖll call this project org.xtext.example.helloxvars (and the files will have extension helloxvars). Now, we can start generate Xtext artifacts. With the defaults, youÔÇÖll also get a HelloXvarsGenerator.xtend, which we now to generate Java code. The basic idea of the generator is: for each Greeting element we will obtain a package and a class named according to the Greeting name feature (package all lower case, and class name with the first letter capital). We’ll deal with the generator later in this post.
Now, the idea is to enrich the greeting DSL with some Xbase variables (before the greeting statements) and some Xbase expressions (used within the greeting statements) which can access the declared variables; thus, we modify the grammar of the language (HelloXvars.xtext) as follows (you should be familiar with ÔÇÿimportÔÇÖ functionalities of Xtext):
grammar org.xtext.example.helloxvars.HelloXvars with org.eclipse.xtext.xbase.Xbase generate helloXvars "http://www.xtext.org/example/helloxvars/HelloXvars" Model: imports += Import* varDeclarations += XVariableDeclaration* greetings+=Greeting*; Import: 'import' importedNamespace = QualifiedNameWithWildcard ; QualifiedNameWithWildcard: QualifiedName '.*'? ; Greeting: 'Hello' name=ID 'from' expression = XExpression '!' ;
The idea is to end up (at the end of this post) with a DSL which allows us to write programs as follows:
Note that Xbase (its validator) already checks that the types are correct, i.e., that the initialization expression has a type which is a subtype of the declared type for the variable. But, more important, you will reuse the type inference mechanism of Xbase: you don’t need to declare the type for the variable when this can be inferred by Xbase itself!
Regenerate the Xtext artefacts, and start a new runtime eclipse instance. Create a new plugin project (say ÔÇÿhelloxvarsÔÇÖ), and in the source folder we create a .helloxvars file (say ÔÇÿMy.helloxvarsÔÇÖ) and accept to add the Xtext nature. To fully enjoy Xbase syntax, please make sure that in the project you created in the runtime workspace you also have org.eclipse.xtext.xbase.lib as a dependency in the MANIFEST.
Now, if you try to write code like in the above image you’ll get errors due to the Xbase expressions that cannot access Xbase variables; in fact, Xbase variables are automatically visible to Xbase expressions only when they are both part of an XBlockExpression, which we don’t use in our case. Thus, we must extend the Xbase scoping mechanism in order to make all the variables declared before the greetings to all the expressions of the greetings.
As soon as we start to use Xbase in our grammar, the generated runtime module will bind the IScopeProvider to XbaseScopeProvider; so, first of all, we must override this binding in our runtime module, in order to bind the scope provider to the one of our DSL:
public class HelloXvarsRuntimeModule extends org.xtext.example.helloxvars.AbstractHelloXvarsRuntimeModule { @Override public Class<? extends IScopeProvider> bindIScopeProvider() { return HelloXvarsScopeProvider.class; } }
And now, we make our scope provider extend the XbaseScopeProvider; by inspecting the code of XbaseScopeProvider we find a possible good candidate method for customizing the scope of variables:
protected IScope createLocalVarScope(IScope parentScope, LocalVariableScopeContext scopeContext) {
Take a look at the code of this method in XbaseScopeProvider, and you should get the general idea (the base implementation walks through the containment hierarchy of the model to build the scope for local variables); in particular, LocalVariableScopeContext contains the context for the scope. In our case, we need to provide a custom scope when the context is the greeting Model (in all other cases we rely on the base implementation):
package org.xtext.example.helloxvars.scoping; import org.eclipse.emf.ecore.EObject; import org.eclipse.xtext.scoping.IScope; import org.eclipse.xtext.scoping.Scopes; import org.eclipse.xtext.xbase.scoping.LocalVariableScopeContext; import org.eclipse.xtext.xbase.scoping.XbaseScopeProvider; import org.xtext.example.helloxvars.helloXvars.Model; /** * This class contains custom scoping description. * * see : http://www.eclipse.org/Xtext/documentation/latest/xtext.html#scoping on * how and when to use it * */ public class HelloXvarsScopeProvider extends XbaseScopeProvider { @Override protected IScope createLocalVarScope(IScope parentScope, LocalVariableScopeContext scopeContext) { if (scopeContext != null && scopeContext.getContext() != null) { EObject context = scopeContext.getContext(); if (context instanceof Model) { Model model = (Model) context; return Scopes.scopeFor(model.getVarDeclarations()); } } return super.createLocalVarScope(parentScope, scopeContext); } }
So, when the context is the root of our model (Model) we return as the scope all the variables declared in our greeting model.
Now, restart your eclipse runtime instance and you’ll see that errors go away: the expressions in the greetings can access the variables declared above!
Note that the scoping does not concern only the expressions in the greetings, but also the initialization expressions of local variable declarations themselves:
val v1 = 'foo' val v = new String(v1) val v2 = v + v1
Now, if you do some experiments you’ll note that we provide scopes that contain too much! Try the following:
val v = new String(v2) val v2 = v
This code should raise errors, since the initialization expression for v should not be able to access variables declared afterwards (e.g., v2); while, with our scope implementation, this code would be silently accepted.
So, we need to tweak our scope implementation; however, let’s stop following the workflow of trying a modification, restart the runtime eclipse instance, and see whether we made it… let’s be agile 🙂 and write some unit tests! Create an Xtend2 class in the tests plugin project org.xtext.example.helloxvars.tests and add in the MANIFEST org.eclipse.xtext.xtend2.lib as a dependency.
package org.xtext.example.helloxvars.tests import com.google.inject.Inject import org.eclipse.xtext.diagnostics.Diagnostic import org.eclipse.xtext.junit4.InjectWith import org.eclipse.xtext.junit4.XtextRunner import org.eclipse.xtext.junit4.util.ParseHelper import org.eclipse.xtext.junit4.validation.ValidationTestHelper import org.eclipse.xtext.xbase.XbasePackage import org.junit.Test import org.junit.runner.RunWith import org.xtext.example.helloxvars.HelloXvarsInjectorProvider import org.xtext.example.helloxvars.helloXvars.Model @InjectWith(typeof(HelloXvarsInjectorProvider)) @RunWith(typeof(XtextRunner)) class HelloXvarsParserTest { @Inject ParseHelper<Model> parser @Inject extension ValidationTestHelper @Test def void testParsingAndLinking() { parser.parse("Hello foo from new String()!").assertNoErrors } @Test def void testParsingAndLinkingWithVars() { parser.parse(" val s1 = 'foo' val s2 = 'bar' val s3 = s1 + s2 Hello foo from new String(s3)! ").assertNoErrors } @Test def void testParsingAndLinkingWithClosures() { parser.parse(" val s1 = 'foo' val s2 = 'bar' val s3 = s1 + [ s | s.toFirstLower + s1 ].apply(s2 + s1) Hello foo from new String(s3)! ").assertNoErrors } @Test def void testParsingAndLinkingWithMissingVar() { parser.parse(" Hello foo from new String(s)! ").assertError(XbasePackage::eINSTANCE.XFeatureCall, Diagnostic::LINKING_DIAGNOSTIC, "Couldn't resolve reference to JvmIdentifiableElement 's'.") } @Test def void testParsingAndLinkingWithWrongVarOrder() { parser.parse(" val s1 = s2 val s2 = s1 Hello foo from new String(s1)! ").assertError(XbasePackage::eINSTANCE.XFeatureCall, Diagnostic::LINKING_DIAGNOSTIC, "Couldn't resolve reference to JvmIdentifiableElement 's2'.") } }
You see, the first test checks a greeting program without variables, the second checks a valid program with variable access, the third one checks a valid program with variable access and a closure, the forth one checks that accessing a variable which is not declared generates an error, and the last one checks that variable initialization expressions cannot access variable declared afterwards. Of course, the last test fails! And we need to get the green bar back now 🙂
So now we modify the scoping:
package org.xtext.example.helloxvars.scoping; import java.util.List; import org.eclipse.emf.ecore.EObject; import org.eclipse.xtext.EcoreUtil2; import org.eclipse.xtext.scoping.IScope; import org.eclipse.xtext.scoping.Scopes; import org.eclipse.xtext.xbase.XExpression; import org.eclipse.xtext.xbase.XVariableDeclaration; import org.eclipse.xtext.xbase.scoping.LocalVariableScopeContext; import org.eclipse.xtext.xbase.scoping.XbaseScopeProvider; import org.xtext.example.helloxvars.helloXvars.Model; /** * This class contains custom scoping description. * * see : http://www.eclipse.org/Xtext/documentation/latest/xtext.html#scoping on * how and when to use it * */ public class HelloXvarsScopeProvider extends XbaseScopeProvider { @Override protected IScope createLocalVarScope(IScope parentScope, LocalVariableScopeContext scopeContext) { if (scopeContext != null && scopeContext.getContext() != null) { EObject context = scopeContext.getContext(); Model containingModel = EcoreUtil2.getContainerOfType(context, Model.class); XVariableDeclaration containingVarDecl = EcoreUtil2 .getContainerOfType(context, XVariableDeclaration.class); List<XExpression> varDeclarations = containingModel.getVarDeclarations(); int index = varDeclarations.size(); if (containingVarDecl != null) { index = varDeclarations.indexOf(containingVarDecl); } return Scopes.scopeFor(varDeclarations.subList(0, index)); } return super.createLocalVarScope(parentScope, scopeContext); } }
This time we do something different: the scope will still be all the variables of the containing greeting Model; but if we’re inside a XVariableDeclaration, the scope will be the list of all the variable declarations up to the current variable, and we get the green line back… oh oh… something it’s still wrong: the previous failing test now succeeds, but the test for closure now fails! It says it cannot resolve the reference to ‘s’, the local variable of the closure itself:
@Test def void testParsingAndLinkingWithClosures() { parser.parse(" val s1 = 'foo' val s2 = 'bar' val s3 = s1 + [ s | s.toFirstLower + s1 ].apply(s2 + s1) Hello foo from new String(s3)! ").assertNoErrors }
If you take a look at our implementation of scoping, you’ll soon realize that we customized it too much: we basically override completely the method of XbaseScopeProvider, which will then be unable to build the scope for local variables declared in the expressions, and closures declare local variable themselves!
Indeed, the right solution is something in between the two implementations of scoping we saw here:
- we must check whether the container of the context is a greeting Model
- if it is, then the scope is the list of variables up to the current one, if the context is a variable declaration (we can simply search for the context in the list of variable declarations, since List.indexOf accept any Object
- otherwise, we delegate to XbaseScopeProvider
package org.xtext.example.helloxvars.scoping; import java.util.List; import org.eclipse.emf.ecore.EObject; import org.eclipse.xtext.scoping.IScope; import org.eclipse.xtext.scoping.Scopes; import org.eclipse.xtext.xbase.XExpression; import org.eclipse.xtext.xbase.scoping.LocalVariableScopeContext; import org.eclipse.xtext.xbase.scoping.XbaseScopeProvider; import org.xtext.example.helloxvars.helloXvars.Model; /** * This class contains custom scoping description. * * see : http://www.eclipse.org/Xtext/documentation/latest/xtext.html#scoping on * how and when to use it * */ public class HelloXvarsScopeProvider extends XbaseScopeProvider { @Override protected IScope createLocalVarScope(IScope parentScope, LocalVariableScopeContext scopeContext) { if (scopeContext != null && scopeContext.getContext() != null) { EObject context = scopeContext.getContext(); if (context.eContainer() instanceof Model) { Model model = (Model) context.eContainer(); List<XExpression> varDeclarations = model.getVarDeclarations(); int index = varDeclarations.indexOf(context); if (index < 0) index = varDeclarations.size(); return Scopes.scopeFor(varDeclarations.subList(0, index)); } } return super.createLocalVarScope(parentScope, scopeContext); } }
We finally get the green line for our tests 🙂
Now let’s move on to generation! Similarly to the previous post about Xbase expressions, we will generate a main method for each Greeting, and we will generate the Java code for all the variable declarations and initializations by reusing the XbaseCompiler. Most of the code for the generation has already been explained in the previous post.
package org.xtext.example.helloxvars.generator import org.eclipse.emf.ecore.resource.Resource import org.eclipse.xtext.generator.IGenerator import org.eclipse.xtext.generator.IFileSystemAccess import com.google.inject.Inject import org.eclipse.xtext.xbase.compiler.XbaseCompiler import static extension org.eclipse.xtext.xbase.lib.IteratorExtensions.* import org.xtext.example.helloxvars.helloXvars.Greeting import org.eclipse.xtext.common.types.TypesFactory import org.eclipse.xtext.xbase.compiler.ImportManager import org.eclipse.xtext.xbase.XExpression import org.eclipse.xtext.xbase.compiler.StringBuilderBasedAppendable import org.xtext.example.helloxvars.helloXvars.Model import org.eclipse.xtext.xbase.XFeatureCall class HelloXvarsGenerator implements IGenerator { @Inject protected XbaseCompiler xbaseCompiler override void doGenerate(Resource resource, IFileSystemAccess fsa) { for(greeting: resource.allContents.toIterable.filter(typeof(Greeting))) { fsa.generateFile( greeting.packageName + "/" + // package greeting.className + ".java", // class name greeting.compile) } } def getJvmType(Greeting greeting) { val declaredType = TypesFactory::eINSTANCE.createJvmGenericType declaredType.setSimpleName(greeting.className) declaredType.setPackageName(greeting.packageName) declaredType } def compile(Greeting greeting) ''' «val importManager = new ImportManager(true, getJvmType(greeting))» «val mainMethod = compile(greeting, importManager)» package «greeting.packageName»; «IF !importManager.imports.empty» «FOR i : importManager.imports» import «i»; «ENDFOR» «ENDIF» «mainMethod» ''' def compile(Greeting greeting, ImportManager importManager) { var result = new StringBuilderBasedAppendable(importManager) for (varDecl : (greeting.eContainer as Model).varDeclarations) { result.append("\n// variable declaration") result = compile(varDecl, result) } result.append("\n// greeting expression") result = compile(greeting.expression, result) var expressionVar = result.getName(greeting.expression) if (greeting.expression instanceof XFeatureCall) expressionVar = result.getName((greeting.expression as XFeatureCall).feature) val compiled = ''' public class «greeting.className» { public static void main(String args[]) { «result» Object expression = «expressionVar»; System.out.println("Hello «greeting.name» from " + expression.toString()); } } ''' compiled } def compile(XExpression xExpression, StringBuilderBasedAppendable result) { xbaseCompiler.toJavaStatement(xExpression, result, true) result } def className(Greeting greeting) { greeting.name.toFirstUpper } def packageName(Greeting greeting) { greeting.name.toLowerCase } }
Let us concentrate on the interesting part
def compile(Greeting greeting, ImportManager importManager) { var result = new StringBuilderBasedAppendable(importManager) for (varDecl : (greeting.eContainer as Model).varDeclarations) { result.append("\n// variable declaration") result = compile(varDecl, result) } result.append("\n// greeting expression") result = compile(greeting.expression, result) var expressionVar = result.getName(greeting.expression) if (greeting.expression instanceof XFeatureCall) expressionVar = result.getName((greeting.expression as XFeatureCall).feature) val compiled = ''' public class «greeting.className» { public static void main(String args[]) { «result» Object expression = «expressionVar»; System.out.println("Hello «greeting.name» from " + expression.toString()); } } ''' compiled }
We basically use the XbaseCompiler for generating the Java code corresponding to variable declarations and initializations; then we need to access the generated Java variable name corresponding to the XExpression of the current Greeting element. We use the getName method, as explained in the previous post; however, this time, we need to distinguish the case when the XExpression is a XFeatureCall: this is the case when the XExpression is just the name of a declared variable, as in this code
var myList = new LinkedList<Integer>(Collections::singleton(10)) Hello barList from myList !
In this case, we need to get the variable name which corresponds to the feature of the XFeatureCall, and not to the XFeatureCall itself (since no synthetic expression is generated for the XFeatureCall itself). Let’s try the generator
Remember, that we did not want to generate something meaningful: we wanted to experiment with code generation 🙂
You can find the sources for the project helloxvars at
https://github.com/LorenzoBettini/Xtext2-experiments
Hope you find this post useful, and stay tuned for new posts about Xtext 🙂
Lorenzo I wanted to thank you, these are the best tutorials on Xtext I have found so far (much better than the official ones on the Xtext website). They really helped me get started with Xtext. Keep up the great work :)!
Thank you Marcus!
I’m glad you find these tutorials useful 🙂
How about using XVariableDeclaration and XBaseCompiler to write
Integer v1 = 1 // There will be a warning after the code generation, I know.
Integer v2 = null
String blah = “12313”
in the grammar and get java code generated from it.
@nonuma, I’m not sure I understand what you mean, can you elaborate please?
Thanks for the info on xtext and especially on JvmInferrer…
What I mean is…
I have my own grammar. I have expressions, statements and variables etc. I want to use the
Inferrer/XbaseCompiler to generate the java code. So… what I am trying to do is to get the element
created from the grammar in the inferrer code and create the corresponding xbase artifact
— int a; would be for example the featurecall of Xbase — and fill the properties with my
info and then call necessary method of the xbase compiler passing the xbase artifact as parameter.
Is this a feasible approach? Bad/Whacky idea?
The inferrer is very badly documented. I am spending a lot of time just for the simplest things.
I am using the inferrer in the hope that the debugging will be (according to the docs anyway)
taken care of automatically. If you could tell me if there are things that I should be aware of
documents that I need to read and examples I should checkout, I will be very thankful.
I am using java 1.5, linux/ubuntu machine Xtext 2.3.0.
have you looked at my last article on the inferrer?
Yes I have the Xtext experiments from github. The helloinferrer project
is throwing NPE on
typeof(HelloResult).getTypeForName(o, o.parameterType)
in the inferrer’s returnType method. Debugging is not wroking too.
I can set a breakpoint in the helloinferrer source but the
debugger goes to the generated java source or crashes.
Well the class HelloResult had to be added in the
java project created using the helloinferrer.ui plugin.
After that I have no NPEs. Is there a possibility that the
ui/JavaProject becomes the classes needed by the
generated source code automatically?
No NPE but the debugging is not working. Am I missing something? 😀
I think it’s better to move this discussion on the article concerning helloinferrer 🙂
by the way, after Xtext 2.3 was officially released I haven’t updated that example…
however, probably, the problem you’re experiencing might be due to the fact that you created the project yourself and not through the wizard? I mean the project where you’re trying the DSL and the debugging…
Is there a IRC channel or something where I can bug the developers with my questions?
Do you mean Xtext’s developers? in that case, I don’t know of any IRC channel… but there’s the Xtext forum 🙂
Hi Lorenzo,
Thank you very much for this post. It is very useful. I am using the Xtext documentation DSL example following this post. I want to declare a property of type Entity but getting error saying Couldn’t resolve reference to JvmType ‘Entity Name’. It cannot recognise Entity as JvmTypeReference. How can I make entity as JvmTypeReference using JvmGenericType. Thanks for your help.
Best Regards,
Ambreen
Ambreen, I’m not sure I understand your question: are you referring to this example or to another example?
Hi Lorenzo,
Iam designing a DSL with support of XBlockExpression. I don’t need the code generation feature, but only the interpreter. while interpreting the xblock i need to pass an object and need to work on the object fields. Object is the data on which xblock xpression should work. Feature is to implement validations on the data.
I am not finding any way to access the object and access its members inside.
Grammer is like:
…..
Entity:
annotations+=XAnnotation*
‘entity’ name=ID
(”)?
(‘extends’ superType=JvmParameterizedTypeReference)? ‘{‘
operations += Operation*
‘}’;
Operation:
‘op’ (type=JvmTypeReference)? name=ID
‘(‘ (params+=FullJvmFormalParameter (‘,’
params+=FullJvmFormalParameter)*)? ‘)’
body=XBlockExpression;
…..
Sample input:
import java.util.ArrayList
import java.util.Date
entity A {
attr int i=0;
op meth() {
var x=self.a.b.d1 as Date
var y=self.a.b.d2 as Date
return x.after(y)
}
}
Interpreter:
var self= new HashMap();
self.put(“a”, new HashMap());
self.get(“a”).put(“b”,new HashMap());
self.get(“a”).get(“b”).put(“d1”, new Date());
self.get(“a”).get(“b”).put(“d2”, new Date());
context.newValue(QualifiedName.create(“self”), self)
var result=evaluate(expression, context, CancelIndicator.NullImpl);
I have created a name provider also:
override getSimpleName(JvmIdentifiableElement element) {
if (element.simpleName.equals(“self”))
‘self’
else
super.getSimpleName(element)
}
Can you please guide in this.
Basically i need to access an object inside the xblock interpretation mechanism