Scopes in XText 2.9.x with XCore and Generics

This post is about some juggling with technologies in the EMF ecosystem, namely XText and XCore. Consider a small (and not completely realistic) DSL for defining entity types (like the introductory grammar example of the XText documentation). Often there are two different kinds of types in such a language:

  1. primitive/native types, provided by the system, e.g., String, Integer, Boolean, …
  2. composite/complex types, which have been modeled in the language and can have attributes of primitive type or refer to other complex types

So we might model these variants of properties as Attribute for primitive types and Association for complex types…

Since we would like to have a nice and expressive meta model, we use generics to define it (in XCore):

interface EntityModel {
  contains Type[] types
}
interface Nameable {
  String name
}
interface Type extends Nameable {}
class PrimitiveType extends Type {
  BaseType baseType
}
class ComplexType extends Type {
  contains Property<?>[] properties
}
interface Property<T extends Type> extends Nameable {
  refers T ^type
}
class Attribute extends Property<PrimitiveType> {}
class Association extends Property<ComplexType> {}
enum BaseType { String Integer Boolean }

The corresponding XText grammar could look like this:

EntityModel: (types += Type)*;
Type: PrimitiveType | ComplexType;
PrimitiveType: 'data' name=ID: baseType = BaseType;
BaseType: String | Integer | Boolean;
ComplexType:
  'class' name=ID ('{'
    (properties += Property)+
  '}')?;
Property: Attribute | Association;
Attribute: 'attribute' name=ID ':' [PrimitiveType];
Association: 'association' name=ID ':' [ComplexType];

So now if we generate the eclipse plugin and start it in a new eclipse instance, we may write something like this:

data Text: String
type Animal {
  attribute name: Text
  association favorite: Food
}
type Food {
  attribute name: Text
}

And the AST (abstract syntax tree or semantic model as the XText documentation call it) looks like this:

Rendered by QuickLaTeX.com

A nice feature of XText is content assist. That means, if you hit ctrl+Space the editor offers you possibilities what you might want to enter next. One assistance type is showing referenceable elements which are in scope. That means, it offers you available primitive types for attributes and complex types for associations. But this feature is not aware of the generics we used above.

Consider, we enter in line 4 (see the highlighting above) of our data model example association favorite:. The editor creates a meta model object as soon as the first assigned action in the corresponding grammar matched. In our case it is an object of class Association with name=favorite. If we now hit ctrl+Space, the scoping mechanism of XText is invoked and it provides the scope provider with a context element (the current meta model object… in our case the favorite association ) as well as a reference for which the scope should be evaluated.

Although class Association tackles/defines the type parameter T extends Type of Property to ComplexType the attribute type is not declared in Association and its type is implicitly narrowed in Association together with type parameter T. Hence, the attributes type is mistakenly evaluated to Type and not to ComplexType. The default semantic of the scope mechanism is to retrieve all objects matching the type of the reference. Therefore, the editor will provide Food and Text (the same holds for Attributes). If we choose Text over Food, the editor tries to add a value of PrimitiveType to the attribute type, but the meta model allows ComplexType, only. This fails and results in a validation error message.

An editor should not present you a code completion that is just wrong, since this is just wrong! Fortunately, we can add our own implementation (in XTend) of IScopeProvider and “fix” the default behavior:

class DataScopeProvider extends AbstractDataScopeProvider {
  override getScope(EObject context, EReference reference) {
    switch context {
      Association case reference == EntityModelPackage.Literals.PROPERTY__TYPE: {
        val rootElement = EcoreUtil2.getRootContainer(context)
        val candidates = EcoreUtil2.getAllContentsOfType(rootElement, ComplexType)
        Scopes.scopeFor(candidates)
      }
      Attribute case reference == EntityModelPackage.Literals.PROPERTY__TYPE: {
        val rootElement = EcoreUtil2.getRootContainer(context)
        val candidates = EcoreUtil2.getAllContentsOfType(rootElement, PrimitiveType)
        Scopes.scopeFor(candidates)
      }
      default: super.getScope(context, reference)
    }
  }
}

In line 4 and 9 we add the partitioning type check for Association and Attribute regarding our scope provider. This way the default behavior is overwritten to decide the scope via the generic association type only. In lines 6 and 11 the elements to propose as content assistance are added (either all ComplexTypes or PrimitiveTypes defined in the same file).

Exciting 🤓

Leave a Reply

Your email address will not be published. Required fields are marked *