6.7 Modules and Design Considerations

In order to understand the points being made about design in this section, it is necessary to know something about what happens when the various kinds of modules are compiled--for each of the three compiles in a somewhat different way.

As previously noted, when a definition module is compiled, a symbol file of the various entities with their syntax is created so that compilations of client programs can reference the entities defined. At the same time, a unique identification key is generated by the compiler and placed in the symbol file. If that particular definition module is ever re-compiled for any reason, the new key generated will be different from the old one.

Exception: Some compilers ignore comments to the extent that changing only these does not produce a new key on recompilation. Do not count on this fact.

NOTE: The definition of Modula-2 does not require that the symbol file be separate from the definition module source code; an implementation might choose to keep the module key with the source and do nothing else. Then, the source would also be the symbol file.

When the implementation part of a library module is compiled, the disk is searched for the symbol file of the corresponding definition part. For the compilation to be successful, the syntax of the various entities must match. For instance, a procedure heading that is given in the definition part and then altered, say, as to the number and type of parameters, or omitted in the implementation, would cause compilation of the implementation to fail.

However, something else takes place at this time. The compiler also copies the previously generated key from the symbol file into the code file that is generated for the implementation part.

Likewise, when a program module is compiled, the syntax of program statements is checked against that defined in any modules being imported from, and the names and keys from all the definition parts of the library modules are collected and stored in the object code file from the program module. When the code files are all linked, the implementation modules are all read, and the keys are checked. If there are any differences, the system will refuse to link together the (presumed) incompatible modules, and the program will not link or run.

It must be understood that this checking of version compatibility is not implementation dependent, but is part of the very warp and woof of the notation. All Modula-2 systems must implement this library control behaviour, though the way in which they put it into practice may vary slightly.

The result is that compiling a program depends only on the definition modules for its success, but linking and running it requires that the implementation modules not only have been coded and compiled by this time, but that their keys agree with those of their corresponding definitions.

If it is later decided that there is a more efficient way to write the implementation of one of these portions of the package, this can be done without affecting either its definition, or any client programs.

On the other hand, any changes or recompiling of a definition module (with the possible exception of changes in comments in some implementations) requires that all clients depending on it also be recompiled before trying to link again. After all, changes at this level imply that there may now be syntax errors in both its corresponding implementation and in all client programs.

This behaviour of Modula-2 has two important implications for the designers of programs.

The first is that a program can be decomposed into a collection of modules, and the responsible team of programmers can agree collectively on the content of the various definition modules. These can be written and compiled so as to be available for all later stages of the project. Once this is done, the implementation modules and the program module can be assigned to various members of the team without any consultation on the details being necessary. This is because the definition modules contain the entire interface among the various parts of the final program, and are the only means by which they can obtain information from one another. Any changes one team proposes to a definition module have to be agreed to by all the teams, incorporated into the common interfaces, compiled and distributed to the teams before anyone uses them.

One consequence is that it does not matter what names each member of the team uses for variables and other local entities in the individual sections; there is no possibility for conflict among them. Modula-2 therefore completely eliminates side-effects caused by inadvertent re-use of entity names from one compilation unit to another. There is still a problem if a name is re-used within the same compilation module of course.

Furthermore, if someone finds a more efficient way of writing the implementation of a library module at a later date, this can be done, and the new version compiled, without having to re-compile the definition module or even any client program. (The keys all still agree.) However, if the definition module is re-compiled for any reason (even if it has not actually been changed), a new key will be is placed in its symbol file, and the corresponding implementation part of the module and any program modules using it will become incompatible and will have to be re-compiled before the next attempt to link the code together.

Second, it is important to keep these dependencies in mind even in small projects, because ignoring them could cause major problems. These could arise from one of two situations:

1. The programmer makes changes to the number and type of parameters for some user library routine. Here, the enforcement of version compatibility is of great benefit, for the definition, implementation, and client program modules must all be rewritten and recompiled to reflect the changes. If it were possible to change the first without changing the other two, then the (presumably correct) definition part would interface to an incorrect or obsolete implementation part. This would undoubtedly cause a system crash at run time if it were allowed.

2. The programmer inadvertently recompiles a definition module without actually making any changes in it. This time, unnecessary problems are created, for what appears to be an identical definition part is in fact different, because of the new key associated with it. Since the implementation modules get their key from the previously compiled definition part of the module with the same name, the actual code is now inaccessible to the linker. It will compare the key from the program module, obtained from the previous version of the definition, with the key in the implementation obtained from the new version of the definition, and refuse to do the link because they are different. The system cannot, after all, "know" that you have made no changes--the assumption is that the programmer must have had some reason for re-compiling the definition part.

If linking is done at run time, these problems will become evident the first time one tries to run a (perhaps previously successful) program that is a client of the incompatible module. If, on the other hand, the system does linking at a separate step and requires all code from library modules and elsewhere to be stored in a separate executable file (the most common method), the previously linked programs would still work, but a new attempt at linking would fail.

One can get into real trouble of the second type, because in some versions of Modula-2 the source codes for the system library definition modules are distributed on the disks along with the compiled versions. If one compiles any of these, access to the associated library module is cut off. Imagine not being able to use STextIO or any module that depends on it! Since few vendors also include the source code for the implementation parts of library modules for recompiling, it would now be necessary to restore the original from a back-up (there is one, isn't there?) or place an embarrassing call to the vendor for a new disk.

Careful housekeeping and cautious file management will prevent such problems from arising. Original disks should be copied and filed in a safe place, and the text files for definition modules should be printed out for reference and then deleted from operating disks to prevent their recompilation by a user of the final package. Likewise, text files for a programmer's own library modules should be backed up on separate disks than those from which the compiled versions will eventually be run.