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.
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.
printValue()
in ParseNode.asm
value()
in Constant.asm
main()
in ParseNode.asm
value()
in Sum.asm
main()
in ParseNode.asm
value()
in Difference.asm
main()
in ParseNode.asm
value()
in Product.asm
main()
in ParseNode.asm
value()
in Quotient.asm
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.
Static methods are invoked like ordinary C or C++ functions, using the
jal
instruction with the method label as an operand.
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.
Constructors also invoked like ordinary C or C++ functions, using the
jal
instruction with the method label as an operand.
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.
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.
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.
Important sections from the following references are linked in through the "References" submenu.