This assignment concerned the design, development, testing, and documentation of a translator from MiniJava to the Java Virtual Machine. The skeleton for the assignment provided much of the translator implementation. We needed to finish it off by implementing the missing portions.
The features that we had to be implement were:
true and false)
and operations (and, not), and
The following discussion considers each one of these in turn.
The skeleton already implemented addition by first translating the
two operand expressions, then generating an iadd
instruction. I followed the same scheme for the two missing operations.
For example, for subtraction I use an isub operation as
follows in translateExp.
case MinusExp (left, right) =>
translateExp (left)
translateExp (right)
gen (Isub ())
In the week 10 practical exercise we saw a simple scheme for translating conditional statements. The difference for this assignment is that the conditional statements have both then and else branches, whereas in the practical they only had one branch. It was easy to extend the scheme to accommodate the extra branch.
The code I generate for a conditional statement first checks the
condition, jumping to a label for the else branch if the condition
is false. Otherwise, the code falls through to the code for the
then branch, followed by a jump over the else branch. The code that
was added to translateStmt is as follows.
case If (cond, stmt1, stmt2) =>
val label1 = makeLabel ()
val label2 = makeLabel ()
translateCond (cond, label1)
translateStmt (stmt1)
gen (Goto (label2))
gen (Label (label1))
translateStmt (stmt2)
gen (Label (label2))
While statements are handled essentially the same as in the practical. The code first checks the condition and jump after the loop body if the condition is false. Otherwise, we fall through to the body followed by a jump back up to the top of the loop. The code is as follows.
case While (cond, stmt) =>
val label1 = makeLabel ()
val label2 = makeLabel ()
gen (Label (label1))
translateCond (cond, label2)
translateStmt (stmt)
gen (Goto (label1))
gen (Label (label2))
As noted in the assignment handout, the JVM doesn't really have Boolean
operations, so to translate MiniJava Booleans, we use integers, with the
convetion that one is true and zero is false. Thus, the two cases for
true and false in translateExp became:
case TrueExp () =>
gen (Iconst_1 ())
case FalseExp () =>
gen (Iconst_0 ())
I chose to implement the Boolean and and not
operations using flow control, rather than trying to find integer operations
that had the desired effect. In the case of not it suffices to
evaluate the operand expression, then use an ifeq instruction
to jump to code to push one if the expression is zero. Otherwise, the
code falls through to code to push zero and jump over the other code.
case NotExp (exp) =>
val label1 = makeLabel ()
val label2 = makeLabel ()
translateExp (exp)
gen (Ifeq (label1))
gen (Iconst_0 ())
gen (Goto (label2))
gen (Label (label1))
gen (Iconst_1 ())
gen (Label (label2))
The and operation can be handled in a similar fashion,
but we need to take care to implement the short-circuit evaluation
semantics. In other words, if the left operand expression evaluates to
false, then we should not evaluate the right operand expression.
The code first evaluates the left expression. If that expression is equal to zero, it jumps straight to code to push a zero (false) value as the result of the whole expression. Otherwise, if the left expression is true, the control falls through to code that evaluates the right expression. No more work needs to be done, since at that point the value of the right expression is the same as the value of the whole expression, and that value is on the operand stack already. So, after the right expression has been evaluated, we just need to jump over the false code.
case AndExp (left, right) =>
val label1 = makeLabel ()
val label2 = makeLabel ()
translateExp (left)
gen (Ifeq (label1))
translateExp (right)
gen (Goto (label2))
gen (Label (label1))
gen (Iconst_0 ())
gen (Label (label2))
Arrays were a bit trickier than the other constructs, not because they were hard to generate code for, but because I needed to find the right instructions to deal with arrays. At this point it is worth noticing that the JVM has two sets of array instructions: one set for arrays of primitive values (e.g., integers) and another set for arrays of object references. MiniJava only has arrays with integer elements, so we only use the first set of instructions.
First, array creation. For integer arrays creation is achieved by
the newarray instruction where the argument is the
primitive element type. When using Jasmin, we can specify the
elementy type by name. The number of elements that we want in the
array must already be on the operand stack when the creation
instruction is executed. Thus, in translateExp a new
array expression is translated as follows.
case NewArrExp (exp) =>
translateExp (exp)
gen (NewArray ("int"))
The length of an array is obtained by pushing the array reference
on the operand stack and executing an arraylength
instruction. Thus, the translation code for this case is:
case LengthExp (base) =>
translateExp (base)
gen (ArrayLength ())
An array indexing operation requires the array reference and the
index of the element which we want to obtain to both be on the
stack (in that order). Then the iaload instruction
replaces them with the requested element.
case IndExp (base, ind) =>
translateExp (base)
translateExp (ind)
gen (Iaload ())
Finally, in translateStmt we need to implement
array assignment statements. These are implemented using
iastore instructions, which expect the array
reference, the index value and the value to be assigned to be
on the stack (in that order). In an ArrAssign
statement construct the array is given by an identifier, so
we use the skeleton's translateIdnLoad method to
generate code to load the value of whatever that identifier
refers to (since it might be a field, argument or local
variable). Thus, the code to translate array assignments
is as follows.
case ArrAssign (idnuse, ind, exp) =>
translateIdnLoad (idnuse)
translateExp (ind)
translateExp (exp)
gen (Iastore ())
I used two testing approaches for this asignment. First, for each of the constructs that I had to implement I created small MiniJava programs that uses these constructs in simple ways, trying to focus on one aspect of the construct at a time.
I tried to get tests that tested all of the different possibilities
for a construct. E.g, for the and expressions, we have
three possibilities, depending on the truth value of the left and
right expressions. Thus, there are three test cases to make sure that
the possibilities are all covered. (The implementation of the basic
Boolean values are also tested in these tests.)
To illusrate the idea, consider the following test case:
class AndFalseLoop {
public static void main () {
System.out.println (false && (new LoopClass ().run ()));
}
}
class LoopClass {
public boolean run () {
while (true) {}
return false;
}
}
This test is designed to test the short-circuit evaluation. An
and expression is evaluated which has a false left
operand and a right operand that goes into an infinite loop.
Under short-circuit evaluation we expected to get false for this
program, so the expected output is 0.
Similarly, for statements such as conditional or while statements, I have tests that make sure that all of the flow control possibilities are covered. E.g., for a while statement, we cover zero iterations of the loop, one iteration and more than one iteration.
As a further illustration, we have the following test case which
tests that loops that iterate more than once iterate the correct
number of times. The expected output for this test is 10.
class WhileMany {
public static void main () {
System.out.println (new WhileManyClass ().run ());
}
}
class WhileManyClass {
public int run () {
int v;
v = 0;
while (v < 10) {
v = v + 1;
}
return v;
}
}
As well as using these small synthetic tests, I also tested my translator using the bigger MiniJava programs that were provided in the skeleton. These bigger tests are likley to exercise the translation much more than the simple cases, particularly using more complex operands for the expressions or bodies for the structured statements.
I wrote a small script setup that runs my compiler over all of the
tests in the test sub-directory, uses Jasmin to produce class files,
runs the main program with the JVM, and captures the output from the
run. That output is compared to the .exp file that
belongs to the test and any difference is flagged as a test failure.
In each case, I obtain the output that I expect to see, so I am reasonably confident that my translations are correct.