Parse Trees

In this assignment you will develop MIPS code equivalent to the Java code described under the "Classes" menu item. This program will illustrate how object-oriented language compilers generate code that supports the three cornerstones of the object-oriented paradigm: encapsulation, inheritance, and polymorphism.

The main function is in the ParseNode class. It constructs a parse tree and prints out values for some of its subtrees. The parse tree has various kinds of nodes. Each node implements a method that defines the value for the subtree rooted at the node. The simplest class of nodes is the Constant class. Nodes of this class have constant values. The other node classes have left and right subtrees. Their values are computed by adding, subtracting, multiplying, or dividing the values of their subtrees.

Do not be intimidated by the amount of Java code. The code development is broken down into five steps. The first is the hardest, but it is not too difficult. The second step is easier. Code writing in the last three steps only involves cutting, pasting, and minor editing. If you are careful and devote a few minutes to checking over your work, you should not need to spend much time on the last four steps.

Each of the classes should be implemented in its own file. This eliminates naming conflicts that would arise if the code were all in one file.

Development

To simplify your development efforts I am giving you some of the code in the zip file parse-trees.zip. Start by downloading this file and unzipping it to a convenient location.

Your development should proceed in 5 incremental steps. Each step involves adding some code into the ParseNode class file and completing the implementation of the value() method in one of the subclass files.

  1. parse tree with a single Constant node
    • complete printValue() in ParseNode.asm
    • complete value() in Constant.asm
  2. add Constant and Sum nodes
    • add code to main() in ParseNode.asm
    • complete value() in Sum.asm
  3. add Constant and Difference nodes
    • add code to main() in ParseNode.asm
    • complete value() in Difference.asm
  4. add Constant and Product nodes
    • add code to main() in ParseNode.asm
    • complete value() in Product.asm
  5. add Constant and Quotient nodes
    • add code to main() in ParseNode.asm
    • complete value() in Quotient.asm

Implementing Static Methods

In the first step of implementing the code, you will be implementing the printValue() method. In the remaining steps you will be adding code to the main() method. Both of these methods are declared as static in the ParseNode class. This means that messages using these methods are sent to classes rather than class instances (normal objects).

Names of classes in Java are globally defined. Consequently, you can implement these methods like ordinary C or C++ functions, using the method name as the label for the first assembly language instruction. You should return from all methods using jr $ra as usual. The main() method should be coded as a typical assembly language main program using the label main.

The code for the printValue() method involves sending a value() message to an instance of one of the ParseNode subclasses. This is explained later in this web page in the section "Implementing and Invoking Instance Methods". Do not attempt any shortcuts here - you may get unpredictable, hard to debug results.

Invoking Static Methods

Static methods are invoked like ordinary C or C++ functions, using the jal instruction with the method label as an operand.

Implementing Constructors

You will need to implement constructors in all of the steps. Like static methods, constructors are actually performed by classes rather than class instances. Thus they are invoked using the jal instruction with the method label as an operand. The name is the same as the class name.

The primary task of constructor code is creating and initializing an instance structure for a new instance of the class. An instance structure should be created using the sbrk system call. This call returns the address of a newly allocated chunk of memory. Although a Java constructor does not explicitly declare a return type, it does return a value: the same address that is returned by sbrk.

Part of the code for initialization is a translation of the Java constructor code. It differs from ordinary assembly language code in that you are assigning values to fields in the newly allocated memory chunk instead of labeled memory locations. To do this, you use the sw instruction with base-displacement addressing. The base register is the register that contains the return value from the sbrk system call. The implementation comments in the Java code suggest displacements you should use.

Part of initialization is not explicit in the Java code. This part of the code provides access to instance methods. For this assignment you should use a simple form of access: just put the address of the method directly into the instance structure.

Each of your instance structures should include the address of the value() method. You can get the address of the method into a register with the la pseudoinstruction, using the method label as an operand. Then use the sw instruction to save the address into the instance structure.

Invoking Constructors

Constructors also invoked like ordinary C or C++ functions, using the jal instruction with the method label as an operand.

Implementing Instance Methods

You will need to implement one instance method in each of the steps. It will be the value method of the ParseNode subclass that is implemented in the step. Since Mars supports separate assembly, you should implement these methods using the same label in different files for each ParseNode subclass.

An instance method always has an implicit first parameter, which is the address of the instance structure of the receiver of a message. The instance structure gives you access to the instance variables and methods of the receiver.

The assembly language code for a method is just a translation of the Java code. When you need to access instance variables, use lw and sw instructions with base-displacement addressing as described in the "Implementing and Invoking Constructors" section.

Take care when implementing the instance methods. Since they can be invoked from within another method, you will always need to save and restore $ra. Starting with Step 2, the value() method for a node sends a value() message to the children of the node. You may need to save and restore register values across these calls.

Invoking Instance Methods

To invoke an instance method, you need to set up parameters and the jump to the appropriate method code. To get polymorphic behavior, you need to load the method address from the object structure into a register. This ensures that each object uses its own implementation.

The method address is loaded using base-displacement addressing just like accessing instance variables. After the method address is loaded into a register use the jalr (jump and link register) instruction, specifying that register as an operand.

What to Turn in

Turn in both a copy of your program and a Mars session record. In this session, you will not need any input.

Your TA may also ask you to demonstrate your program in lab.

References

Important sections from the following references are linked in through the "References" submenu.