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.
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:
Reference scope offers lower ceremony, while Inherited scopes allow the scope to be built gradually following the hierarchy of nodes in the model.
We will describe the new hierarchical (inherited) mechanism of scope resolution first. This mechanism delegates scope resolution to the ancestors, who implement ScopeProvider.
MPS starts looking for the closest ancestor to the reference node that implements ScopeProvider and who can provide scope for the current kind.
If the ScopeProvider returns null, MPS continues searching for more distant ancestors.
- 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.
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:
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:
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 N/A 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:
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.
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.
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
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: