Converting existing software to void-safety

If you have been using Eiffel for a while, you may be maintaining systems which were developed before Eiffel became void-safe. If that's the case, then you will probably want to make those systems void-safe.

In this section we will use the experience of converting a set of simple (but not too simple) legacy Eiffel classes to show the types of issues that you may encounter, and how to deal with them.

So in the discussion below, you will see references to these classes:

Class name Description
APPLICATION Simple root class containing declarations of types NVP and NVP_LIST
NVP Class modeling name/value pairs of type STRING
NVP_LIST Class modeling a list of NVP's with specialized behavior; heir of TWO_WAY_LIST [NVP]

It's not important that you know much about the details of these classes. We will, however, look at certain parts of the classes in enough detail to resolve the conversion issues.

Conversion considerations

To redesign or not to redesign

During the process of conversion of classes to void-safety, the compiler will point out problems which you will have to fix. Some of these will be straightforward, while others may be tricky. It is natural, or sometimes mandatory, at times to consider changing elements of the design of your software.

Also, as you sift through your existing software during the void-safe conversion, you may not get very far before you see things that you wish had been done differently. This occurs often during reviews of existing systems, not just because of the introduction of void-safety.

In the discussions that follow you will see these redesign opportunities arise, and the decisions that were made for these cases.

Be aware of changes to Eiffel libraries

The libraries distributed with EiffelStudio have been converted to support void-safety. Mostly the changes made will cause no problems for existing software. However a few changes have been identified as "breaking" changes. You may or may not encounter the effects of these changes, but you should be aware of how they could effect your software and what options you have for adapting to them. Breaking changes are described in the EiffelStudio release notes and in the page dedicated to Void-safe changes to Eiffel libraries.

Conversion process

Enable full class checking

First make sure your project will compile correctly under the configuration of EiffelStudio that you intend to use to convert to void-safe.

Then set the project setting Full Class Checking to True. Do a clean compile of your system.

Full class checking will analyze your classes to make sure that in cases of inheritance, features of the parent classes are rechecked for validity in the heirs.

Here's an example of the kind of error you might expect when compiling with full class checking:

The situation here is that the feature split has been inherited (from class TWO_WAY_LIST [G]) by our class NVP_LIST. Feature split includes code to create and attach feature sublist which is typed attached like Current which in this case means attached NVP_LIST. To do this creation, split uses a creation procedure make_sublist.

Now here's the rub: NVP_LIST has not named make_sublist as a creation procedure: create make, make_from_string, make_from_file_named If we go to the create part of NVP_LIST and add make_sublist to its list of creation procedures, this will fix the problem: create make, make_from_string, make_from_file_named, make_sublist

So, fix any problems that arise out of turning on full class checking.

Enable other project settings

The second step in conversion of existing software is to change the values of the other void-safe related project settings and use the void-safe configurations for any delivered libraries and precompilations.

In the project settings for the target in which you are working, set Void safety to Complete, Transitional , Initialization or Conformance.

Note: Remember that during a transitional period starting with v6.4, there will be multiple versions of the configuration files for Eiffel libraries and precompiles. For example, base.ecf (void-unsafe) and base-safe.ecf (void-safe). Starting with v16.11 there is only one configuration file for libraries (e.g., base.ecf) that works with both void-safe and void-unsafe client software, but if you are using a precompile, there could be different versions for void-safe and void-unsafe precompiles.

If necessary, remove Eiffel libraries and any precompiled library that your project uses and re-add them with their void-safe configuration files. Because you've set your target to void-safety, when you click Add Library, you should see only void-safe configurations by default. You will see a check box on the dialog that you can uncheck if you want to see all available library configurations:

Now do a clean compile.

If you've replaced a precompiled library that you have not already built, EiffelStudio will offer to build it for you on the fly:

Now you should see error messages representing any situation in your project in which the compiler determines that it cannot guarantee void-safety.

This is what our legacy system produced:

Fix the issues

Next you fix the problems that the compiler discovered. The compiler errors concerning void-safety typically will be of three varieties.

  1. VEVI: violations of the Variable initialization rule. An attached variable is not properly set.
  2. VUTA: violations of the Target rule. The target of a feature call is not attached.
  3. VJAR (and other related codes): violations of attached status considered in conformance. The attachment status of the source of an assignment (or an argument to a feature call) is not compatible with that of the target of the assignment (or the formal argument).

Let's look at some specific cases and how fixing them unfolds.

Variables not properly set

There are two VEVI errors like this in class APPLICATION of our legacy system. They are probably the most obvious and easiest cases to handle.

feature {NONE} -- Initialization make -- Run application. do ... end feature -- Access my_nvp: NVP -- NVP for testing my_nvp_list: NVP_LIST -- NVP_LIST for testing

Here attribute declarations for my_nvp and my_nvp_list are made. These are assumed to be attached because of the project setting. But the create routine make fails to create objects and attach them. So by adding those creations, as shown below, the compiler is satisfied.

make -- Run application. do create my_nvp.make ("SomeName", "SomeValue") create my_nvp_list.make ... end

In a second case, there is also an Initialization rule violation (VEVI), this time on Result, in this routine:

at_first (nm: STRING): NVP -- The first found NVP with name matching nm. -- Or Void if not found require nm_valid: nm /= Void and then not nm.is_empty local tc: CURSOR do tc := cursor start name_search (nm) if not exhausted then Result := item end go_to (tc) ensure index_unchanged: index = old index end

Here we cannot just ensure that Result is always attached, because, as indicated by the header comment, Result is allowed to be void by design.

So the least impact to this routine will be to declare its type as detachable:

at_first (nm: STRING): detachable NVP -- The first found NVP with name matching nm. -- Or Void if not found

The same change is made in other routines that can return void by design, particularly including a routine called value_at_first, which gets our attention next.

The case of at_first offered us an opportunity to redesign (or not). We might have been able to leave at_first attached. After all, in void-safe software, the fewer detachables, the better. Maybe we could devise a way, possibly through preconditions and other queries, that would guarantee that at_first attempts to execute only when it can return a value.

But at_first is an exported query, so a consequence of such a change in the class design is that it would affect the class interface in such a way that existing clients would have to be modified to comply. In other words, it would be a "breaking" change.

Source of assignment does not conform to target

The change to at_first satisfies the VEVI issue in at_first, but it introduces a previously unseen conformance issue (VJAR) in the routine value_at_first:

value_at_first looks like this:

value_at_first (nm: STRING): detachable STRING -- Value from first found NVP with name matching nm -- Or Void of not found require nm_valid: nm /= Void and then not nm.is_empty local tn: NVP do tn := at_first (nm) if tn /= Void then Result := tn.value end end

The problem is that the local variable tn is declared as attached, but we know that now the result of at_first is detachable, making this assignment invalid: tn := at_first (nm)

Here the attached syntax can fix the problem and streamline the routine:

value_at_first (nm: STRING): detachable STRING -- Value from first found NVP with name matching nm -- Or Void of not found require nm_valid: nm /= Void and then not nm.is_empty do if attached at_first (nm) as tn then Result := tn.value end end

In this version tn need not be declared as a local variable. Remember that the attached syntax provides a fresh local variable, if the expression is not void.

Both VEVI and VJAR errors

A design issue in class NVP_LIST causes both conformance and initialization compiler errors. In the original design, an instance of the class NVP_LIST could traverse its contents NVP-by-NVP with inherited functionality. Additionally, NVP_LIST has immediate functionality allowing an instance to traverse its contents in two different ways returning "sublists" based on recurring patterns of the name attributes of a sequence of name/value pairs.

These two traversal methods are referred to as "sequencing" and "segmenting". It's not important that you understand the details of what these traversals do. But it is important to know that a valid instance of NVP_LIST can either be in the process of sequencing or in the process of segmenting, or neither. It is invalid to be both sequencing and segmenting.

Two class attributes are maintained to store the recurring patterns of values of {NVP}.name that guide traversal:

feature {NONE} -- Implementation sequence_array: ARRAY [STRING] -- The current array of names being used for -- sequence traversal segment_array: ARRAY [STRING] -- The current array of names being used to determine -- the termination of list segments

In the original class design, each of these attributes would be void unless their corresponding traversal was active. So the class contains the following clauses in its invariant:

not_sequencing_and_segmenting: not (segment_readable and sequence_readable) sequence_traversal_convention: (sequence_array = Void) = (not sequence_readable) segment_traversal_convention: (segment_array = Void) = (not segment_readable)

Of course by default these attributes are considered to be attached. So, because they are not initialized during creation, we see initialization errors. Because elements of the class intentionally set them to Void, we see conformance errors.

Here we have another opportunity to redesign (or not). We could mark the two arrays as detachable, recompile and fix any problems this causes (in fact, it causes eight errors: six Target rule violations, and two conformance issues).

However, because these attributes are not exported, we may be able to leave them attached and make changes to the implementation design without making breaking changes to the interface.

Those exported features which take arguments of the type ARRAY [STRING] which will serve as sequencing or segmenting control also require that the array contain at least one element. For example, the contract for feature segment_start contains these preconditions:

segment_start (nms: ARRAY [STRING_8]) -- Place segment cursor on the first occurrence of a seqment of list which -- begins at the current cursor position and -- terminates in a sequence with names equivalent to and ordered the same as `nms'. -- If no such sequence exists, then ensure exhausted require nms_valid: nms /= Void and then (nms.count > 0) not_sequencing: not sequence_readable

Because the restriction always exists that a valid sequence_array or segment_array must contain at least one element, it is possible to redesign the implementation of the class such that an empty sequence_array and segment_array could serve the same purpose as a Void one does in the original design.

So the invariant clauses that we saw above would now become:

not_sequencing_and_segmenting: not (segment_readable and sequence_readable) sequence_traversal_convention: (sequence_array.is_empty) = (not sequence_readable) segment_traversal_convention: (segment_array.is_empty) = (not segment_readable)

We already have compiler errors (VJAR's) that point us to those places in which we have code that sets either sequence_array or segment_array to Void. Like this:

segment_array := Void

These instances need to be changed to attach an empty array, maybe like this:

create segment_array.make (1, 0)

Additionally, some postconditions which reference the implementation features sequence_array and/or segment_array would have to be changed. Looking at the postcondition clauses for segment_start we see that segment_array is expected (or not) to be Void:

ensure started: (not exhausted) implies (segment_readable and (segment_array /= Void) and (last_segment_element_index > 0)) not_started: exhausted implies ((not segment_readable) and (segment_array = Void) and (last_segment_element_index = 0))

To support the "empty array" design, segment_start's postcondition clauses would be:

ensure started: (not exhausted) implies (segment_readable and (not segment_array.is_empty) and (last_segment_element_index > 0)) not_started: exhausted implies ((not segment_readable) and (segment_array.is_empty) and (last_segment_element_index = 0))

See Also:
Converting EiffelVision 2 Systems to Void-Safety
Void-safe changes to Eiffel libraries