Macquarie University
Department of Computing

COMP332 Programming Languages 2012
Assignment Three (Sample Solution)

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:

  1. artihmetic operations (subtraction and multiplication)
  2. conditional and while statements
  3. Boolean constants (true and false) and operations (and, not), and
  4. array creation, length, indexing and assignment.

The following discussion considers each one of these in turn.

Arithmetic operations

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 ())

Conditional and while statements

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))

Boolean constants and operations

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

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 ())

Testing

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.


Tony Sloane

Copyright (c) 2012 by Anthony Sloane, Macquarie University. All rights reserved.