Since MPS frees you from defining a grammar for your intented languages, you obviously need different ways to specify the structure of your languages. This is where the Structure Language comes in handy. It gives you all the means to define the language structure. As we discussed earlier, when coding in MPS you're effectively building the AST directly, so the structure of your language needs to specify the elements, the bricks, you use to build the AST.
The bricks are called Concepts and the Structure Language exposes concepts and concept interfaces as well as their members: properties, references, and children.
Concepts and Concept Interfaces
Now let's look at those in more detail. A Concept defines the structure of a concept instance, a node of the future AST representing code written using your language. The Concept says which properties the nodes might contain, which nodes may be referred to, and what children nodes are allowed (for more information about nodes see the Basic notions section).
Apart from Concepts, there are also Concept Interfaces. Concept interfaces represent independent traits, which can be inherited and implemented by many different concepts. You typically use them to bring orthogonal concepts together in a single concept. For example, if your Concept instance has a name by which it can be identified, you can implement the INamedConcept interface in your Concept and you get the name property plus associated behavior and constraints added to your Concept.
Just like in OO programming, a Concept can extend another Concept, and implement many Concept Interfaces. A Concept Interface can extend multiple other Concept Interfaces. This system is similar to Java classes, where a class can have only one super-class but many implemented interfaces, and where interfaces may extend many other interfaces.
If a concept extends another concept or implements a concept interface, it transitively inherits all members (i.e if A has member m, A is extended by B and B is extended by C, then C also has the member m)
Concept interfaces with special meaning
There are several concept interfaces in MPS that have a special meaning or behavior when implemented by your concepts. Here's a list of the most useful ones:
Used if instances of your concept can be deprecated. It's isDeprecated behavior method indicates whether or not the node is deprecated. The editor sets a strikeout style for reference cells if isDeprecated of the target returns true.
Used if instances of your concept have an identifying name. By default, this name represents nodes as possible targets in the code completion list for references. Additionally, the name is shown in the Project View.
Used if instances of your concept have an identifying name. The interface extends INamedConcept and adds constraints that guarantee a valid Java identifier to be used as the name. Belongs to BaseLanguage and should only be utilized in BaseLanguage extensions.
Used for named concepts that are part of some aspect of a language definition. The interface ensures valid Java identifiers to be used.
Is used to mark all concepts representing types
Deleting a node whose immediate parent is an instance of IWrapper deletes the parent node as well.
Property is a value stored inside a concept instance. Each property must have a type, which for properties is limited to: primitives, such as boolean, string and integer; enumerations, which can have a value from a predefined set; and constrained data types (strings constrained by a regular expression). You may define a getter and setter for a property in the Constraints of a concept.
Holding scalar values would not get as far. To increase expressiveness of our languages nodes are allowed to store references to other nodes. Each reference has a name, a type, and a cardinality. The type restricts the allowed type of a reference target. Cardinality defines how many references of this kind a node can have. References can only have two types of cardinalities: 1:0..1 and 1:1.
For references there is no direct way to traverse them in the reverse direction. The
jetbrains.mps.lang.findUsages language and the
execute finder should be used for that. (These do not work reliably during generation).
A node containing a single reference of 1:1 cardinality and with no alias defined is called a smart reference. These are somewhat special references. Provided the language author has not specified an alias for them, they do their best to hide from the language user and be as transparent as possible. MPS treats the node as if it was a the actual reference itself, which simplifies code editing and code-completion. For example, default completion items are created whenever the completion menu is required: for each possible reference target, a menu item is created with matching text equal to the presentation of a target node.
In order to make a reference smart when it does not meet the above mentioned criteria for being treated as smart automatically, the concept declaration has to be annotated with the @smart reference attribute: A typical use-case would be a concept that customizes the presentation of the reference or holds additional references.
To compose nodes into trees, we need to allow children to be hooked up to them. Each child declaration holds a target concept, its role and cardinality. Target concept specifies the type of children. Role specifies the name for this group of children. Finally, cardinality specifies how many children from this group can be contained in a single node. There are 4 allowed types of cardinality: 1:1, 1:0..1, 1:0..n, and 1:1..n.
Programatically, node.children retrieves all nodes that are children to the current node. The reverse of node.children is node.parent or node.ancestor(s).
Specialized references and children
Sometimes, when one concept extends another, we not only want to inherit all of its members, but also want to override some of its traits. This is possible with children and references specialization. When you specialize a child or reference, you narrow its target typ and possibly also change its name. For example, if you have concept A which extends B, and have a reference r in concept C with target type B, you might narrow the type of reference r in C's subconcepts. It works the same way for concept's children.
You need to add the specializes: section to your link declaration and refer to the link from a super-concept that you are modifying.
The alias, referred to from code as conceptAlias, optionally specifies a string that will be recognized by MPS as a representation of the Concept. The alias will appear in completion boxes and MPS will instantiate the Concept, whenever the alias or a part of it is typed by the user.
Constrained Data Types
Constrained Data Type allows you to define string-based types constrained with a regular expression. MPS will then make sure all property values with this constrained data type hold values that match the constraint.
Enumerations allow you to define properties that hold values from pre-defined sets.
Each enumeration member has an name and an optional presentation.
Name (left column) - this string value is used to refer programmatically to this member.
Presentation (right column) - this string value is used to represent the enum members in a user model (completion menu, editor). If no presentation is explicitly defined, the name is used instead.
Aside from a set of members, each enumeration can optionally define the default member. You can consider a default member as an implicit value that is present for all properties for which there is no other value is explicitly set.
To access enumerations and their members programmatically, use the enum operations and the enummember type defined in the jetbrains.mps.lang.smodel language.
Enumeration members can be queried for their name and presentation. Checking a value of a property against an enum data type value can be done with the is operation.
Handy from name, from presentation and members operations on an enumeration give you access to the members.
An enum switch statement provides a convenient control structure - given an enumeration member, it’s easy to switch over to a particular member instances using the enum switch statement:
or the enum switch expression:
If some enumeration members are omitted and no otherwise branch is specified, a warning reporting a non-exhaustive switch is raised. Similarly, an error is raised when a single value occurs more than once in a switch.
Enumeration Data Types (deprecated, use Enumerations instead)
Enumeration Data Types allow you to use properties that hold values from pre-defined sets.
Each enumeration data type member has a value and a presentation. Optionally an identifier can be specified explicitly.
Presentation vs. Value vs. Identifier
Presentation - this string value will be used to represent the enum members in the UI (completion menu, editor)
Value - this value, the type of which is set by the member type property, will represent the enum members in code
Identifier - this optional value will be used as the name of the generated Java enum. This value is typically derived from either the presentation or the value, since it is meant to be transparent to the language users and has no meaning in the language. It only needs to be specified when the id deriving process fails to generate unique valid identifiers.
Name - when accessing enum data type's members from code, name refers to either presentation, value or identifier, depending on which option member identifier is active
Deriving identifiers automaticaly
When deriving identifiers from either presentation or values, MPS will make best efforts to eliminate characters that are not allowed in Java identifiers. If the derived identifiers for multiple enum data type members end up being identical, an error will be reported. Explicit identifiers should be specified in such cases.
To access enumeration data types and their members programmatically, use the enum operations defined in the jetbrains.mps.lang.smodel language.
Checking a value of a property against an enum data type value can be done with the is operation. To print out the presentation of the property value, you need to obtain the corresponding enum member first:
Attributes, sometimes called Annotations, allow language designers to express orthogonal language constructs and apply them to existing languages without the need to modify them. For example, the generator templates allow for special generator marks, such as LOOP, ->$ and $, to be embedded within the target language:
The target language (BaseLanguage in our example here) does not need to know anything about the MPS generator, yet the generator macros can be added to the abstract model (AST) and edited in the editor. Similarly, anti-quotations and Patterns may get attributed to BaseLanguage concepts.
MPS provides three types of attributes:
LinkAttribute - to annotate references
NodeAttribute - to annotate individual nodes
PropertyAttribute - to annotate properties
By extending these you can introduce your own additions to existing languages. For good examples of attributes in use, check out the Description comments cookbook as well as the Requirement tracking language cookbook.