In a recent post I suggested that you prefer the typesystem aspect over constraints when implementing the “business rules” of your language. The reason was that the typesystem aspect is less restrictive and therefore lets the language users express themselves better and provides opportunities for guidance.

Today I want to cover one particular case where the best practice is the opposite: to prevent the user from seeing invalid choices. The case is a so called “dot expression”, an expression such as variable.method() in Java (BaseLanguage).

The left-hand side of DotExpression in BaseLanguage is called operand and the part after the dot is operation. Dot expression is rather peculiar in that the set of available operations depends on the operand: which methods I can call on a variable depends on what variable is.

Java even allows complex expressions to the left of the dot: (isFoo ? bar : baz).method() is legal Java. In fact, the available operations depend on the type of the operand.

Providing the user with all available methods of all classes in this case would make for a very poor editing experience, therefore it’s best to hide inapplicable choices from the menu. It is the responsibility of each concept that can be used as an operation to make sure it is only visible when the operand of the parent DotExpression is of the right type.

This can be implemented in several ways: a can be child constraint, a reference scope, or by customizing the default substitute menu.

Here is how InstanceMethodCallOperation in BaseLanguage (the foo.bar() method call) implements its reference scope (the code to compute all available bars):

link {instanceMethodDeclaration}
  referent set handler <none>
  scope (referenceNode, contextNode, containmentLink, position, linkTarget, operationContext)->Scope {
    final node<> enclosingNode = (referenceNode.isNull ? contextNode : referenceNode.parent);
    if (!enclosingNode.isInstanceOf(DotExpression)) { return new EmptyScope(); }
    node<Expression> instance = enclosingNode:DotExpression.operand;
    node<> instanceType = TypeContextManager.getInstance().runResolveAction({ => instance.type; });
    node<ClassifierType> classifierType = coerceStrong(instanceType :<< concept = ClassifierType as foo);
    if (classifierType.classifier.isNull) { return new EmptyScope(); }
    new MethodsScope(classifierType, Members.visibleInstanceMethods(classifierType, contextNode));
  }
  <no presentation (deprecated)>

The code checks that the node is part of a DotExpression whose operand has a ClassifierType (class or interface), and then it returns all visible instance methods of the classifier type. Otherwise it returns an empty scope, thus only showing applicable methods.

Sequence operations in the jetbrains.mps.baseLanguage.collections language take a different approach, checking the operand type in the substitute (completion) menu:

default substitute menu for concept SequenceOperation 
  subconcepts menu (concept, parentNode, currentTargetNode, link, editorContext, model)->boolean { 
    coerceStrong(parentNode as DotExpression.operand.type :<< concept = SequenceType as sequenceType).isNotNull; 
  }

And finally, ArrayLengthOperation (the .length of an array in Java) checks the operand type in the “can be child” constraint:

can be child (node, parentNode, childConcept, link, operationContext)->boolean { 
  boolean result = false; 
  if (parentNode.isInstanceOf(DotExpression)) { 
    result = coerceStrong(parentNode:DotExpression.operand.type :<< concept = ArrayType as arrayType) != null; 
  } 
  result; 
}

When considering whether to use the substitute menu or the constraint, I prefer the latter. Hiding an item from the menu merely prevents the user from typing it in manually but no error would be shown if they manage to insert the hidden item somehow (either by copypasting or by using the MPS Console). You would need to implement an additional check to show an error if the operation is not applicable to the operand. When you define the constraint you both hide the inapplicable item from the menu and get the error message if the user manages to insert the item manually.