DSL Implementations with DSLs
A Domain Specific Language starts with the domain model. For a language implemented with Xtext the model is typically defined either using Ecore directly or with Xcore DSL. For simple DSLs one can also let Xtext derive the Ecore model from the grammar.
The concrete syntax for the DSL is then defined with Xtext Grammar Language. It is then used to parse text into models and serialize models back into text.
Xtext and Xcore are the DSLs that define two main aspects of a DSL implementation. In DSL Developer Kit we introduced four further DSLs that cover further aspects of a DSL implementation. Each language comes with the required runtime libraries extending capabilities of Xtext runtime.
Check DSL is based on Xbase and allows defining model constraints using Xbase expressions. It generates an EMF model validator with all necessary registration hooks and preferences.
Below is an example of a simple check for
ProcedureDeclaration EClass which has an attribute
name of type EString. The check is configurable with
maxLength parameter which has a default value
10. If the name of the procedure declaration exceeds
maxLength the check issues a diagnostic message with error severity. Live execution time means that within IDE this check runs as you type.
Pretty printing of a DSL is important for model serializer and hence one of the core features of any DSL implementation. It is also used by IDE to perform auto format. In Format DSL one can define formatting rules for grammar rules in Xtext Grammar Language. It is also an Xbase-based language and Xbase expressions are used to compute conditions and dynamic values for conditional formatting baed on abstract syntax tree.
Below is an example of a formatting rule for
NamedArgument grammar rule. The rule is conditional on the number of parameters. A call with just one parameter will be kept on a single line. When there are two or more parameters, each will be on a new line. Also the values will be aligned in a column and the column position is computed based on the longest parameter name.
Expression and the end of the rule is a Boolean guard. The rule is active only if it evaluated to true in the given context.
Symbol Tables in Xtext are implemented as an Xtext Index. What gets stored in an index for a specific DSL is defined by a resource description strategy. Export DSL defines for what EClass-es object descriptions are computed, their structure as well as strategies for computing object fingerprints and URI fragments.
Below is an example for a simple scripting language. Import section lists EPackages used in the export file. Interface section defines fingerprint computation for EObjects. The following two export statements declare how EObjectDescriptions for EClasses
MethodDeclaration are constructed. Along these declarations one can also influence how fragment provider computes a segment for this object in URI fragments.
object-fingerprint entry we instruct resource description strategy to include object fingerprints into object descriptions. This enables fine-grained dependency analysis.
When the user makes a small change to a declaration in a source 1 - even if this change shouldn’t invalidate any other sources (if this declaration isn’t referenced anywhere) - the Xtext builder will revalidate (rebuild) all other sources which have a dependency on any declaration in the changed source 1. That is the standard behaviour of the Xtext builder.
More often than not many of these other sources actually don’t need rebuilding, as they don’t depend on the declaration which was originally changed in source 1. So the Xtext builder ends up spending a lot of time revalidating a still valid model.
DSL Developer Kit solves this by offering a fine-grained dependency analysis based on object fingerprints. The Xtext builder then only rebuild sources which actually depend on the changed declarations.
The following diagram illustrates the principal idea of fine-grained dependencies. Instead of having a single resource fingerprint accounting for all the exported objects of source 1 (as to the left), every exported object has its own object fingerprint. As a result the builder doesn’t need to rebuild source 2 as it only depends on declaration C of 1, while it was declaration B which was changed.
Fine-grained invalidation requires stable URI fragments. Export DSL allows defining
“semi-positional“ syntax with selectors for not unique names. For example
0/1/3(0=='foo').1 for objects
foo. If the name is unique fragment gets additional
! and omit last part: e.g.
0/1/3(0=='foo'!). Fine-grained dependency comes with a price: every visible declaration must have an exported object.
In the URI fragment example the first
0 is the index of the top-level object within the EMF resource. The following
1 is the feature ID of the single valued containment feature rather than a position index. The leading
/ indicates that what follows is the ID of a containment feature. In the same fashion the
3 is the ID of the multi-valued containment feature. What follows in brackets is the selector (similar to XPath selectors): The leading
0 in the selector is the feature ID of the selector feature
Declaration#name. After the equality operator follows the object’s value for the selector feature. The last digit (the selector index) is the index of the object within the filtered (by the selector) containment feature
ScriptPackage#declarations. These fragments are thus very similar to the short fragments used internally by Xtext; except for the new selector, of course.
Scope language helps to describe the logic for exactly how a proxy for a cross-reference resolves to the correct target. Scope computation is a prerequisite for linking: the linker simply asks for all the elements in scope, then it chooses the first element (in order of precedence as defined by the scope chain) with a matching name.
Following examples illustrate some of the constructs of scope language. Naming section defines default naming functions for EClasses. These naming functions are applied by linker if no other naming function is specified explicitly in a scoping rule.
ObjType#type is used to resolve
type feature of
find(Row, key = "code_obj_type.*") means a global name lookup of an EClass
Row with qualified names starting with
* means that the result of the scope is invariant on a resource level. Framework will make sure to provider proper caching on this level.
Next example illustrates chaining of scope rules and delegation to a named scope rule.
Unit tests with Xtend
We find Xtend very useful for writing unit tests for DSLs. Multi-line strings are nice for copy-pasting code snippets directly within JUnit test method bodies. In DSL Developer Kit we’ve implemented a number of abstract classes helping to write linking, validation, content assist and other tests.
The following is an example of a linking test. In this test we link from label translation to a label definition. Label definition is in a different source from label translation. Label definition is added as a required source for the test. We tag the EObject with label definition by calling
mark helper method from within the example template. Test implementation will simply take the EObject at the offset of the tag. Note that you don’t need to initialise the tag constant - active annotation of Xtend does it. Then in the label implementation right before the cross reference we want to test we place a reference to the tag. Test will then verify that a cross-reference to the right from the
ref call actually resolve against the expected object.