MPS 2021.2 Help

Scopes

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. By default, when no scoping is defined for a reference, all targets in the current model as well as in the imported models, are in scope and thus available for the reference.

MPS offers two ways to define scopes:

  • Inherited scopes

  • Reference scopes

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

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, MPS continues searching for more distant ancestors.

  3. Each ScopeProvider can 

    • build and return a Scope implementation (more on these later)

    • delegate to the parent scope 

    • add its own elements to the parent scope

    • hide elements from parent scope (more on how to work with scopes will be discussed later)

Call to obtain the parent scope must be made explicitly from within a ScopeProvider.

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

Sc1

Sc2

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.

Sc3

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:

Co1111

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

Sc5

The getScope() method takes two parameters:

  • kind - the concept of the possible targets for the reference

  • child - the child node of the current (this) ScopeProvider, from which the request came, so the actual reference is among descendants of the child node

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 (for parent scope to be available, import the jetbrains.mps.lang.Scopes language):

Sc8

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/Cmd + 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:

  • ListScope - represents the nodes passed into its constructor

  • DelegatingScope - delegates to a Scope instance passed into its constructor, typically to be extended by scopes that need to add functionality around an existing scope, e.g. LazyScope

  • CompositeScope - delegates to a group of (wrapped) Scope instances

  • FilteringScope - delegates to a single Scope instance, filtering its nodes with a predicate (the isExcluded method)

  • FilteringByNameScope - delegates to a single Scope instance, filtering its nodes by a name blacklist, which it gets as a constructor parameter

  • EmptyScope - scope with no nodes

  • SimpleRoleScope - a scope providing all child nodes of a node, which match a given role

  • ModelsScope - a scope containing all nodes of a given concept contained in the supplied set of models

  • ModelPlusImportedScope - like ModelsScope, but includes all models imported by the given model

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

Scopex1001

VariableReference

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

Scp1

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

Scp2

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.

Using reference scope

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

Rsc1

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.

Rsc2

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:

  • SimpleRoleScope - simply adds all nodes connected to the supplied node and being in the specified role.

  • ListScope - builds a scope implementation arround a list (sequence) of nodes provided in the constructor.

  • ModelPlusImportedScope- provides reference targets from imported models. Allows the user add targets to scope by ctrl + R / cmd + R (import containing model).

  • FilteringScope - allow you to exclude some elements from another scope. Subclasses of FilteringScope with override the isExcluded() method.

  • DelegatingScope - delegates to another scope. Meant to be overridden to customize the behavior of the original scope.

You may also look around yourself in the scope model:

Scp3
Last modified: 14 September 2021