Implementing Semantic Actions and ASTs
in a Table-Driven Parser

Representing Semantic Actions in a Program

The next pragmatic challenge is implementing semantic actions in your parser. What are some alternatives? I can think of a few.

In a procedural style, we might implement them as enums, ints, or char to be switched on. For example:

//
// in a helper function called by the parsing algorithm
//
switch (semanticAction)
{
  case MAKE_IDENTIFIER:
    value = lastToken.semanticValue();
    node  = new IdentifierNode(value);
    semanticStack.push(node);
    break;
  case MAKE_ADDITION:
    right = semanticStack.pop();
    left  = semanticStack.pop();
    node  = new AdditionNode(left, right);
    semanticStack.push(node);
    break;
  ...
}

...

//
// in the parsing algorithm
//
applySemanticAction(semanticAction, lastToken, semanticStack);

In a functional style, we might make the semantic action a function pointer or an index into a table of function pointers. Once we have the function pointer, we call the function with the semantic stack as an argument.

Once we have gone that far, we are close to...

In an object-oriented style, we can make our semantic actions objects to which we send a message. This approach uses the command pattern.

public class MakeIdentifier implements SemanticAction
{
  public void updateAST( SemanticStack s ) {
    value = lastToken.semanticValue();
    node  = new IdentifierNode(value);
    s.push(node);
  }
}

public class MakeAddition implements SemanticAction
{
  public void updateAST( SemanticStack s ) {
    right = s.pop();
    left  = s.pop();
    node  = new AdditionNode(left, right);
    s.push(node);
  }
}

...

//
// in the parsing algorithm
//
semanticAction.updateAST(semanticStack);

Channel your inner Woz, and ask questions when you have them.

Representing the Abstract Syntax Tree in a Program

Finally, there is the abstract syntax tree itself. We can represent an AST and its nodes in many different ways. At two ends of a spectrum are:

The choice between data and objects reflects a larger-scale design decision between programming styles that you first encounter in Intermediate Computing. This decision involves a set of trade-offs:

In many large systems, especially ones that model parts of the external world, this trade-off can favor an object-oriented approach. But when implementing language processors, many people find that the balance shifts toward a procedural approach. Why?

It turns out that we are far more likely to know up front all the types of things we need to represent in our AST than we are to know all the kinds of behaviors we might want to implement on the tree. After all, the language grammar specifies the syntax of our language from the start! The grammar may change, but it usually changes infrequently relative to the number of processing tools that we may want to write.

For example, consider code generation. While you are not likely to add a new kind of expression to the abstract syntax of a language, you may well want write a new code generator, either to target a new machine or to experiment with a new technique. That's a modification of behavior, which would require a change to every class in an OO abstract syntax hierarchy.

Whatever representation we choose, it must support downstream processing. The later stages of a compiler must read and modify the abstract syntax of a program in several different ways, some perhaps not anticipated by the compiler writer.

An object-oriented approach to abstract syntax will create a common interface for nodes in the abstract syntax tree and different classes for each kind of node. The AST of a program will be a composite, with compound expressions and statements that group particular configurations of nodes and with simple expressions such as literals and identifiers as "base cases".

ASTs as objects

This configuration of classes is so common in the OO world that it has a name, too: the composite design pattern.

You can see how, in a context such as compiler construction, the negatives of this approach begin to offset its positives. Because we are much more likely to implement new behaviors (analysis tools) han types (kinds of nodes in the tree), implementing compiler behaviors as methods requires that we continually modify and recompile all the files that define abstract syntax nodes.

A pure procedural approach implements behaviors that manipulate an AST as switches on node type, typically with recursive calls to manipulate the components of node. The AST will be some sort of record or structure that groups data but embodies no behavioral knowledge.

ASTs as records

Many compiler writers prefer this approach to the OO approach, even though its negatives also nearly offset its positives. In a statically-typed language, this approach requires frequent up- and down-casting of records in order to operate on them. Type checking in the compiler itself becomes a programmer concern, not a compile- or run-time concern.

Can we achieve the best of both worlds? To do so, we need a way to add behavior for manipulating ASTs without having to modify the ST classes themselves. But we also need a way to expose type information dynamically without casting. There is a way to implement abstract syntax trees in an object-oriented language that balances these competing forces: the visitor design pattern. Feel free to ask me about it if you'd like to learn more, or read about the visitor design pattern on your own.