MPS 2019.2 features a new way of declaring structure enumerations. Therefore the existing enumeration declarations become deprecated by this feature, MPS provides a set of migration scripts to replace them as well as their usages with the new constructions. This article precisely describes how these migrations are processed.
Upgrading enumeration declaration
Firstly, the migration replaces the old enumeration declarations with the featured declarations. For each deprecated enumeration in a migrated language the migration creates a new enumeration using the featured concept. Also, the migration places the old enumeration declaration in a migration's particular attribute marking that this declaration is no longer operating and stays there only for documenting and as migration's auxiliary.
Since the new declaration holds different kinds of properties (means in general sense, not a concept's property), mapping old declaration's properties to the new ones might be non-trivial. Here we describe how the migration handles each property of the old enumeration declaration:
name of the old enumeration are trivially copied to the name to the new declaration;
"has default" and "default member" became just "default member";
"empty text", "member datatype" and "member identifier policy" are no longer supported;
For each old enumeration's member, the migration creates a member in the new declaration. Member's "external value" became "presentation" (that is the optional text line in 2nd column of new enumeration declaration). Member's "internal value" and "identifier" no longer are supported but they are used to choose a suitable name for new enumeration member.
Upgrading enumeration properties
For each migrated enumeration declaration, the migration replaces property declarations of the old enumeration type with a new property declaration of the new enumeration type. The migration places the old property declaration in a migration's attribute to handle the migration of its usages properly.
Migration of smodel and aspect languages' code
MPS 2019.2 provides concise experience working with the new structure's enumerations. Therefore the SModel language presents new enumeration's property values with "enummember<_>" type in smodel queries instead of raw values. The raw values were essential for old enumeration members but had lack of design-time support in smodel queries and therefore were dropped with the new enumerations and replaced with "enummember<_>" types. A new approach guides the language designer towards writing correct code with more restrictive types for property access operations and reduced boilerplate via providing elegant switch operation over enum members. However, this approach is not compatible wild the smodel code behaviour for old enumerations where the language designer was forced to use "raw value" of an enumeration member. Therefore, additional migration is required to update smodel queries to work with the new enumerations.
Migrations of smodel and aspects code are triggered when the code refers to enumeration properties or enumeration declarations that were replaced with the new alternatives by the structure aspect migrations. Basically, for each such reference, one of these migrations updates it so that it refers to the new counterpart. The migration also adjusts the code nearby to preserve code's semantics. There are several cases where such procedure might change code significantly, so we describe them in more detail below.
Node's property access operations for enumeration properties represents one such situation where the migration changes the code a lot, since the typing rules of these expressions are different between the old and the new enumerations. As an example, consider the following enumeration's classic declaration and smodel code that reads a property of this enumeration's datatype and stores it in a local variable.
Old declaration of “WeekDays” enumeration
Property declaration “dayOfWeek” of enumeration type “WeekDays”
Usage of “dayOfWeek” property in smodel code
After the migration is applied, we get the following enumeration's declaration:
Migrated declaration of “WeekDays” enumeration
If naive migration of smodel code was applied, it would merely lead to a type mismatch error between the "string" and the "enummember<WeekDays>" types. Instead, to prevent a typing error, the migration wraps each property access with additional code that restores code's original semantics. In this particular case, migration produces a call to the "name" operation on enum member returned by the property read operation:
Migrated usage of “dayOfWeek” property in smodel code
In the described situation, the produced code might differ depending on the particular old enumeration declaration. There are general rules which migration follows when handling smodel code:
If the enumeration declaration had a "boolean" member datatype, then smodel code semantics is restored by use of "is" checks on enumeration members.
If the enumeration declaration had "integer" member datatype and its members form ordered ordinal number sequence starting from 0 or 1, then the migration generates "indexOf" operations upon the enumeration's member list.
If the enumeration declaration had "string" member datatype, the migration could generate "name" or "presentation" operation on enumeration member, if the given names/presentations for new member declarations match their past internal values. Usually, this is the case unless the internal values do not satisfy naming constraints and don’t match member presentations.
Otherwise, the migration produces calls to special methods that emulate the semantics of an initial smodel code. The migration script creates such methods beforehand in a distinct model called "enumMigration" inside the enumeration-owning language.
The migration script applies similar transformations in all places where type mismatch might occur due to upgrades to some enumeration property. Here is a list of such places:
Node's property read and write accesses
Generator's property macro queries
Property constraint's queries
Editor's queries for transactional property cells
Property initializers for node builders (a.k.a. light quotations)
Property antiquotations in node quotations
Usages of property pattern variables in node patterns
Another place where migration has to transform code significantly to keep original semantics is old "name" and "value" operations on enumeration member. The migration handles such places in a way similar to described above.
As there are potentially lots of places where a transformations might be used, the migration script also detects patterns in the code where such transformation are not required or can be simplified in order to produce fewer changes to the user codebase. For instance, if the migration encounters the following code, then it doesn't add any "name" operations to the right side of property reads:
Although MPS offers automatic migration for new enumeration, a user might end up doing manual migration of his/her code. For example, this may happen during resolving merge conflicts. Here we describe common scenarios of a manual migration.
If you encounter non-migrated enumeration or non-migrated enum property in a language on which MPS has already executed migrations consider to rerun migrations scripts via `Migration -> Migrations -> Execute Re-Runnable Migrations`.
Non-migrated smodel code can be handled with this action also. However, you also can update smodel code manually by replacing references on properties, enumerations or its members in smodel code from old declarations to new ones. After doing that, type errors might occur, so you have to resolve them manually as well.
Sometimes smodel code might depend on property not by directly referencing it but rather indirectly via property attribute. To migrate property attributes manually, you have to invoke "Migrate by hand" intention on the attribute and fix probable type errors afterwards.
Enumeration migration produces lots of additional attributes during the migration process. The migration framework requires such attributes for processing correctly further migrations on depending modules and projects. Although, language designer might not expect to have such depending modules outside his/her project and hence would like to remove migration attributes. This chapter describes how to do it and in which situations it is a safe change.
There are three kinds of artefacts that the migration process produces: migration attributes on new enumerations, migration attributes on enumeration properties and auxiliary methods in "enumMigration" model.
Attributes on the migrated enumerations provide data for correct migration of smodel code that refers to particular enumeration and of property instances in models written in the owning language. Generally, it means that if the language that owns such enumeration is published externally, migration attribute should be kept to perform the migration correctly. However, it's not the case for sandbox/test languages or languages in internal projects. For these scenarios, a language designer might legitimately want to drop enumeration's migration attributes by invoking "Drop Enumeration Migration's Attribute" intention on a migrated enumeration.
Attributes on migrated enumeration properties are required only for handling dependent smodel code. That means that if a language designer doesn't expect anyone to extend his/her language or maintain smodel code that operates with the enumeration property, then such attribute can be dropped safely. To make this, consider invoking "Drop Enum Property Migration's Attribute".
Likewise, auxiliary methods in "enumMigration" model are only needed for migrating smodel code. If the designer doesn't expect any smodel code operating with the migrated enumeration's members outside his/her project and has no occurrences inside the project, he/she might remove such methods manually.
Note that removing attributes is up to your choice. The MPS team, for example, schedules to perform automatic code clean up after a few releases.
Implementing automatic migrations for meta-languages
This chapter is mainly for MPS extensions' writers who implement either a new aspects or extend existing aspects (e.g. editor). Here we describe how they should update their extensions to tailor changes in MPS meta-languages.
This chapter describes the MPS metalanguages API for migrating enumeration data types and therefore contains lots of link to MPS source code that refer to either handy utility methods or examples for migrations. Open MPS 2019.2 application to be able to navigate by these links.
Generally speaking, if some MPS extension provides a concept that refers to a property declarations or a datatype declarations or depends on them by other means, the designer of this concept has to provide a migration to update the concept's instances. Otherwise, after applying the MPS enumeration migrations, nodes of this concept become to depend on outdated code, so a user becomes forced to fix them manually. The migration might differ between different concept so MPS can't provide a general solution for all possible extensions. Instead, MPS provides a set of utility functions that help extension's author to implement migration efficiently. Below we describe a few possible common scenarios.
If a concept has a reference link on a property declaration:
For such a concept, a language designer has to write a migration script that updates it on a new declaration if this property is of enumeration type that has been migrated. To do so, the extension's author might use the auxiliary method `EnumUsagesMigration#migratePropertyReference`. It accepts a node that has a reference for migrating and a reference link that targets PropertyDeclaration instances. The method returns a new target node of a given reference if it has been updated, or null otherwise. To use this auxiliary, the extension designer needs to import `jetbrains.mps.lang.structure.migration` model into the extension's migration aspect.
A canonical example here is a migration for editor cell properties.
If a concept has a reference link to a datatype declaration:
Similarly to the scenario above, a language designer has to implement a migration script to update such concept's nodes. He/she might use `EnumUsagesMigration#migrateEnumReference` to achieve this. The method accepts a node that has a reference for migrating and a reference link that targets to EnumerationDataTypeDeclaration_Old or its superconcepts. It returns a new target node for the given reference if it has been updated, or null otherwise.
Migration for “enummember<_>” instances might be an example of “migrateEnumReference” usage.
If a concept has a reference link to an enumeration's member declaration:
To migrate such concept, the language designer might use `EnumerationMemberDeclaration_Old#findReplacement()` behavior method to get an EnumerationMemberDeclaration instance that replaces old declaration. As `EnumerationMemberDeclaration_Old` and `EnumerationMemberDeclaration` concepts have no essential common superconcept, there is no option to reuse one reference link to refer to nodes of both concepts. Therefore the language designer has to consider introducing a new link in the concept and migrate from the old reference link to the new one. If a concept has the meaning of a smart reference, as an alternative, the language designer can introduce a new concept with the new link and migrate old concept's instances to new concept's instances.
Migration for deprecated “is” operation over enumeration properties can serve as an example.
If the typing rules of BL expressions or queries depend on a datatype:
Some extensions might cooperate with baseLanguage and smodel languages by introducing new expressions or queries. For such concepts, a language designer writes typing rules which might consider a baseLanguage's type of a referenced property or enumeration. MPS 2019.2 now provides a utility to evaluate a baseLanguage type for given structure's datatype - `jetbrains.mps.lang.smodel.typesystem.RulesUtil#datatypeBLType(node<DataTypeDeclaration>)`. This utility should be used in the typesystem aspect's rules to acquire proper property value's BL type by a given datatype.
If some concept is being migrated from the old enumerations to the new ones as in the first two described scenarios, and it has typing rules that manipulate with property value's BL type, then a node of this concept might end up having type errors even if this node was correct before the migration.
Lets return back to an example of migration of smodel code for the “WeekDays” enumeration described in “Migration of smodel and aspect languages' code” section. When the smodel migration updates a property reference in the property access operation, the BL type of such operation suddenly changes from `string` to `enummember<WeekDays>` and hence the smodel migration additionally wraps this property read with a call to the `name` operation that extracts the raw value from the enumeration member so that the overall expression behaves the same way as it did before the migration was applied. The smodel migration script does not actually implement the creation of such a call but delegates to an auxiliary class `jetbrains.mps.lang.smodel.migration.EnumExpressionsMigration` instead. This class provides a set of methods that the language designer can use to pass through similar problems in his/her extensions.
Particularly, it provides several methods:
`downgradeExpressionType(node<EnumerationDeclaration> enumeration, node<Expression> expression)` - transforms a given $expression$ of `enummember<$enumeration$>` type to evaluates it to a raw value of the originally evaluated enumeration member.
`upgradeExpressionType(node<EnumerationDeclaration> enumeration, node<Expression> expression)` - transforms a given $expression$ that evaluates to a raw value by means of the old declaration of $enumeration$ to evaluate it to an actual enumeration member that represents such a raw value.
`upgradeQueryReturnExpressions(node<EnumerationDeclaration> enumeration, node<IMethodLike> query)` - transforms a given $query$ body that evaluates to a raw value by means of the old declaration of $enumeration$ to evaluate it to an actual enumeration member that represents such araw value. Technically it collects all return clauses of the query and transforms them with `upgradeExpressionType` functionality.
`optimize()` - attempts to optimize the code around an expression that has been transformed with this the `EnumExpressionsMigration` instance. A general workflow with `EnumExpressionsMigration` follows these steps: it is instantiated at the beginning of a migration script, it uses the 3 methods described above to correct the user models and then the `optimize()` method is invoked to clean up the transformed code.
Example: Migration script for property constraints highlights the usages of EnumExpressionsMigration utility.
If a concept extends PropertyAttribute:
Even if the `PropertyAttribute` concept has no direct reference to `PropertyDeclaration`, the language designer might discover such instances through a deployed `SProperty` instance via `PropertyAttribute#getProperty()`. Since 2019.2, the `PropertyAttribute` concept provides a `#getPropertyDeclaration()` method that is aware of the enumeration migration, and for enumeration properties it provides the old property declaration if the attribute hasn't been migrated yet and the new property declaration if the migration has already been applied.
If a concept extends `PropertyAttribute` then there should be a migration that updates attributes of such a concept. The migration script has to invoke `EnumUsagesMigration#migrateEnumPropertyAttribute(node<PropertyAttribute>)` to update it. The method returns a new enumeration property declaration if the attribute is for the property of an enum type and the attribute hasn't been migrated yet. Otherwise, the method returns null.