Inspirel banner

Metaprogramming

As defined in Wikipedia, metaprogramming is a programming technique in which computer programs have the ability to treat programs as their data. In the context of modeling, the model is the subject of computation.

The previous chapter on preprocessors already explored this idea in the sense that a preprocessor took some model as its input and returned another (preprocessed) model as a result. The limitation of preprocessors, however, is that they can operate only on operation bodies and have no impact on the design structure, where packages, types, data objects and operations are defined.

As a simple example showing a possible use for metaprogramming the following solves a problem of defining multiple data objects according to some naming pattern. That is, instead of defining a number of data objects:

Image

the engineer might want to make his life easier by automating this process (and he is not satisfied with copy-pasting code around in the text editor).

This task can be automated with the help of the Symbol function, which creates a Wolfram symbol from the given name. The list of rules for data object definition can be created with the following Table expression (here limited to just 4 symbols):

Image
Image

Such a list is exactly what is needed in the defineDataObjects function, so the complete definition for 100 data objects is:

Image

This is sufficient to define all 100 data objects, which can be demonstrated with this (shortened!) generated C++ header:

#define FILE_MY_PACKAGE_H_INCLUDED

#include <cstdint>
 
namespace my_package
{

// Data declarations.

extern std::int32_t signal1;
extern std::int32_t signal2;
extern std::int32_t signal3;
// ...
extern std::int32_t signal99;
extern std::int32_t signal100;

} // namespace my_package

#endif // FILE_MY_PACKAGE_H_INCLUDED

Of course, in a real project, signal names would be read from a configuration file or perhaps an external database.

The above definition invoked the defineDataObjects function just once, which created a single group of 100 data objects. It is also possible to create distinct groups containing just one definition by invoking defineDataObjects multiple times instead:

Image

This way the defineDataObjects function was invoked in a loop (driven by Scan and an anonymous Function), once for each symbol from the list - but the most interesting part of this example is that individual symbols (that is, signal1, signal2, and so on) were passed to defineDataObjects as parameters, via symbolName. That is, each time defineDataObjects is called with a different value of some parameter and a different definition is created in the model.

The parameters can be injected into the model (except the operation body) by means of any symbol that has some value - the symbol will be naturally resolved to its value and that value will be ultimately used as part of the model. This can be used to name some commonly used values in the model, for example:

Image

Here, rangeLimit is used as a symbol multiple times in the model. To be exact, the defineDataTypes function does not even see this symbol, only its already evaluated value, which is 100 - that value will be later used in all relevant places, especially in proof statements, to verify that values of these types really fall into defined ranges. But from the user perspective the whole code snippet above becomes part of the model and rangeLimit becomes the model parameter.

Similarly, two arrays of the same size can be defined as:

Image

which leads to the following definitions in the final generated code (C++):

typedef std::int32_t one_array[10];
typedef std::int32_t second_array[10];

The above examples can be used as explanations for the lack of genuine constants in the core modeling language. Constants would be used by programmers to achieve the same effect in a typical programming language, but the added complexity of the core language and the code generator (as a hint, C does not support constants in the same way as C++ does) were not considered worth the effort, as parameterized modeling constructs have much broader applications. The disadvantage of this flexibility, however, is that constants are no longer named in the final generated code, where they degrade to become so-called magic numbers. Comments added to definition groups can help to make the resulting code more self-explanatory.

The possibility to parameterize calls to basic FMT model-building functions allows also to achieve the effect of generics or templates known from Ada and C++ - just as constant values can be parameterized, so can be types.

A simple swap operation can be used to demonstrate this capability:

Image
Image

The above is a straightforward implementation of swap - but it is limited to only integer type, which is hard-coded both in the operation profile (via parameter types) and in its body (via local variable type). If another swap operation is needed for boolean, or any other type, both declaration and definition would have to be repeated with some new operation name, as operation overloading is not supported in FMT. Such a repetition would be clearly wasteful and possibly error-prone.

The metaprogramming solution to this problem is to parameterize the above invocations, for example:

Image

The makeSwap function goes beyond solving the initial problem and parameterizes not only the target type, but also the model and package name. This makes it even more flexible, but requires setting the HoldFirst attribute to allow that function to modify the model object (this is a standard Wolfram way of saying that the first parameter, which is a model object, is passed “by reference” and not “by value”).

Possible instantiations of such generic swap in two different models can look like:

Image
Image

After executing these actions, the swap operation is instantiated in package “MyPackage`” within model sys under name swapInts for integers and swapBools for Boolean values and, additionally, as just swap for integers in package “Utils`” within unrelated model otherSystem. This can be seen in the final generated source code files, this time for Ada.

Specification of package MyPackage (file my_package.ads):

package My_Package is
   
   -- Operation declarations.
   
   procedure Swap_Ints (X : in out Integer; Y : in out Integer);
   
   procedure Swap_Bools (X : in out Boolean; Y : in out Boolean);
   
end My_Package;

Implementation of package MyPackage (file my_package.adb):

package body My_Package is
   
   -- Operation definitions.
   
   procedure Swap_Ints (X : in out Integer; Y : in out Integer) is
      Temp : Integer;
   begin
      Temp := X;
      X := Y;
      Y := Temp;
   end Swap_Ints;
   
   procedure Swap_Bools (X : in out Boolean; Y : in out Boolean) is
      Temp : Boolean;
   begin
      Temp := X;
      X := Y;
      Y := Temp;
   end Swap_Bools;
   
end My_Package;

Specification of package Utils (file utils.ads):

package Utils is
   
   -- Operation declarations.
   
   procedure Swap (X : in out Integer; Y : in out Integer);
   
end Utils;

Implementation of package Utils (file utils.adb):

package body Utils is
   
   -- Operation definitions.
   
   procedure Swap (X : in out Integer; Y : in out Integer) is
      Temp : Integer;
   begin
      Temp := X;
      X := Y;
      Y := Temp;
   end Swap;
   
end Utils;

What can be seen from these generated source files is that such repeated instantiations lead to some apparent duplication in the final code. This might be surprising, but in fact is not different from how compilers treat generics in languages like Ada or C++ - so-called macro-expansion approach instantiates generics by expanding generic definitions with substituted parameters (for example types) and the resulting duplication of code, even though not visible at the source level, is real at the object code level. Here, since modeling raises the level of engineering activity to above source code, the duplication is no longer implicit and becomes visible in the generated source files. Such apparent duplication is safe, since the actual definition exists only once, in the makeSwap function; modifications applied at this level properly propagate downstream to source code - and ultimately to object and executable code. As such, from the modeling perspective, the swap operation is modeled only once in terms of parameterized constructs and its definition is reused by instantiating it multiple times with actual parameter values.

Previous: Grammar preprocessors, next: Model synthesis

See also Table of Contents.