Extending SAVANT: A Tutorial - nturley/aire-xml GitHub Wiki

Introduction

The purpose of this document is to introduce the user to the SAVANT plugin extension mechanism. This mechanism allows the user to take complete control over the AIRE intermediate format generated by the scram compiler from VHDL source file(s). This document assumes a certain degree of familiarity with C++, Abstract Syntax Trees (ASTs), and scram. It also relies heavily on knowledge of the AIRE format, which has been covered extensively in other places. In addition to describing the extensibility mechanism, it also provides an example of building a static profiler for VHDL.

Extension Mechanism

As stated before the idea of the extension mechanism is to allow the user complete control over the ASTs generated during the parse of the VHDL source code. These ASTs are made of AIRE nodes, which completely represent the information originally in the VHDL source code. Each AIRE node is made up of two basic classes, IIR and IIRBase. The IIR level contains a interface to the AIRE API for that type of node. Next, is the IIRBase level, which contains the default implementation of the AIRE API. The actual API itself is documented in the AIRE specification, interested users are directed to this document, as the contents of AIRE are beyond this simple tutorial. To this the user should add their own node specific code. This is done by inheriting directly from the IIRBase level.

TODO: INSERT CUTE DIAGRAM

To allow multiple user extensions to function within the compiler, and allow the compiler to deal with the all extensions in an agnostic manner we manage them as plugins. First, all extensions are each located in a separate library, and interface to the compiler via a simple plugin interface API. Second, all plugins have an associated class factory which allows their AIRE nodes to be instantiated by the compiler in an indirect manner.

The plugin API consists of three functions:

  • getPluginType – Returns the general class of plugin.
  • getPluginName – Returns the name of this specific plugin
  • process_tree – Performs the extension action.

The function process_tree is the most important part of the interface, since the plugin uses this function to perform it's specific action. It takes three arguments, a copy of the current tree, and the number and type of arguments to the plugin specifically. The basic algorithm used is as follows. First, the old tree is transmuted into a new tree containing the plugin's AIRE nodes. Next, the plugin specific action is performed. Finally, the plugin's version of the AST is returned to the compiler.

Creating a new Plugin

To create a new plugin two steps need to be taken. First, the user needs to run the plugin_creation.pl script. Second, they need to update the process_tree function, and add their extension code to the relevant classes. Generally plugins will require a walk of the AIRE AST to perform their task. To support this the create_tree_walk.pl script will generate a generic tree walk, which can be used as generated, or modified to meet user requirements.

The plugin_creation.pl script automates the plugin creation process by performing the following tasks:

  1. Creates AIRE node classes. - It creates all the classes for the AIRE nodes, allowing the user to worry only about implementation specific details.
  2. Creates the class factory. - A class factory for creating all the AIRE leaf nodes is created.
  3. Creates the plug interface. - A default implementation of the plugin interface is created, allowing the user to modify this if desired, or simply extend the process_tree function.
  4. Creates the Makefile.am – A default Makefile.am is generated for building the entire plugin. This includes all the AIRE nodes, class factory, and the plugin interface generated by the script. Any special support classes needed by the plugin should be added to this manually.
  5. Creates the configure.ac - This is used by automake to setup the system for compiling. It includes a number of tests to make sure that the development environment supports everything necessary to build the project.

Before running the plugin_creation.pl script, the user defined variables need to be adjusted for the development environment in which the compilation is to run.

  • $Name – This is the name of the plugin it. It is used to generate the names of all the AIRE classes, plugin interface, class factory, and default name of the plugin library.
  • $RootDir – This is the root directory for the plugin that is to be created.
  • $SavantSrc – The directory which contains the savant source code.
  • $HeaderFile – This variable allows the user to define a file that contains text that should be placed at the top of all files generated by the script. Generally this is used for copyright notices, but could also be used for other things such as generic descriptions of the plugin's intention, author, date of creation, etc.

Automated Tree Walks

As stated above, the create_tree_walk.pl script automates the generation of a tree walk. This is done to provide users with a starting point for making their own specific modifications to the tree process, and to automate what is usually boilerplate code.

Here is the generic tree walking algorithm in pseudo-code:

generic_tree_walk() {
   if (not_visited) {
      not_visited = false
      perform_node_specific_actions()
      list->generic_tree_walk()
   }
}

Lists are a special case, so in the list base class we have the following code to walk to all the nodes in the list:

generic_tree_walk() {
   if (not_visited) {
      not_visited = false
      PTR *node = first();
      while (node != NULL) {
         node->generic_tree_walk();
         node = successor(node);
      }
   }
}

Note: Due to SAVANT's potential to generate ASTs with cycles, it's important to avoid processing the same node twice. Thus we use a simple boolean flag to indicate nodes that have already been visited, and do not process them a second time.

To use the create_tree_walk.pl script it's necessary to adjust the settings for the build environment.

  • $Name – Same as the plugin_creation.pl script, this defines the plugin's name, which is used in a variety of places. This should match the setting used for plugin_creation.pl exactly.
  • $RootDir – Location of the plugin we wish to modify. This should match the variable of the same name from plugin_creation.pl.
  • $SavantSrc – Location of the savant source code.
  • $functionSig - Function signature. This is the function declaration that will be added to the headers and source files for the walk. By using a signature, the plugin writer can specify what gets passed into the function. (ex: generic_tree_walk() )
  • $functionType - Return type for the function (ex: void)
  • $functionCall - Function call. Used then the function should be called for whatever reason (generic_tree_walk() )
  • @headers – Any headers associated with the walk that aren't included automatically. This might include system headers, such as iostream, headers associated with a passed in parameter, such as output_file, or functionality used within the body of the function, such as stdlib.
  • @forwardDefs – Forward definitions used to avoid including header files to support the generic tree walk functionality. This is most useful for function arguments, or return types. By using forward definitions instead of header files in the class header files we can reduce dependencies between files, thus reducing compilation time.

Example Plugin – XML Generator

This section details the complete creation of a plugin designed to generate XML files based on the VHDL source files parsed. The intention is to walk the user through the creation of a SAVANT plugin to allow them to see the details in action, and have some working code to experiment with.

Step 1 – Adjust Script Settings

As mentioned above, first we need to adjust the script settings. Since this is an XML generating plugin, I'm going to call it XML. I also work for the University of Cincinnati, so I'm going to use the header file to make all code copyright UC, 2004. Finally, I'm going to create all this in the savant/src/plugin/xml directory, so I adjust the OutputDir setting accordingly. Thus I have the following settings:

  • $Name = “XML”;
  • $RootDir = “/work/savant_plugin_tutorial”;
  • $SavantSrc = “/work/savant”;
  • $HeaderFile=”copyright”;

After adjust the plugin_creation.pl settings we run the script. It should produce a list of all the files as it processes them, finally terminating when all the files have been processed. Next, I want to generate a generic tree walk using the create_tree_walk.pl script. This will enable me to focus on only making changes necessary to support the addition of XML, and ignore the details of walking the tree. To do this I have the following settings:

  • $Name = “XML”;
  • $RootDir = “/work/savant_plugin_tutorial”;
  • $SavantSrc = “/work/savant”;
  • $functionSig = “xmlGeneration(xmlFile &file)”;
  • $functionType = “void”;
  • $functionCall = “xmlGeneration(file)”;
  • @headers = ( “\”xmlFile.hh\”” );
  • @forwardDefs = ( “xmlFile” );

NOTE: Some of these variables probably need to be adjusted to work with your development environment. In particular $RootDir & $SavantSrc will probably vary widely depending on where savant and the tutorial are located on your system. You also notice that the $function* make reference to the class xml_file, this is essentially a helper class that hides some of the details of generating XML files.

Step 2 – Update Interface

Now that the script has created all the files necessary to build a new plugin, we need to make some adjustments to get it all working. First, open I open the xml_plugin_interface.cc file, and adjust the plugin name. This is found in the getPluginName function mentioned earlier. Originally the string was “XML Plugin”, but I didn't feel this was detailed enough, so I changed it to “XML document generation from VHDL plugin”.

Next I adjusted the process_tree function. This is the interface entry point for the compiler, so unless we add our code to this function, the plugin will do nothing. You'll notice that the script has generated a comment for us, starting with “TODO:”. We'll remove that, and replace it with a call to start the XML generation. Thus replace:

// TODO:  Insert plugin specific code here!

with:

// Start XML generation
dynamic_cast<XMLDesignFile *>(new_tree)->xmlGeneration();

Step 3 – Add Plugin Code

This portion of the code varies widely from application to application. Essentially at this point the user needs to add in their code to the specific AIRE nodes that need to be extended. Due to this variance, this tutorial can only give examples related to build the XML generation plugin. However, the general concepts should be applicable to most plugins. There is also a hints section at the end to summarize some of the more efficient approaches to plugin design.

First we will design a class to support writing information out in the XML format. This will enable us to focus on only the VHDL portions of the code within the VHDL classes. Since this is going to be a fairly simple XML file, we should have a simple function for adding new elements. We'll call this addNode, and allow the user to pass in the name of the element, and it's contents if any. We also want to be able to include one element inside another, to preserve the hierarchical structure of the VHDL file, so we'll also include functions openScope, and closeScope. The function openScope will be similar to the addNode function, but without closing the element, and providing support for automatic indentation of the elements during publishing. Finally, we'll have a function for writing the file to disk, including all the XML header information, called publishDocument. Thus the XML publishing file should look something like this:

class XmlFile {
   public:
      /** Writes the XML document out to disk. */
      void publishDocument(const string fileName);

      /** Functions for adding information to the XML document */
      /** Adds a new "leaf" node */
      void addNode(const string tag, const string content = "", bool closeElement = true);

      /** Adds a new trunk node.  This opens a tag, and leaves it open. */
      void openScope(const string tag, const string content = "");

      /** Closes a trunk node.  It closes the last tag we opened. */
      void closeScope();
};

Next, we need to provide support for generation from the design file node. This is because all the nodes we will be handed from the compiler start at this node. Thus we add the xmlGeneration function to the XMLDesignFile. This function will function as our starting point into the rest of the AIRE abstract syntax tree.

The design file node contains a list of all the other library units (VHDL architecture, entity, package, etc) contained in the parsed file. Thus we'll process each of these nodes in the list. Since we want everything to be in a single file, we'll create the XmlFile class instance in the design file xmlGeneration function, and pass it down the list. Thus our XMLDesignFile::xmlGeneration() function looks like this:

XmlFile *outputFile = new XmlFile;
_get_library_units()->xmlGeneration(outputFile);
...
outputFile->publishDocument(fileName);

In order to handle the library units in the list, we also need to implement functionality to go through the list and invoke the xmlGeneration function. Since this is a pretty general technique, we'll place this in the XMLList class, which is the base class for all lists. If we need to perform some more specific tasks, we can override this function in the derived list classes. Thus our XMLList::xmlGeneration(XmlFile &) function looks like this:

for (IIR* ptr = first(); ptr != NULL; ptr = successor(ptr)) {
   dynamic_cast<XML *>(ptr)->xmlGeneration(outputFile);
}

Since we're dealing with everything at a very general level, we use the lowest common demoninator, which happens to be the XML base class. Thus we also need to add functionality to this class. We also want to add additional functionality to some of the derived classes, so we'll make this a virtual function.

Our default behavior is to add a node to the XML document we're creating, so the base class function will reflect this. Currently the AIRE ASTs also have the potential to create cycles, so to avoid this, we'll also add a variable to the base class, and check it to make sure we haven't already processed this node. Thus our XML::xmlGeneration(XmlFile &) code looks like this:

if (!bVisited) {
		outputFile.addNode(get_kind_text());
	bVisited = true;
}

Next, we'll expand the XML generation to the library units. These are pretty similar in a approach, so we'll only look at changing the two most common, XMLArchitectureDeclaration, and XMLEntityDeclaration. In the sample code, I've also modified the other library units and XMLProcessStatement.

VHDL architecture declarations consist of two parts. The declaration section consists of signals and other declarations which will be used in the concurrent statement section. The concurrent statement section consists of processes and other concurrent statements that perform the actions necessary to simulate the system. In each case we'll want to open a new scope, and also a scope for the architecture declaration itself. Finally, we don't want to start a new scope unless there's some actual content, so we'll check the list sizes. Thus our xmlGeneration function for XMLArchitectureDeclaration looks like this:

if (!bVisited) {
	// Process the node itself
	file.openScope(get_kind_text());

	//   Process the variables
	if (get_architecture_declarative_part()->size() > 0) {
		file.openScope("Declarations");
		_get_architecture_declarative_part()->xmlGeneration(file);
		file.closeScope();
	}

	if (get_architecture_statement_part()->size() > 0) {
		file.openScope("Concurrent Statements");
		_get_architecture_statement_part()->xmlGeneration(file);
		file.closeScope();
	}

	file.closeScope();
}

Our xmlGeneration function for entity looks similar, with a few more statements, due to the additional complexity of entity declarations.

if (!bVisited) {
	// Process the node itself
	file.openScope(get_kind_text());

	//   Process the variables
	if (get_generic_clause()->size() > 0) {
		file.openScope("Generic");
		_get_generic_clause()->xmlGeneration(file);
		file.closeScope();
	}

	if (get_port_clause()->size() > 0) {
		file.openScope("Port");
		_get_port_clause()->xmlGeneration(file);
		file.closeScope();
	}

	if (get_entity_declarative_part()->size() > 0) {
		file.openScope("Declarations");
		_get_entity_declarative_part()->xmlGeneration(file);
		file.closeScope();
	}

	if (get_entity_statement_part()->size() > 0) {
		file.openScope("Statements");
		_get_entity_statement_part()->xmlGeneration(file);
		file.closeScope();
	}

	if (get_architectures()->size() > 0) {
		file.openScope("Architectures");
		_get_architectures()->xmlGeneration(file);
		file.closeScope();
	}

	file.closeScope();
}

Step 4 – Compilation

Now that we have completed adding the code to the plugin, the next step is compilation. The SAVANT extension system uses libtool and automake to create plugins, so it is necessary that the latest versions of these tools be installed on the system the plugin is going to be compiled on. The plugin interface also relies on clutils, which is available from Clifton Labs via the same cvs archive that SAVANT and this tutorial are located in. Since SAVANT will not compile without clutils we will assume that this has already been compiled, and installed on the system.

The first step in compilation is to generate the configuration script. From the tutorial root directory type in the following command:

autoreconf -i

This will cause the configuration script, and it's support scripts and files to be generated. Next we need to generate the Makefiles. When the plugin_creation.pl script was originally run we generated the Makefile.am files, from which the configuration script will generate the Makefiles used to compile the system. During this step we use CXXFLAGS to enable debugging and warnings by setting it to “-Wall –ggdb3”.

In addition, as stated earlier we require clutils to be present to support the plugin interface, thus we also use the parameter –with-clutils=your clutils directory. Finally, we need to have the completed library installed for SAVANT to load it. Thus we use –prefix= to indicate where the library should be installed. The actual directory that the library will be installed into is actually prefix/lib. (Ex: --prefix=/work/main library installed into /work/main/lib) This directory can either be in the LD_LIBRARY_PATH environment variable, or we can specify this directory to scram using a command line option, more on this later. With all this taken into consideration we need to issue the following command at the tutorial root directory:

CXXFLAGS=”-Wall -ggdb3” ./configure –with-clutils=<your clutils dir> --prefix=<your installation dir>

Finally we need to build the system. This is done by issuing the following command:

make install

This should build the entire system, and install the plugin library in the appropriate directory.

Step 5 – Execution

The final step is to execute the plugin. Change to the directory you installed scram to. Make sure that the SAVANTROOT environment variable has been correctly set to point to this directory. If the plugin is not located in a directory in the LD_LIBRARY path, it's necessary to pass the location of the plugin directory to scram using the SAVANT_PLUGIN_PATH environment variable to indicate this. Finally, we need to tell scram what the name of the plugin is, and what file(s) to process. Thus, if we want to process the file simple-integer-assign.vhd in the /work/temp directory, we would enter the following command:

./scram –plugins libxmlplugin.la /work/simple-integer-assign.vhd

During execution of scram you should see messages indicating that it is processing the file using the plugin passed and, and then it should generate the XML file, based on the name of the VHDL file passed in. In this case the name simple-integer-assign.vhd should result in a VHDL file named simple-integer-assign.vhd.xml. If there are any undefined symbols in the library, these won't be reported until this stage.

Hints

There are a couple of things to keep in mind when designing your plugin. First, the base class of the entire plugin is going to be named after whatever you designated the plugin to be. For example, since the plugin is named XML, the base class is also called XML, and located in XML.hh & XML.cc. Second, all the AIRE nodes use a common base class to contain lists of nodes, which is then used to derive the specific node lists. In this example, XMLList is the base class for all the node lists, while XMLIdentifierList is derived from it, and contains identifiers.

Most processing of the AIRE tree will involve processing a node (starting with DesignFile) and then processing it's variables. Using these two facts, it's much easier to design generic systems, since the general code can be placed in these classes, and more specific code placed in derived classes, following object oriented design methodologies.

⚠️ **GitHub.com Fallback** ⚠️