|
CS 374 Compilers
Homework #8 |
Due: Thursday 4/26 at the beginning of classNote: This work is to be done in assigned groups. Each group will submit one assignment. Although
you may divide the work, both team members should be able to present/describe
their partner's work upon request.
Code Generation - Part III
At this point, we can generate code that prints compound arithmetic and
logical expressions, create new objects, make simple method calls (without
dynamic method lookup), and return results. Now we extend our
implementation to reflect the way Java performs dynamic method lookup.
0. Preparation: Read section 14.2 on static versus dynamic method lookup.
In our previous implementation, a new object has a class identifier (e.g. 0, 1,
2, 3). We will be replacing these with the address of a lookup table for
method addresses, much like those shown in Figure 14.3.
1. Symbol Table Addition: You'll want to add to your symbol table
such that it will be easy to:
- For each class, compute the position of each method in the method lookup
table. For top level classes, one needs merely to keep the methods in a
list. For subclasses, overriding methods keep the same position
as the method that is overridden, and new methods are given higher
indices.
2. Allocating Tables:
- In the ".data" section of your assembly, create a special internal
variable for tables by emitting a label (e.g. "_dml_tables") for the address
of the list of dynamic method lookup tables. In the line before, use the
".align" directive to make sure that the variable is on a word boundary. In the next line, use the
".word" directive to set it to 0 initially.
- After setting up the stack frame in your main method, emit code that does
the following:
Dynamically allocate c words of memory (using the sbrk syscall) and
store the address to your tables variable, where c is the number of
classes.
In this table, for each class, dynamically allocate m words of
memory and store the address in the main table, where m is the number of
methods for each class. That is, class i should have an address at the
ith word of the table which points to another table with a size in words
equal to the number of methods in class i.
In each class table, load the addresses of the appropriate address labels
for each method call. Your symbol table additions should help you compute
these labels.
3. NewObject: Change new object such that the class identifier is
replaced by the address of its dynamic method lookup table.
4. Call: Change your call implementation such that it uses the objects
dynamic lookup table to retrieve the correct method address and jump to this
address. That is, it loads the table address from offset 0 of the object.
Then it loads the method address from the appropriate offset in the table.
Finally, it jumps unconditionally to that address, placing the next instruction
in $ra. Note: there seems to be a discrepancy between the documentation
and the simulator. If you have your jump address in $v0, use "jalr $ra,
$v0".
Now retest your method calls/returns with NewObject.java, Factorial.java, and
DynamicMethodLookup.java.
5. While:
- Emit a numbered label (e.g. "whileTest42").
- Generate the test expression code.
- On a result of 0, branch to the numbered ending label
- Generate the statement code.
- Branch unconditionally to the first label.
- Emit a numbered ending label (e.g. "whileDone42").
Now you can test your code with BinaryTree.java and LinkedList.java
6. NewArray: Allocating a new array is similar to
allocating an object. Allocate an extra word at the beginning which holds
the length of the array.
7. ArrayLength: Generate code for the integer array
expression. Then load the length into $v0 from the first word of the array.
8. ArrayLookup:
-
Generate code for the integer array expression.
-
Push the array address from $v0 to the stack.
-
Generate code for the array index expression.
-
Pop the array address to $v1 from the stack.
-
Compute the address of the array integer and load it into $v0
9. ArrayAssign:
-
Generate code for the array identifier.
-
Push the array address from $v0 to the stack.
-
Generate code for the array index expression.
-
Pop the array address to $v1 from the stack.
-
Compute the address of the array + offset and push the address
onto the stack.
-
Generate code for the assignment expression.
-
Pop the offset array address from the stack to $v1.
-
Store the assignment expression result from $v0 to the address
in $v1.
Now you can test your code with the remainder of the test cases:
SimpleArray.java, BinarySearch.java, BubbleSort.java, LinearSearch.java,
QuickSort.java, and TreeVisitor.java.
Congratulations! You have completed your MiniJava compiler!
- Take a moment to look over the generated assembly code and marvel that
it works! A compiler is one of the most complex pieces of software
that an undergraduate implements.
- Review the stages that make up the project to get the big picture.
As you were going through the project, you couldn't have the perspective
you have now. Take advantage of the project being fresh in your mind and
consider the overall process: lexical analysis, parsing, abstract syntax tree
building, symbol table building, type-checking, code generation.
- Consider the valuable skills you've gained and resolve to improve them.
Although few people write compilers...
- ... many more write interpreters. Interpreters are common in
any application that allows user to extend its capabilities, adding value to
the product with little cost to the software company. Rather than
generating code, one can always write an interpreter.
- ... even more write parsers. Consider the amount of
structured information that flows through networks and between processes.
The front end compiler tools you've learned are the best way to write
correct, robust, bulletproof code for the common task of interpreting a stream
of information.
- Reflect on the value of recursion and design patterns in this task.
There are many more design patterns that are useful for different
problem solving tasks. Watch for them!