|CS 374 Compilers
Note: 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.
Up to this point, we have created an abstract syntax tree of a legal program that can be compiled. We have also studied the MIPS assembly language, and simple use of a stack frame to implement recursive procedures. We now turn our attention to the beginning of an incremental development of the MIPS code generator.
For simplicity, we're bypassing intermediate code generation, and directly generating MIPS assembly code. For the MiniJava language, this can be accomplished through the implementation of a single visitor with slight modification to your symbol table class.
0. Preparation: Review Appendix A.6 and read chapter 6. Download the following starter files.
1. Visitor Creation: Create a class MIPSTranslator in the visitor package which extends DepthFirstVisitor and is constructed with your symbol table and (optionally) a java.io.PrintStream object. If not supplied, the default PrintStream will be System.out. Import the syntaxtree classes and java.util.*. Create methods (e.g. emit, emitLabel) for generating formatted assembly code. You'll use these a lot, so keep the names short. Labels are non-indented and followed by a colon. Other emitted code should be tab-indented. Finally, include all of the DepthFirstVisitor methods to serve as starting points for your implementation. (You will not need to override all of DepthFirstVisitor's methods.)
2. Main Class: Emit preamble assembly code to (1) define an ASCII string which contains a newline, (2) labels the start of the main method code, (3) sets up the stack frame, (4) recursively visits appropriate subtree(s) to generate main method code, (5) pops the stack frame, and (6) performs the exit syscall.
3. IntegerLiteral: For all expressions, assume that the result of the expression will always be placed in register $v0. Thus the code generated from IntegerLiteral will load the given immediate value into $v0.
4. Print: Assume that the expression to be printed is of type integer. (Add such a restriction in your type-checking phase.) Generate the expression code. From the previous assumptions, the integer value of this expression is in $v0 after this code's execution. Print this integer and follow it with a newline. You can now test the correct compilation of Print0.java.
5. Using the Stack Pointer: To simplify our implementation, we will be making minimal use of registers, relying on our stack to store all locals and temporaries. There will be many times when you will want to store the value of one expression (i.e. a temporary) while evaluating another. Since such evaluation is recursive, we cannot simply move the result to another register. Our compiled code will operate within each method as a stack machine, storing results on the call stack until they are needed (much like our use of the stack in building the abstract syntax tree). We'll have more stores/loads to/from the stack, but we'll avoid the need for register allocation in our simplified compiler.
First, create a method pushV0() that emits code to (1) decrease the stack pointer $sp by one word, and (2) store the value of $v0 to the that position of the stack. Next, create a method popV1() that emits code to (1) load the value at the $sp address to register $v1, and (2) increase the stack pointer by one word.
6. Plus: Now you can implement code generation for Plus. Emit code to (1) compute the left expression (which leaves its value in $v0), (2) push $v0, (3) compute the right expression, (4) pop the stack to $v1, (5) put $v0 + $v1 into $v0. Test your code with Print1Plus2.java. Then create and test a file PrintPlusses.java which features multiple additions and parentheses to change the order of evaluation.
7. Minus and Times: Develop and test Minus and Times similarly, using test files Print3Minus2.java and Print6Times7.java, and developing your own test cases. Also test with PrintMath.java.
8. Boolean Values: True and False will be represented internally as integers 1 and 0 respectively. Implement code generation for True and False as you did for IntegerLiteral using these values.
9. If: The code emitted for If will follow this structure:
As you can imagine, code with multiple if statements would have multiply-defined labels. Therefore, add a field to your Visitor to act as an integer counter (e.g. count). Use the integer to form your labels (e.g. IfFalse42), and then increment the counter (e.g. to 43). In this way, you'll have unique yet readable labels. Test your implementation with TruePrint1.java and FalsePrint0.java
10. And, LessThan, Not: Using the previous techniques, develop code generation for And, LessThan, and Not. Use the test cases named for each, and create test cases of your own.
(These comments are to supplement your reading. Question(s) asked are for you to think about on your own and need not be turned in with the homework.)
6.0: Skim "Higher-Order Functions". If you wish to learn more about such languages, take our Survey of Programming Languages course. In the past, that course has included a project of writing an interpreter for a functional languages.
6.1: Pay close attention to the terminology and stack frame layout of Figure 6.2. We will seek to respect the stack frame layout previously described in our Appendix A reading. Compare Figure 6.2 with Figure A.11. Note that since the first four argument are passed in registers, only the following arguments (5+) are stored above the frame pointer. We'll be deviating from the text project, and storing all of our arguments and locals on the stack, making minimal use of registers. Therefore, out stack frame layout will be as follows:
... 8($fp): Argument 6 (the fifth parameter of the call) 4($fp): Argument 5 (the fourth parameter of the call) 0($fp): Argument 4 (the third parameter of the call) -4($fp): Argument 3 (the second parameter of the call) -8($fp): Argument 2 (the first parameter of the call) -12($fp): Argument 1 (this - the object of the method call, see "FUNCTION CALL" on p. 154) -16($fp): Saved Registers ($fp, $ra, and any callee saved registers ($s0-$s7) we use) ... Local variables and temporary values of method computation ... $sp: Last local/temporary
Note that the stack pointer may need to temporarily change value for a function call in order to place arguments for the next function call.
Registers: As mentioned, we will be simplifying our compiler implementation
by making minimal use of registers. This obviously yields very poor
runtime performance, since we'll often be storing/loading values to/from the
Return Addresses: We will not be distinguishing leaf from non-leaf procedures. For simplicity, we will always save the return address $ra to the stack.
Frame Resident Variables: Keep in mind that all of our non-field
variables will be frame-resident variables.
Skim the section on "Static Links". This is relevant to functional languages mentioned in 6.0.
6.2: For more details on our project see the homework description above. One important difference between our MIPS $sp and $fp conventions and those of the book is the fact that the caller MIPS $sp is one word above the callee $fp, rather than being one in the same. This accounts for the 4 byte offset in computing the new $sp and $fp values in the procedure call conventions of A.6. Also note that although the author states that no argument "escapes" (see top of p. 130) in MiniJava, we'll still be storing everything in the frame. Under "Temporaries and Labels", the author states "We use the word temporary to mean a value that is temporarily held in a register, ...". If the registers do not contain room for enough temporaries to compute a method, these temporaries spill and are stored in the frame. In effect, we'll spill all of our temporaries into the frame and act as if we only have a handful of registers to work with. Skip the section on "Managing Static Links".