AF Common Compiler and Parser Guide

From Agent Factory

Jump to: navigation, search

Contents

Introduction

The AF Common Compiler and Parser toolkit provides a generic Java-based implementation of a compiler and a parser that has been used to provide support for the AFAPL, AFAPL2, and ALPHA languages.

These tools have been designed to be simple and make the extension/modification of existing existing language implementations as easy as possible. In the following sections, we focus first on the support provided for implementing parsers, and then move on to discuss how this toolkit supports the implementation of compilers.

To make use of this toolkit, you can download the Compiler and Parser API from Sourceforge. Additionally, you can find Javadoc here.

The Parser API

The Parser API is designed to simplify the construction of parsers. When developing a parser, you must create two basic types of class:

  • You must implement the Scanner interface.
  • You must implement a number of handlers, which are responsible for generating parse trees for tokenised statements returned by your scanner implementation.

How to perform these two tasks is discussed below.

Scanner Implementation

The purpose of the Scanner is to convert source code (given in textual format) into a token based representation of that source code. In our model, source code is decomposable into a list (not necessarily order dependent) of statements that are separated by one or more delimiters. Statements themselves are modeled as lists of tokens (the fundamental units of the programming language).

Basic API

As is illustrated in the snippet of code below, the Scanner interface requires the implementation of three basic methods.

 public interface Scanner {
     public void init(String source, String resource);
     public List<Token> getNextStatement();
     public boolean completed();        
 }

The first of these methods is used to initialize the scanner. It takes the source code as input together with a string that denotes where the source came from (e.g. a local file name, a network address, ...). The last of these methods implements a check to see whether or not the scanner has completed scanning the source code, and the middle method returns a list of tokens representing the next statement in the source code. The Token class represents a single token in the language. It combines the string representation of that token (known as the content of the token), the line number on which the token was found (here called a row), and the resource from which the token was generated (this is the value that is passed in to the init(...) method discussed above). While the particular approach taken to implementing the scanner is left to the developer, the advised approach is to tokenize the source on the fly (i.e. maintain an index into the source code, and only generate the next set of tokens when requested - i.e. when the getNextStatement() method is called).

Example: The AFAPLScanner class

To illustrate how to create a simple scanner, this section will discuss briefly how the AFAPLScanner was implemented. This scanner is used for the core AFAPL language as well as its variants. In particular, it focuses on the getNextStatement() method - it assumes that the source code has been stored in an internal field, called "source", and that index, row and lastRow (this is used to determine the start row of any statement) fields exist that are initialized to 0.

AFAPL supports to forms of statement - basic statements and compound statements. The former type of statement is terminated by a semi-colon, while the latter type of statement is terminated by a close brace. The figure below gives an overview of their syntactic structure.

 // A Basic Statement
 <basic-statement> ::= <statement> ";"
 
 // A Compound Statement
 <compound-statement> ::= <keyword> <identifier> "{" (<parameter> ";")+ "}"

The first step of the getNextStatement() method is to construct an appropriate List object to store the tokenised representation of the statement. Here, we choose to use a java.util.ArrayList object as only a single thread will access the list, and that thread may access it both sequentially and by index.

 public List<Token> getNextStatement() {
   List<Token> statement = new ArrayList<Token>();

Next, because we are going to parse the source code at the character level, we also assume the existence of a field entitled tokenBuffer that is of type StringBuffer. This field is used to contain the token that is currently being constructed. In our getNextStatement() method, we must initialise this field to refer to a new StringBuffer.

   tokenBuffer = new StringBuffer();

Also, because of the format of our compound statement (which contains semi-colons inside the braces), we will need to count braces to make sure that we ignore semi-colons that occur inside the braces. We do this by counting braces using the following variable:

   int braces = 0;

The final preparatory step is to create a boolean value, that is used to flag when the end of a statement is reached.

   boolean found = false;

The main loop of the scanner uses the found flag as a guard. Inside the loop, we check each character individually to see whether it is a delimiter. This is done through a switch statement. In the switch statement, we make use of a createToken(...) method, which creates a token from the current tokenBuffer. While details of this method are provided at the end of this section, informally, the method uses the tokenBuffer and row fields to create a Token object which it adds to the statement parameter. It then resets the tokenBuffer to be a new empty token (so that the new token can be read into an empty buffer).

   while (!found) {
     char ch = source.charAt(index++);
 
     switch (ch) {

In AFAPL, while the semi-colon (outside matched sets of braces) and the close-brace are statement delimiters, we also have a number of additional delimiters that break the statement up into individual tokens. Currently, the full set of delimiters used by AFAPL include: " ,()\n\t{};&" in addition, the string "=>" is also a delimiter.

The first case in the switch that we will consider is the case of a found semi-colon. If we find a semi-colon, then we have two possible scenarios - it is either the end of a basic statement, or the end of a parameter within a compound statement. Prior to checking the scenario, we create a token from the existing tokenBuffer. We differentiate between the two scenarios by checking the current brace count. In the former case, the found flag is set to true, causing the loop to terminate. In the latter case, a second token is created (for the semi-colon) by adding the character to the new tokenBuffer and then re-invoking the createToken(...) method.

       case ';':
         createToken(statement);
         if (braces == 0) {
           found = true;
         } else {
           tokenBuffer.append(ch);
           createToken(statement);
         }
         break;

The second case to consider is the case where a close brace is found. In this case, we create a token from the existing tokenBuffer, and then create a second token to hold the close brace. Strictly speaking, this is not necessary, however, the close-brace is included here for completeness. Finally, the found flag is set to true.

       case '}':
         createToken(statement);
         tokenBuffer.append(ch);
         createToken(statement);
         found = true;
         break;

Next, we consider those delimiters that should be used to generate tokens within a statement, but which are also part of the syntax of those statements. Examples of this include the open-brace; commas and brackets (these latter three delimiters are used within first-order structure expressions); and the ampersand (used as part of belief sentences to represent a logical and). In all of these cases, we must create a token, and then add the delimiter as a second token (much the same as was done of the close brace).

       case '{':
         braces++;
       case ',':
       case '(':
       case ')':
       case '&':
         createToken(statement);
         tokenBuffer.append(ch);
         createToken(statement);
         break;

Examples of AFAPL code where the above delimiters are used include the first-order structure: isa(man, happy). This code fragment should be converted into a partial token list of the form: ['isa', '(', 'man', ',', 'happy', ')'].

The fourth set of delimiters are those that generate tokens within a statement, but which are not part of the syntax of those statements. These include spaces, tabs, and new line characters. In this case, we just create the relevant token. The only exception to this is that the new line token should cause the row count to be increased by one - this needs to be treated as a special case.

       case '\n':
         createToken(statement);
         row++;
         break;
 
       case ' ':
       case '\t':
         createToken(statement);
         break;

The final delimiter is the string "=>" which represents a logical implication. This string is caught by the following snippet of code which performs a "look ahead" to see whether the next character is the second part of the implication symbol.

       case'=':
         if (source.charAt(index) == '>') {
           createToken(statement);
           buf.append("=>");
           index++;
           createToken(statement);
         } else {
           buf.append(ch);
         }
         break;

Finally, the default behaviour (for characters that are not delimiters) is to add the character to the curretn tokenBuffer.

       default:
         tokenBuffer.append(ch);
     }

After the switch statement has been called, a final step before terminating the loop is to check whether or not we have reached the end of the source code. If the end of the code is found, and the tokenBuffer is not empty, then the source code is considered incomplete and a warning is generated.

     if (index == source.length()) {
       if (tokenBuffer.length() > 0) {
         System.out.println("WARNING: Unexpected End Of File Reached.");
         System.out.println("\tFound: " + tokenBuffer.toString());
         System.exit(0);
       }
       found = true;
     }
   }

NOTE: Later versions of the scanner will terminate more gracefully.

Finally, our getNextStatement() method returns the current statement:

   return statement;
 }

For completeness, we include the createToken(...) method, which is implemented as follows:

 private void createToken(List<Token> tokens) {
   String token = buf.toString().trim();
   if (token.length() > 0) {
     tokens.add(new Token(token, lastRow, filename));
   }
       
   lastRow = row;
       
   tokenBuffer = new StringBuffer();
 }

Using the AFAPLScanner class

To use this class, we implement a basic loop as follows:

 // ...
 AFAPLScanner scanner = new AFAPLScanner(sourceCode, resource);
 while (!scanner.completed()) {
   List<Token> statement = scanner.getNextStatement();
   // Process the statement...
 }
 // ...

While it is possible to use the scanner directly in this way, this API provides a second class, called Parser, that uses a scanner implementation, together with a specified set of Handler classes (see below) which parse individual statements.

Handler Implementation

Handlers are user defined classes that implement the functionality necessary support a particular type of statement in the source language. More specifically, handlers have the following responsibilities:

  • To generate a parse tree for a given statement (only if that statement is relevant to the given handler).
  • To handle all visitors to the root node of the parse tree that is generated by the former step.

This is achieved through the definition of an abstract base class, which is defined below. The use of this abstract based class is then illustrated through a worked example taken from the AFAPL language.

Basic API

Handlers are implemented by extending the Handler abstract base class. The source for this class is given below. As can be seen, it implements three basic methods, only one of which (the generateParseTree(...) method) must be implemented by the concrete subclasses.

 public abstract class Handler {
   public abstract Node generateParseTree(List<Token> statement) throws SyntaxError;
   public void accept(Node node, Visitor visitor) throws SemanticError {
     Class[] paramClasses = {Node.class, visitor.getClass()};
     Object[] paramValues = {node, visitor};
     Method method = null;
     try {
       method = getClass().getMethod("accept", paramClasses );
       method.invoke(this, paramValues);
     } catch (NoSuchMethodException nsme) {
       for (Node child : node.getChildren()) {
           visitor.visit(child)
       }
     } catch (IllegalAccessException iae) {
       for (Node child : node.getChildren()) {
           visitor.visit(child);
       }
     } catch (InvocationTargetException ite) {
       throw (SemanticError) ite.getTargetException();
     }
   }
   
   public String toString(Node node) {
     return node.getToken().getContent();
   }

}

Perhaps the key method in this base class is the accept(...) method. This method is invoked whenever a visitor object attempts to traverse the associated node (which normally is generated by the handler in the generateParseTree(...) method). It basically uses reflection in an effort to find an implementation of the accept(...) method that is specifically for the type of Visitor object that it has received. If no such method is defined, it reverts to its default behaviour, which is to traverse the subtree associated with the specified node. In the event that a InvocationTargetException is thrown (which happens when a suitable accept(...) method is found, but throws an exception), the method unwraps the corresponding SemanticError and throws this instead.

Example: The FOSHandler class

The FOSHandler is responsible for handling first-order structures of the form:

  • Constants: constant
  • Variables: ?variable
  • Composites: functor(arg1, arg2, ...,argN)

In this example implementation, we do not discern between a constant and a variable. Also, arguments must be either constants or variables. Before launching into a discussion of the code underlying this handler, we first need to understand how this handler will be used. Basically, first-order structures are a core component of the AFAPL language, and are used in all of the valid statements of the language (i.e. they are not valid statements in themselves). This means that, we will only use this handler from within other handlers. As a result, we can assume that, the statement parameter should be a first-order structure. Thus, if proves not to be a valid first-order structure, we must throw a SyntaxError exception in response.

So, lets get on with illustrating how to create a handler by declaring the FOSHandler class:

 public class FOSHandler extends Handler {

In this class, we must implement the generateParseTree(...) method:

   public Node generateParseTree(List<Token> statement) throws SyntaxError {

This method is responsible for checking whether or not the statement is a valid first-order structure, and if it is, must generate a corresponding parse tree. In order to achieve this, it must perform a number of checks.

The first check to be performed is to check whether or not the statement is a constant or a variable. To do this, we simply check the size of the statement list - if its size is one, then it could be a constant or variable, otherwise, it may be a composite.

     if (statement.size() == 1) {
       return new Node(statement.get(0), this);
     }

If necessary, we could expand this check to validate the form of the text associated with the token (e.g. check whether the constant/variable has a valid identifier), but of the purposes of this example, we do not.

Now, we know that the statement is either a composite, or not a valid first-order structure. So, we can make some basic assumptions about what we expect the tokens in the statement list to look like. Basically, a composite first-order structure will have a token list that looks something like this: ['functor', '(', ..., ')'] What appears inside the brackets can vary, however the basic structure is constant.

Before checking whether or not the statement is structurally correct, we can do a basic range check to see that we have got something that is valid (i.e. the statement must contain at least four tokens - the three specified above and one argument).

     if (statement.size() < 4) {
       throw new SyntaxError("Malformed Logic Statement",
                             "Expected a composite first-order Structure, but got: " + Utilities.context(statement), 
                             statement.get(0));
     }

Now, we know that the statement is long enough to hold a valid composite, so lets check if the syntax is correct - we expect our statement to have an open bracket token at index 1 and a close bracket token at index statement.size()-1. The next step is to enforce this:

     if (!statement.get(1).contentEquals("(")) {
       throw new SyntaxError("Malformed Logic Statement",
                             "Missing ( near " + statement.get(0).getContent(), 
                             statement.get(0));
     } else if (!statement.get(statement.size()-1).contentEquals(")")) {
       throw new SyntaxError("Malformed Logic Statement",
                             "Expected ) near: " + Utilities.context(statement),
                             statement.get(0));
     }

Now, the final thing we need to know, is: are the arguments correctly specified. To check this, we create our root node for the parse tree, storing the functor inside it:

     Node node = new Node(statement.get(0), this);

Next, we can loop through the remaining positions in the statement and check that they have the format arg, arg, arg, arg)

     int index = 2;
     boolean valid = true;
     while (valid & (index < statement.size()-1)) {
       Node child = new Node(statement.get(index++), this);
       node.addChild(child);
       valid = statement.get(index++).contentEquals(",");
     }
   
     if (!statement.get(index-1).contentEquals(")") {
       throw new SyntaxError("Malformed Logic Statement",
                             "Incorrectly formed argument list near: " + Utilities.context(statement),
                             statement.get(0));
   
     return node;
   }
 }

Again, in the ideal solution, a name validity check should be carried out prior to the creation of the child, however, this is omitted for the purposes of this example.

Finally, we now have a node object that is the root of a parse tree that represents a valid first-order structure. This node will itself become a subtree of a larger parse tree that corresponds to an individual statement within the AFAPL language.

NOTE: The example given here is, in fact, a simplification of the actual FOSHandler that is used by the AFAPL Compiler. In particular, AFAPL allows the arguments to be composite objects (which themselves are represented as first-order structures) e.g. isa(man(rem), happy) represents the fact that the man object is happy, and that the man object itself has one parameter - a name.

Using the FOSHandler

The FOSHandler class supports the generation of parse trees that represent first-order structures. However, as was discussed above, this type of structure is not a valid statement in the AFAPL language. Instead, it should be used by other handlers that do represent valid statements in the source language. For illustrative purposes, this guide will introduce a second handler, known as a BeliefHandler, which handles the BELIEF statement. This statement is an example of a basic statement, and takes the following format:

BELIEF(isa(rem, man))

The code for this second handler is given below:

 public class BeliefHandler extends Handler {
   public Node generateParseTree(List<Token> statement) throws SyntaxError {
     // Check if the first statement is a BELIEF token.  If it isn't then
     // this handler is not responsible for the statement, and should return
     // null to indicate this.
     if (!statement.get(0).contentEquals("BELIEF")) {
       return null;
     }
       
     // Okay, so we have a valid BELIEF statement
     Node node = new Node(statement.get(0), this);
       
     // Check the brackets
     if (!statement.get(1).contentEquals("(")) {
       throw new SyntaxError("Malformed Logic Error",
                             "Missing ( near " + statement.get(0).getContent(), 
                             statement.get(0));
     } else if (!statement.get(statement.size()-1).contentEquals(")")) {
       throw new SyntaxError("Malformed Logic Error",
                             "Expected ) near: " + Utilities.context(statement),
                             statement.get(0));
     }
     
     // Now extract the tokens that represent the inner content of the belief
     // statement and try to generate a valid first-order structure.
     List<Token> fosStatement = statement.subList(2, statement.size()-1);
     node.addChild(new FOSHandler().generateParseTree(fosStatement));
     
     // The FOSHandler generateParseTree(...) method did not generate an
     // exception, so the inner content must be a valid first-order structure...
     return node;
   }
 }

This code should be used to generate a valid BELIEF statement, which is a valid statement in the AFAPL Language.

To use this, we can simply expand the parsing code that we used earlier to include a list of Nodes (that will hold the root nodes of each statement in the source code).

 // ...
 BeliefHandler handler = new BeliefHandler();
 List<Node> program = new ArrayList<Node>();
 
 AFAPLScanner scanner = new AFAPLScanner(sourceCode, resource);
 try {
   while (!scanner.completed()) {
     List<Token> statement = scanner.getNextStatement();
   
     // Process the statement...
     Node node = handler.generateParseTree(statement);
     if (node != null) {
       program.add(node);
     } else {
       throw new SyntaxError("Parse Error",
                             "Unexpected Statement: " + Utilities.context(statement),
                             statement.get(0));
     }
   }
 } catch (SyntaxError se) {
   System.out.println(se.getMessage);
 }
 // ...

The key points to note in the above code are as follows:

  • We loop through the program checking each statement until we have either found a statement we don't know how to handle or we have handled all the statements in the program.
    • In the event that a statement is not understood, an appropriate SyntaxError exception is thrown
    • This syntax error, or any other syntax error that is thrown by one other handlers is caught by the outer try...catch, which prints out the error and terminates the parse process.
  • Handlers produce partial parse trees for statements which are then stored in a list of parse trees - one entry for each statement in the program.
  • We have to explicitly declare each handler we want to use and to call it from within the parsing loop.

The above parsing code is intended only to be used to illustrate how the parser works. The actual parsing algorithm is provided as part of the core API via the Parser class. This class implements a more generalised parsing algorithm that is easily configurable, and which is described in more detail below.

The Parser class

Support for parsing source code is provided in the form of the Parser class. This class implements a version of the core parsing algorithm outlined above, with the exception that it is designed to be configurable via an external configuration file.

The parser class itself includes two constructors:

  • Parser(Scanner scanner): constructor that takes a Scanner as a parameter - used to create parsers that will parse the source code associated with the scanner.
  • Parser(Scanner scanner, List<Handler> handlers): constructor that takes a Scanner and a list of handlers as parameters. This constructor can be used to programmatically initialize a Parser.

It also provides two methods:

  • parse(): implements the core parsing algorithm - returns true if the parsing was successful, false otherwise.
  • getProgram(): returns a list of Nodes that represents the parse tree for the program.

The class should be used as follows:

 // ...
 AFAPLScanner scanner = new AFAPLScanner(source, resource);
 Parser parser = new Parser(scanner);
 if (parser.parse()) {
   // Successful parsing took place
   List<Node> program = parser.getProgram();
 
   // Do something with the parse tree
 } else {
   // Whoops - the parser failed
 }
 // ...

When declaring the parser, you can use either constructor. However, the example above uses the constructor that loads the set of handlers from an external configuration file. Specifically, this constructor makes use of the Configuration class. This class implements a singleton pattern, and can be used to load a compiler configuration from a specified configuration file. To load a configuration, you simply use Configuration.getInstance(...) where the parameter passed into the method is a string that defines the location of the configuration file in the classpath. This method need only be used once (the first time). All subsequent accesses of the configuration can be made via the Configuration.getInstance() method.

A more detailed description of the Configuration class will be provided later. For the purposes of this example, we will explain only how to create a configuration file that loads a set of statement level handlers.

The syntax of the configuration file (for statement level handlers) is as follows:

 [statements]
 example.BeliefHandler
 example.OtherStatementHandler
 ...

If this configuration was stored in the file "eg.lang" which was located in the "example" package, then the following line could be used to load the configuration for the parser:

 Configuration.getInstance("example/eg.lang");

So long as this is done prior to the invocation of the parser, the parser will be configured to use the handlers specified in the above file.

The Compiler API

In addition to the support provided for parsing source code, Agent Factory also includes support for the creation of compilers for code. This support is realised through the Compiler API, which builds upon the core parsing algorithm described above to introduced above. Specifically, the Compiler API introduces three new concepts / classes that can be used to support the creation of compilers:

  • Visitors: These are classes that are used to traverse the parse trees that are generated by the parser. While visiting a given node in the tree, the visitor is able to perform a specified activity, such as: activity uniqueness checks, ontological correctness of inner content language terms, the ability of the agent to perform the activities it can commit to, ...
  • PreProcessors: This set of classes is used to perform a number of steps prior to the parsing step. Example usages of pre-processors include stripping comments out of the source code, importing specified files, ...
  • Flags: These classes provide a mechanism that enables the customisation of the compiler from the command line (or whatever application utilises the compiler).

We explore each of these classes in more detail in the following sections.

Visitor Implementation

Visitor classes should be implemented whenever you wish to traverse the parse trees that are generated by the compiler. In this section, we explore how to implement a simple Visitor, known as a PrintVisitor. This visitor traverses the parse tree, creating a string representation of the source code that is then printed out at the end of the traversal.

The Visitor API

The Visitor API is implemented as an abstract base class that includes a number of methods that must be implemented in concrete subclasses together with a two concrete methods.

The first method allows a session object to be passed to the visitor. This is a shared object that can be accessed by each of the three core Compiler API components. Individual components can store and retrieve information in this shared session as is necessary. A key issue in the use of the session is the ordering of similar types of component as incorrect ordering can result in components attempting to access shared data before it has been placed into the session.

The second method is a standard method that is part of the Visitor pattern, on which this component is based. Basically, whenever a visitor traverses a tree, it starts by visiting the root node of that tree. The accept() method is then invoked on the node, passing the visitor to it. In our implementation, the node itself delegates acceptance of the visitor to the associated handler. This is discussed later in this guide.

 public abstract class Visitor {
   protected AFSession session;
   
   public void setSession(AFSession session) {
       this.session = session;
   }
   
   public void visit(Node node) throws SemanticError {
     node.accept(this);
   }
   
   public abstract void reset();
   public abstract void endOfStatement();    
   public abstract void endOfVisit();
 }

The first of these methods is called immediately before the Visitor commences its traversal of the program parse tree. The second method is called after the visitor completes its traversal of the parse tree for a given statement, and finally, the third method is called after the visitor has visited all of the parse trees for the program.

Finally, it must be noted that implementing a visitor involves two core steps:

  1. Creation of the relevant visitor class.
  2. Modification of relevant handlers to handle the arrival of the visitor to the corresponding node.

The default behaviour attributed to a visitor when visiting a node is to visit all of the children of that node.

Example: Implementing the PrintVisitor

The PrintVisitor visitor is a simple example visitor that can be used to visit all the nodes in a given parse tree and generate a string representation of those nodes. To implement this class, we extend the Visitor base class and introduce a single StringBuffer field called buffer:

 public class PrintVisitor extends Visitor {
   private StringBuffer buffer;

Next, we implement the three traversal methods required by the Visitor class:

   public void reset() {
     buffer = new StringBuffer();
   }
   
   public void endOfStatement() {
     buffer.append("\n");
   }
   
   public void endOfVisit() {
     System.out.println(buffer.toString());
   }

Finally, we implement a custom append(...) method, that appends a String to the buffer:

   public void append(String text) {
     buffer.append(text);
   }

}

This class can be invoked upon any parse tree simply by creating an instance of the visitor, invoking the reset() method, and then invoking the visit(...) method, passing the root of the parse tree as the argument. However, things stand, nothing will happen, as we have not defined how to handle this kind of visitor. This is done by modifying the corresponding handler class (which we describe next).

NOTE: This approach is somewhat different to the more traditional approach to implementing the visitor pattern. In the more normal approach, the logic for both traversing the tree and extracting information from a node is implemented as part of the visit() method. However, previous versions of the Compiler API, which make use of this design, have proven to be less extensible as logic relating to a given statement is not localised within a single class.

Example: Modifying the BeliefHandler

Once a visitor has been implemented, we must modify the relevant handlers to include a method that handles the arrival of the visitor at the associated node. Specifically, we must implement an accept(...) method that takes a Node and a PrintVisitor as parameters. The underlying accept(...) method (defined as part of the abstract Handler class) will then automatically detect the existence of this method and invoke it whenever it is asked to handle a visit by a PrintVisitor.

To illustrate this, we will implement such an accept(...) method for the BeliefHandler class. The updated class is given below:

 public class BeliefHandler extends Handler {
   public Node generateParseTree(List<Token> statement) throws SyntaxError {
     // Check if the first statement is a BELIEF token.  If it isn't then
     // this handler is not responsible for the statement, and should return
     // null to indicate this.
     if (!statement.get(0).contentEquals("BELIEF")) {
       return null;
     }
       
     // Okay, so we have a valid BELIEF statement
     Node node = new Node(statement.get(0), this);
       
     // Check the brackets
     if (!statement.get(1).contentEquals("(")) {
       throw new SyntaxError("Malformed Logic Error",
                             "Missing ( near " + statement.get(0).getContent(), 
                             statement.get(0));
     } else if (!statement.get(statement.size()-1).contentEquals(")")) {
       throw new SyntaxError("Malformed Logic Error",
                             "Expected ) near: " + Utilities.context(statement),
                             statement.get(0));
     }
     
     // Now extract the tokens that represent the inner content of the belief
     // statement and try to generate a valid first-order structure.
     List<Token> fosStatement = statement.subList(2, statement.size()-1);
     node.addChild(new FOSHandler().generateParseTree(fosStatement));
     
     // The FOSHandler generateParseTree(...) method did not generate an
     // exception, so the inner content must be a valid first-order structure...
     return node;
   }
 
   public void accept(Node node, PrintVisitor visitor) throws SemanticError {
       visitor.append("BELIEF(");
       visitor.visit(node.childAt(0));
       visitor.append(")");
   }
 }

If invoked as is on a BELIEF statement, the PrintVisitor would produce output of the form: "BELIEF()". This is because the BeliefHandler uses the FOSHandler to generate the parse tree for the inner content language. As a result, we must also modify the FOSHandler class to handle the visit of the PrintVisitor:

 public class FOSHandler extends Handler {
   public Node generateParseTree(List<Token> statement) throws SyntaxError {
     // Detailed implementation provided earlier in this guide...
     return node;
   }
   
   /**
    * This additional method must be implemented to ensure that the PrintVisitor
    * will generate the correct output.
    */
   public void accept(Node node, PrintVisitor visitor) throws SemanticError {
     visitor.append( node.getToken().getContent() );
     if (node.hasChildren()) {
         visitor.append("(");
         boolean first = true;
         for (Node child : node.getChildren()) {
             if (first) first = false;
             else visitor.append(",");
             child.accept(visitor);
         }
         visitor.append(")");
     }
   }
 }

One of the advantages of this approach is that not all visitors need to do something at every node (for example, an ActivityVisitor may only need to do something at nodes relating to activities). In such cases, we need only to specify the associated accept(...) methods on the relevant handlers, which has the effect of simplifying the coding process.

Using the PrintVisitor

There are two ways that we can apply the print visitor to the parse tree of a program. The first way is to specify the visitor in the language configuration (but we will discuss this later in the guide). The second way is to execute the visitor programmatically, and requires that we extend the fragment of program code we developed in the section on "The Parser Class".

 // ...
 AFAPLScanner scanner = new AFAPLScanner(source, resource);
 Parser parser = new Parser(scanner);
 if (parser.parse()) {
   // Successful parsing took place
   List<Node> program = parser.getProgram();

   // Do something with the parse tree
   PrintVisitor visitor = new PrintVisitor();
   visitor.reset();
   for (Node node : program) {
     visitor.visit(node);
     visitor.endOfStatement();
   }
   visitor.endOfVisit();
 } else {
   // Whoops - the parser failed
 }
 // ...

In the above code, we see that the PrintVisitor object is first created, and then reset(). Following this, we loop through the list of nodes returned by the parser, and the visitor visits each node in turn. After visiting a given node, the visitor invokes the endOfStatement() method, and finally after all nodes have been visited, it invokes the endOfVisit() method. Multiple visitors may be applied to a given program by generating a list of Visitor objects and then looping through that list, applying each visitor to the list of node objects in turn. This more general approach is applied by the configurable compiler, and looks something like the snippet of code below:

 // ...
 List<Visitor> visitors = ...;
 AFAPLScanner scanner = new AFAPLScanner(source, resource);
 Parser parser = new Parser(scanner);
 if (parser.parse()) {
   // Successful parsing took place
   List<Node> program = parser.getProgram();

   // Do something with the parse tree
   for (Visitor visitor : visitors) {
     visitor.reset();
     for (Node node : program) {
       visitor.visit(node);
       visitor.endOfStatement();
     }
     visitor.endOfVisit();
   }
 } else {
   // Whoops - the parser failed
 }
 // ...

PreProcessor Implementations

TBD

Flag Implementations

TBD

Specifying Compiler Configurations

TBD

Providing Command Line Support for Compilation

TBD