11 KiB
| layout | title | weight |
|---|---|---|
| page | Code Rules Language | 30 |
Code Rules Language
Language Aspect
Code rules are defined in handlers, which are root concepts of language jetbrains.mps.lang.coderules. Handlers and other related roots should be created in a language’s aspect model corresponding to a specific kind of analysis. For example, language jetbrains.mps.lang.typechecking declares the aspect types.
Root concepts
The following table contains root concepts that belong to code rules definition.
| Concept | Description |
|---|---|
| Handler | contains rules and constraint declarations |
| Query | entry point to the constraints program |
| MacroTable | defines how terms are constructed |
| DataFormTable 1 | defines terms, the data structure that is used in unification |
Handler
Handler serves two purposes: it declares the constraints that can be used in the head of productions in this handler and its extensions, and it also declares rules, which are, simply put, procedures applicable to specific concept and (optionally) its subconcepts.
Rules
The aim of rules defined by handlers is again twofold: firstly, they may serve as regular “checking” rules, and also, most importantly, they contribute constraint productions. These are created with a DSL that allows mixing of productions and Java code, and can also include constraint fragments inside a production template.
The following example is from the experimental control flow aspect for baseLanguage. It demonstrates how a production is constructed using a template. A template is enclosed into a pair of %% … %% symbols and yields constraints wrapped into special <% … %> brackets. In this particular case write/2 constraint is optional and is only added to the body of production in case the condition is satisfied (a location corresponding to a local variable is written to only if it has an initialiser).

(example of rule with production template)
A handler is a concept in language jetbrains.mps.lang.coderules.

(example of a handler declaration — without contents)
Keyword extends allows to specify another handler that is to be extended. All constraint productions generated by rules in this handler will have higher priority. This enables to override the original handler’s behaviour.
Only declared constraints are allowed to be used in the heads of productions in rules of this handler or its extensions. Handler should declare one or more constraints, unless it only uses the constraints from handlers which it extends. Constraint declaration consists of name and arity, which together constitute a constraint symbol. Constraint’s arity is fixed.
An applicability condition, if specified, is checked before applying the rule.

(example of rule matching concept instances with condition block)
Rules can specify applicable concept, either exactly or including its subconcepts. One can also declare standalone rules, which are applied automatically on every evaluation of code rules.

(example of standalone rule without input)
Rule’s contents is a block of code that gets executed when the rule is applied to the source model. If the input is specified, the declared parameter is available in rule’s body.
Rules may affect the scope of model locations that are processed during an evaluation session. Suppose a production responsible for type checking a particular location in source model relies on presence of productions that are only available if some other location is processed as well. To guarantee that a certain model location is processed during the evaluation session, one uses require statement.

(example of using require statement)
Production templates
Constraint productions are discussed in details in the section on Constraint Processing System, and here we briefly enumerate the main concepts and their usage.
Productions can be created at any place within a rule’s body, which amounts to creating a production template. This means, a production within a loop generates a runtime constraint production on every iteration of this loop.
A production template has three parts called head, guard, and body. Their meanings correspond precisely to those defined for the constraint productions.
There is a certain limitation as to what constraints can be used in head: it can only contain constraints defined by this handler, or one of the handlers it extends. This limitation has its origin in the way productions are chosen when an active constraint is being processed. Namely, productions to match an active constraint are selected from the handler declaring this constraint, and handlers that are its extensions. The order of productions within the handler matters, the ones declared on top are matched first. Productions from extending handlers are prepended to the list of productions of the handler they extend.
Production’s body can contain any visible constraints, as well as predicates. A production must include either the body or the head, no production can omit both. Guard is optional, and it can only contain predicates.
A production with an empty head, not declaring any constraints to serve as its input, is considered an automatic production and is triggered automatically on start of constraints program execution.
Constraints in a production’s head can be declared as either kept or replaced. Replaced constraints are marked with a tilde ~.
Predicates, also known otherwise as “built-in constraints”, are represented either as binary operators, such as = for unification or == for equality, or they are boolean-valued functions. Unification or equality if used in guard, only tests that its arguments can be unified, otherwise if called from body, it invokes the actual unification or assigns the value to a logical variable.

(unifies predicate used in the guard and in the body)
There are also predicates that can only be queries, such as isFree/1 or isBound/1, testing if a logical variable is free or has value assigned. Such predicates can only be used in guard.
Arbitrary Java code can be called with eval/1 predicate. It accepts either an expression of boolean type, in which case it can be used in guard, or expression of any type when called from body. In the latter case the value expression evaluates to is ignored.

(example of using eval/1 predicate)
Both head and body can declare logical variables. By default a logical variable ranges over terms, although any POJO2 may be serve as a value.

(production declaring logical variables)
When a constraint with logical variable as one of its arguments is matched, that variable becomes bound to the corresponding argument of the matching occurrence. The scope of such binding is this production’s guard and body.
There is support for pattern matching, using terms as patterns, which may optionally contain logical variables. For this to work, a logical variable that serves as a constraint argument should have a pattern attached.

(production with pattern matching)
Everywhere locations in the source code (model) must be referenced, node references are used, which are introduced with the @ symbol. This is a shortcut for accessing a reference to a node, which is a regular POJO.

(example of constraint argument referring to a model location)
In order to make use of macro definitions, which are essentially parameterised production templates extracted to a separate root, one of the two pseudo predicates can be used: expand and call. The former accepts a node, whereas the second expects the arguments that are substituted as macro parameters.
Examples of expand/call pseudo predicates
Substitution — passing parameters via context
Template fragments within production template body
Alternative body
Query
Query is the entry point to a constraints program. The only purpose of a query is to activate constraints that trigger computation implemented by constraint productions.
A query is discovered by the API through its kind, which is specific to the language aspect. Query kind specifies parameters that can be passed to it. The body of a query can contain same constraints and predicates as regular constraints production.
Macro table
Macros allow to have parameterised constructors for terms. They can either create terms using nodes coming from the source model, or use the parameters to fill in values. By contrast with rules, macros are always explicitly invoked.
This feature may come in handy when data types used by the constraints program are complicated and include many levels of containment. For example, type checking with code rules relies on types being terms, which means types can contain other types.
Macros are defined in a root Macro table. Its contents is made of macro definitions.

(macro definition in a macro table)
Macro expands instances of specified concept. This means a macro can be invoked on an instance of this concept. In case macro is expanded, its parameters are automatically initialised to appropriate values. Otherwise, a macro can be directly called with parameters specified explicitly.
Expand/call from production body
- Macro body is a template, with every constraint or predicate produced from template fragments being inserted into the production body at the invocation site. There is a special logical variable available within macro body,
macroLogical, which captures the logical variable on the left side ofexpandorcallpredicate.
Substitutions
DataForm table
DataForm table contains dataform declarations, a.k.a. terms. A term is a simple recursive data type that is suitable for implementing unification. Also, terms can model any other data structure, in particular they are suitable for representing types in a programming language.
Dataform is defined by its symbol and its features, which can be a POJO, a child dataform, or a list of dataforms. A feature may have a default value provider. A dataform extending another dataform adds new features or provides default values for existing ones.



