We are going to look at two ways to define scopes for custom language elements - the inherited (hierarchical) and the referential approaches. We chose the Calculator tutorial language as a testbed for our experiments. You can find the calculator-tutorial project included in the set of sample projects that comes with the MPS distribution.

Two ways

All references need to know the set of allowed targets. This enables MPS to populate the completion menu whenever the user is about to supply a value for the reference. Existing references can be validated against that set and marked as invalid, if they refer to elements out of the scope.

MPS offers two ways to define scopes:

Reference scope offers lower ceremony, while Inherited scopes allow the scope to be built gradually following the hierarchy of nodes in the model. 

The oldest type of scopes in MPS is called Search scope and it has been deprecated in favor of the two types mentioned above, because the scoping API has changed significantly since its introduction. The Reference scope can be viewed as the closest replacement for Search scope compatible with the new API.

Inherited scopes

We will describe the new hierarchical (inherited) mechanism of scope resolution first. This mechanism delegates scope resolution to the ancestors, who implement ScopeProvider.

  1. MPS starts looking for the closest ancestor to the reference node that implements ScopeProvider and who can provide scope for the current kind.
  2. If the ScopeProvider returns null, we continue searching for more distant ancestors.
  3. Each ScopeProvider can 

Our InputFieldReference thus searches for InputField nodes and relies on its ancestors to build a list of those.

Once we have specified that the scope for InputFieldReference when searching for an InputField is inherited, we must indicate that Calculator is a ScopeProvider. This ensures that Calculator will have say in building the scope for all InputFieldReferences that are placed as its descendants.

The Calculator in our case should return a list of all its InputFields whenever queried for scope of InputField. So in the Behavior aspect of Calculator we override (Control + O) the getScope() method:

If Scope remains unresolved, we need to import the model (Control + R) that contains it (jetbrains.mps.scope):

The getScope() method takes two parameters:

We also need BaseLanguage since we need to encode some functionality. The jetbrains.mps.lang.smodel language needs to be imported in order to query nodes. These languages should have been imported for you automatically. If not, you can import them using the Control + L shortcut.

Now we can complete the scope definition code, which, in essence, returns all input fields from within the calculator:

A quick tip: Notice the use of SimpleRoleScope class. It is one of several helper classes that can help you build your own custom scopes. Check them out by Navigating to SimpleRoleScope (Control + N) and opening up the containing package structure (Alt + F1).

Scope helper implementations

MPS comes with several helper Scope implementations that cover many possible scenarios and you can use them to ease the task of defining a scope:

For example, the getScope() method could be rewritten using ListScope this way:


A slightly more advanced example can be found in BaseLanguage. VariableReference uses inherited scope for its variableDeclaration reference.

Concepts such as ForStatement, LocalVariableDeclaration, BaseMethodDeclaration, Classifier as well as some others add variable declarations to the scope and thus implement ScopeProvider.

For example, ForStatement uses the Scopes.forVariables helper function to build a scope that enriches the parent scope with all variables declared in the for loop, potentially hiding variables of the same name in the parent scope. The come from expression detects whether the reference that we're currently resolving the scope for lies in the given part of the sub-tree.

  • The parent scope construct will create an instance of LazyParentScope() and effectively delegate to an ancestor in the model, which implements ScopeProvider, to supply the scope.
  • The come from construct will delegate to ScopeUtils.comeFrom() in order to check, whether the scope is being calculated for a direct child of the current node in the given role.
  • The composite with construct (used as composite <expr> with parent scope) will create a combined scope of the supplied scope expression and the parent scope.

Using reference scope

Scopes can alternatively be implemented in a faster but less scalable way - using the reference scope:

Instead of delegating to the ancestors of type ScopeProvider to do the resolution, you can insert the scope resolution code right into the constraint definition.

You may need to import (Control/Cmd + R) the jetbrains.mps.scope model in order to be able to use SimpleRoleScope.

Instead of the code that originally was inside the Calculator's getScope() method, it is now InputFieldReference itself that defines the scope. The function for reference scope is supposed to return a Scope instance, just like the ScopeProvider.getScope() method. Scope is essentially a list of potential reference targets together with logic to resolve these targets with textual values.

To remind you, there are several predefined Scope implementations and related helper factory methods ready for you to use:

You may also look around yourself in the scope model: