Lab 3: Code Generators


Lab Overview

In this lab, your task is to build code generators into Tiger for MiniJava. To be specific, there are 4 code generators in this lab: the first one is a C code generator generating ANSI C code, the second one is a Java bytecode generator targeting Oracle's JVML (this is Java's standard target code format), the third one is a Dalvik bytecode generator targeting Google's Dalvik virtual machine, and the fourth and last one is an x86 code generator targeting x86 chips from both Intel and AMD.

The first code generator: the C code generator is required. All other three code generators: Java bytecode, Dalvik bytecode, and x86 are optional. However, you are encouraged to try the three, for it will give you deeper knowledge and insight of a code generator.

Getting Started

First check out the source for Lab3 at:

  $ git commit -am 'my solution to lab2'
  $ git checkout -b Lab3 origin/Lab3
the first command will commit your changes to Lab2 branch to the local Git repository, and the second command will create a new local Lab3 branch and and check out code into this new branch from the remote Lab3 branch.

Again, you will need to merge your code from Lab2 into the new local Lab3 branch:

  $ git merge Lab2
Don't forget to resolve any conflicts before commit to the local Lab3 branch:
  $ git commit -am 'lab3 init'

You should first import the new Lab3 code into your Eclipse and browse through the source code, which contains these new packages:
    
codegen.C
  classes operating on the C AST (translator, pretty printer, etc..)
codegen.C.*
  data structures defining the C AST
codegen.bytecode
  classes operating on the Java bytecode AST
codegen.bytecode.*
  data structures defining the Java bytecode AST
codegen.dalvik
  classes operating on the Dalvik bytecode AST
codegen.dalvik.*
  data structures defining the Dalvik bytecode AST
codegen.x86
  data structures and classes for the x86 code generator
runtime/*
  a simple runtime system

Hand-in Procedure

When you finished your lab, zip you code and submit to the school's information system.

Part A: The C Code Generator

The first code generator you'll build will target the C programming language, that is, the generator will generate ANSI C code which can then be compiled into native code by a C compiler. Nevertheless to say, this is NOT the standard Java compilation pipeline, because we are generating native code from Java directly, instead of Java bytecode. However, it's profitable for you to build such a C code generator first: you will gain key insights into some key techniques in implementing modern OO language features, including single inheritance, virtual function tables and dynamic dispatching, etc.. From a historical prospective, early C++ compilers used to implement their code generators in a way similar to the one in the Tiger compiler you're building, one such example is Bjarne Stroustrup's CFront compiler, though this is not the case for modern C++ compilers. As a modern sample compiler for Java, the GNU's GCJ compiler for Java can generate native code directly.

Abstract Syntax Trees for C

The first step in designing and implementing a C code generator is to teach the Tiger compiler what the C programming language is (although we only need a subset of C). For this purpose, we should first design a set of data structures to represent the abstract syntax tree for (the chosen subset of) the C programming language, much like the abstract syntax for Java in the previous lab.

Exercise 1. Browse through the code in all packages codegen.C.*, such as codegen.C.exp or codegen.C.stm etc.. Make sure you understand the relationship between these data structures and the corresponding C syntax. For those not familiar with C, the best reference for the C language is The C Programming Language by Brian Kernighan and Dennis Ritchie (widely known as 'K&R'). Pick up this book and read it. (Trust us, for any serious programmers, C is her indispensable tool (friend).)

In the file codegen.C.TranslateVisitor.java resides the code for the C code generator. For now, the code in this file is incomplete (it's your job next to fill in the missing code), however, we have offered sufficient code to compile the test case Fac.java.

Exercise 2. Compile this project and run the the Tiger compiler to compile the Fac.java test case:

  $ java -cp bin Tiger -testFac
which will generate C code in an output file a.c. Browse through the generated C code in the file a.c, for now, you don't need to understand details of the generated program a.c, just make sure you understand the relationship between the C program in a.c and the abstract syntax tree data structure for C (from exercise 1).
In the following, we will make extensive use of pretty printing in debugging the code generator for C. The file codegen.C.PrettyPrintVisitor.java contains the pretty printing code for C.

Exercise 3. Finish this printer by filling in the missing code in some methods (as before, methods with empty bodies). As the output C code will be further compiled by a C compiler, so pay special attention to the C syntax constraints.

For now, don't worry about testing your pretty printer, for it will be painful to write C abstract syntax trees by hand. In the next, you can test your pretty printer along with the C code generator.

Virtual Function Tables

Given the above abstract syntax tree for C, it will be relatively easy to write another visitor which traverses the Java abstract syntax tree and builds a correspondingly (equivalent) C abstract syntax tree for C. However, one must pay attention to some special features of OO languages: the class inheritance and virtual method dispatch.

Java only allows single inheritance, so the inheritance relationship between all classes forms a tree with the sole Object as the root class. The first question one must answer is what fields and methods a class may contain, that is, one should calculate, for each class, the fields and methods for that class. The key point to note here is that the fields and methods in a given class C include not only the ones directly declared in that class C, but also the ones inherited from the current class C's parent class, and from the parent's parent class, and so on, until reaching the tree root class Object. So, the inheritance relationship are cascaded.

As Java only allows single inheritance, so the prefixing algorithm works well for this. The basic idea of the prefixing algorithm can be illustrated by the following pseudo-code:

  void prefixing ()
  {
    build the inheritance tree t for all classes;     // every class is a tree vertex, with "Object" as the tree root
    foreach (class C in the tree t, in a BFS order) {    // visit the tree "t" in a breadth first order
        make a fresh empty class NewC;
        copy all fields from C's parent into NewC;
        append all fields from C to NewC;
        copy all methods from C's parent into NewC;
        append all methods from C to NewC;  // what about method overriding?
        delete the class C;
    }
  }
This algorithm first builds an inheritance tree t, with each tree vertex being a Java class. Then one can visit the inheritance tree in a breadth-first order (BFS), when one come to a tree vertex with class C, one builds a new

There is a subtlety in this algorithm: in order to compile method overriding correctly, one should overwrite the corresponding method slots from the parent class, instead of appending it to the methods in the parent class. By the word "corresponding", we mean methods with same names (as MiniJava does not allow method overloading).

Exercise 4. For the two Java classes below:

class A{
  int x;
  int y;
  void f(){}
  void g(){}
}
class B extends A{
  int x;
  int z;
  void h(){}
  void g(){}
}
Write down the new classes NewA and NewB, after one apply the prefixing algorithm on these two classes A and B. Can we put the field z before the field y? Why or why not?

The prefixing algorithm will make all classes closed, that is, no class will inherit from any other classes, and all references to instance variables and methods will be limited to the current class.

The current implementation of the prefixing algorithm in the Tiger compiler makes use of a slightly different algorithm than the algorithm above. The algorithm in the Tiger compiler makes use of table to store all the inheritance tree nodes (the definition of the table is in the file codegen.C.ClassTable.java). Essentially, this table is a mapping from class name to the class' containing fields and methods. And the inherit method in that class takes as input a class name c and calculates all fields and methods for that class c.

Exercise 5. Read the code in the method inherit in the class codegen.C.ClassTable.java and make sure that you understand how it can calculate all fields and methods for a given class. Especially, how are methoding overridings addressed?

Challenge! The current algorithm used by the method inherit, though correct, is very inefficient. Consider this Java sample program:

  class C1{
    int x1;
  }
  class C2 extends C1{
    int x2;
  }
  ...
  class Cn extends Cn-1{
    int xn;
  }
For this program, the computation complexity of the method inherit will be 1+2+...+n = O(n2). Propose a linear time algorithm to implement prefixing. Then try to implement your algorithm in your Tiger compiler (so that your Tiger compiler can compiler large Java programs with very deep inheritance chains).

Generating C from Java

Now we are going to generate C code from Java, and the visitor for this resides in the file codegen.C.TranslateVisitor.java. This visitor scans a program in two passes: in the first pass, this visitor will scan all classes and calculate an inheritance table (just as discussed above), and the table will be used in the second pass: translating various Java features into appropriate C features.

Exercise 6. Finish the methods in codegen.C.TranslatorVisitor.java, by filling in the missing code. (Again, you should fill in those methods with empty bodies.)

To this point, your Tiger compiler should compile any legal MiniJava program to a corresponding legal C program. You should test your compiler thoroughly before continuing. For instance, to test your Tiger compiler on the test case test/Factorial.java, just compile the MiniJava program like this:

  $ java -cp bin Tiger ../test/Factorial.java -codegen C
which will generate a C file Factorial.java.c, and then you can compile the generated C file:
  $ gcc -c Factorial.java.c
if your implementation is correct, there should be no errors (and even no warnings) in the above two steps.

Fix any possible bugs and don't forget to do regression testing.

A Minimal Runtime

To compile the generated C file into final executables, one need to write a run-time system. In its general meaning, the terminology runtime may refer to any possible forms of supporting facilities a compiler generated binaries needs: libraries, garbage collectors, debugging supports and so on. In this part of the lab, you will implement a minimal runtime system that only include the preliminary functionality. In future labs, you'll enhance this runtime system with more fancier features by adding more functionalities, such as a garbage collector.

Browse through the directory runtime/, in which resides the minimal runtime system we offered you. You may find:
    
gc.c
code for the garbage collector
lib
code for the library
main.c
program entry point
runtime.c
a wrapper
Especially, the file gc.c contains the preliminary skeleton code for a garbage collector (though for now, it is a shame to call it a garbage collector because it just allocates memory but never reclaims them).

Exercise 7. Finish file gc.c, by filling in missing code. For now, you can just allocate memory for objects and arrays but don't worry about memory reclamation issues, which is the task of next lab.

Now, you can link this runtime system into the C file generated from your Tiger compiler to produce the executables. For instance, for the above generated Factorial.java.c, you may run:
  $ gcc Factorial.java.c runtime/runtime.c
which will produce an executable a.out (or a.exe on Windows).

It will be very boring to type various commands on the prompt for the entire compilation pipeline: Tiger compiling, gcc compiling, linking. etc.. A much more convenient way to do this is to add some glue code into Tiger so that it can do all these steps in an automatic manner.

Exercise 8. Add some glue code into the file Tiger.java, which performs compiling MiniJava to C, compiling the generated C and linking with the runtime, etc. in an automatic manner. With this, you don't need to type the above commands manually. You may find the Java runtime class useful.

This completes the C code generator. To this point, your Tiger compiler can generate executables for any legal MiniJava programs. Don't forget to test your Tiger compiler thoroughly.

Part B: The Java Bytecode Generator

In this part of the lab, your job is to design and implement a code generator generating Java bytecode targeting the Java virtual machine (JVM). Java bytecode is designed by Sun (now Oracle) and is the standard target code for Java. Though there are many JVMs available, the one we will use in the lab is the HotSpot JVM developed by Oracle (so you don't need to do further work of installation or configuration, as we have been using HotSpot to compile the Tiger compiler). However, there is nothing special with HotSpot, you can use other JVMs as long as they conform to the JVM specification.

Abstract Syntax for Java Bytecode

Just like implementing the code generator for C, the first step in implementing a Java bytecode generator is to design a set of data structures for representing abstract syntax trees of Java bytecode. However, some difficulties must be addressed before doing so: first, the standard Java bytecode instruction set contains 202 instructions, which, nevertheless to say, are much more than we actually need to compile MiniJava (for instance, we don't need bytecode instructions for floating point-related operations); second, the official Java bytecode specification from Sun actually only specifies binary forms (i.e., a binary string of 0's and 1's) of the bytecode instructions, which is good for JVM execution but bad for compiler to generate code for, as the instructions in binary forms are harder to generate and process.

The first difficulty is easy to resolve: one just choose, from the Java bytecode instruction set, a subset that is enough to compile the MiniJava subset of Java. It's your job next to figure out such a subset of the Java bytecode instructions.

To resolve the second difficulty, one can choose a Java bytecode assembler which can assemble instructions in readable text form into binary forms which can be executed by a JVM. This process is very much like that a traditional assembler assembles assembly program into binaries. With the bytecode assembler, your Tiger compiler can generate text form Java bytecode instructions and leave the tedious work of assembling to the bytecode assembler. Nevertheless to say, writing a bytecode assembler from scratch is, though conceptually simple, tedious and too much work. Fortunately, there are some mature third party assembler that you can use in your Tiger compiler. To be specific, the Java bytecode assembler you'll use in this lab is called Jasmin. There is no specific reason why we choose Jasmin among many such assemblers. But Jasmin has been the oldest Java bytecode assembler and has been used widely in many projects. Jasmin has become the de-facto assembly format for Java bytecode.

Exercise 9. Take a look at the Jasmin homepage and read the user guide to familiarize yourself with how Jasmin works. As a test, you should compile and run the HelloWorld bytecode program. You can use the HotSpot JVM from Sun to run the generated Java classes.

As we are using Jasmin assembler to assemble the Java bytecode, so it will be very convenient to design the Java bytecode abstract syntax tree data structures according to the Jasmin syntax.

Exercise 10. The package codegen.bytecode.* contains the data structures to represent Java bytecode abstract syntax trees. You may want to refer to the Jasmin XT syntax for this. The package codegen.bytecode.stm.* contains the abstract syntax for Java bytecode instructions. You may want to refer to the Java Virtual Machine online instruction reference for a detailed explanation of the syntax and semantics for each instruction. It should be easy to know which class represents which instruction, because they have the same name (for instance, the New.java class represents the new instruction. Make sure you understand all the existing code base before continuing. When in doubt, refer to the official JVM manual (chapter 6).

A final word of reminder, the classes in the codegen.bytecode.stm.* package we offered you is NOT sufficient to compile the MiniJava language. It's your task, in the following, to figure out what other instructions are necessary and how to implement them.

Generating Java Bytecode

In this part, you'll write the Java bytecode generator for Tiger. This generator is (once again) a visitor which translates the Java syntax tree into the Java bytecode syntax tree. For this purpose, you should have a thorough understanding of how a JVM works and especially how to compile Java features to corresponding Java bytecode feature. The chapter 3 of the JVM specification documents the compiling techniques from Java to Java bytecode, read this document if you haven't.

Exercise 11. Finish the Java bytecode generator in the class codegen.bytecode.TranslateVisitor.java. Feel free to modify other parts of your Tiger compiler when necessary: the Java bytecode syntax tree, the pretty printer, the Java abstract syntax tree, the elaborator, and so on.

For the generated Java bytecode, use Jasmin to assemble it and then use a JVM to run it. You don't need to write runtime system or garbage collectors, for they come with the JVM (for free).

Challenge! If your Java bytecode generator contains any bugs, it's often the case that the generated bytecode will behave incorrectly in the following ways:

  • the generated Java bytecode doesn't compile (rejected by the Jasmin assembler);
  • the generated Java bytecode doesn't verify (rejected by any standards-compliant class verifier);
  • the generated Java bytecode doesn't run (rejected by the JVM).
For these cases, the diagostic messages will be helpful for you to locate where the bugs are and what they are. However, is's also helpful to have more debugging facilities, here are some directions you can go:
  • Modify your Java bytecode generator to generate debuggable Java bytecode. For instance, you can annotate the generated Java bytecode with the source program being compiled. Here is one example: for the Java expression 1+2, your code generator may generate this code (expressions generated as comments followed by the instructions generated for it):
      ; 1 + 2
      ldc 1
      ldc 2
      add
    
  • Use Jasmin's built-in debugging facilities: such as the -g option and the .line directive.
  • Use some third party debuggin tools, such as bytecode-visualizer.
Implement some of the ideas into your Tiger compiler.

Challenge! There is a weird static constraint on the code size of a method, to quote from the Java Virtual Machine Specification, section 4.9.1:

     the value of code_length item must be less than 65536.
that is, the generated Java bytecode for any Java methods should be less than 65536 bytes. This makes it difficult, if not impossible, to compile a very large Java method (for instance, the Monster class from Lab 1). Propose some techniques to make compilation of large methods possible, and try to implement into your Tiger compiler so that your Tiger compiler can compile any large Java programs.

Part C: The Dalvik Bytecode Generator

In this part of the lab, your job is to design and implement a code generator generating Dalvik bytecode targeting the Dalvik virtual machine (DVM). Dalvik bytecode is designed by Google and is the standard target code on the Android platform. There are many DVMs implementations available, for instance, the Dalvik VM from Google, the Dalvik Turbo from Myriad Group, the Dalvik-on-Java VM and so on. The one we will use in the lab is the Google Dalvik, so before doing the rest parts of this lab, you should first install the Android SDK. Then follow the instructions in the above link to set up your eclipse appropriately and make sure your virtual devices work properly. And if you happen to have some Android devices: phones, pads, etc., it's OK to use them, but don't forget to enable the "debugging mode" on your devices.

Abstract Syntax for Dalvik Bytecode

Just like implementing the code generator for C, the first step in implementing a Dalvik bytecode generator is to design a set of data structures for representing abstract syntax trees of Dalvik bytecode. However, some difficulties must be addressed before doing so: first, the standard Dalvik bytecode instruction set contains more than 200 instructions, which, nevertheless to say, are much more than we actually need to compile MiniJava (for instance, we don't need bytecode instructions for floating point-related operations); second, the official Dalvik bytecode specification from Google actually only specifies binary forms (i.e., a binary string of 0's and 1's) of the bytecode instructions, which is good for DVM execution but bad for compiler to generate code for, as the instuctions in binary forms are harder to generate and process.

The first difficulty is easy to resolve: one just choose, from the Dalvik bytecode instruction set, a subset that is enough to compile the MiniJava subset of Java. It's your job next to figure out such a subset of the Dalvik bytecode instructions.

To resolve the second difficulty, one can choose a Dalvik bytecode assembler which can assemble instuctions in readable text form into binary forms which can be executed by a DVM. This process is very much like that a traditional assembler assembles assembly program into binaries. With the bytecode assembler, your Tiger compiler can generate text form Dalvik bytecode instructions and leave the tedious work of assembling to the bytecode assembler. Nevertheless to say, writing a bytecode assembler from scratch is, though conceptually simple, tedious and too much work. Fortunately, there are some mature third party assembler that you can use in your Tiger compiler. To be specific, the Dalvik bytecode assembler you'll use in this lab is called Smali. There is no specific reason why we choose Smali among many such assemblers. But Smali has been the most popular Dalvik bytecode assembler and has been used widely in many projects. Smali has become the de-facto assembly format for Dalvik bytecode.

Exercise 12. Take a look at the Smali homepage and familiarize yourself with how Smali works. As a test, you should compile and run the HelloWorld bytecode program. You can use the Dalvik VM from Google to run the generated dex classes. Suppose you have put the code in the file HelloWorld.smali, then run these commands:

  // assemble the "HelloWorld.smali" to "classes.dex"
  $ java -jar smali.jar -o classes.dex HelloWorld.smali
  // compress the .dex to a package
  $ zip HelloWorld.zip classes.dex
  // push the package to your (virtual) Android devices
  $ adb push HelloWorld.zip /data/local
  // run the program using the Dalvik VM
  $ adb shell dalvikvm -cp /data/local/HelloWorld.zip HelloWorld
which should print "Hello world!" terminated by a new line.
As we are using Smali assembler to assemble the Dalvik bytecode, so it will be very convenient to design the Dalvik bytecode abstract syntax tree data structures according to the Smali syntax.

Exercise 13. The package codegen.bytecode.* contains the data structures to represent Smali bytecode abstract syntax trees. However, it seems that the Smali home page doesn't offer an official reference manual on the syntax it uses. So you have to read the Smali source code to famaliarize yourself with the syntax. The package codegen.bytecode.stm.* contains the abstract syntax for Dalvik bytecode instructions. You may want to refer to the official Google reference manual for a detailed explanation of the syntax and semantics for each instruction. It should be easy to know which class represents which instruction, because they have the same name (for instance, the NewInstance.java class represents the new-instance (opcode 0x22) instruction. Make sure you understand all the existing code base before continuing. When in doubt, refer to the official Google manual.

A final word of reminder, the classes in the codegen.bytecode.stm.* package we offered you is NOT sufficient to compile the MiniJava language. It's your task, in the following, to figure out what other instructions are necessary and how to implement them.

Generating Dalvik Bytecode

In this part, you'll write the Dalvik bytecode generator for Tiger. This generator is (once again) a visitor which converts the Java syntax tree into the Dalvik bytecode syntax tree. For this purpose, you should have a thorough understanding of how a DVM works and especially how to compile Java features to corresponding Dalvik bytecode feature. The chapter 3 of the JVM specification documents the compiling techniques from Java to Java bytecode, read this document if you haven't.

Exercise 14. Finish the Dalvik bytecode generator in the class codegen.dalvik.TranslateVisitor.java. Feel free to modify other parts of your Tiger compiler when necessary: the Dalvik bytecode syntax tree, the pretty printer, the Java abstract syntax tree, the elaborator, and so on.

For the generated Dalvik bytecode, use Smali to assemble it, push it to your Android devices and then run it with Dalvik. You don't need to write runtime system or garbage collectors, for they come with the Dalvik VM (for free).

Challenge! If your Java bytecode generator contains any bugs, it's often the case that the generated bytecode will behave incorrectly in the following ways:

  • the generated Dalvik bytecode doesn't compile (rejected by the Smali assembler);
  • the generated Dalvik bytecode doesn't verify (rejected by any standards-compliant class verifier);
  • the generated Dalvik bytecode doesn't run (rejected by the DVM).
For these cases, the diagnostic messages will be helpful for you to locate where the bugs are and what they are. However, it's also helpful to have more debugging facilities, here are some directions you can go:
  • Modify your Dalvik bytecode generator to generate debuggable Dalvik bytecode. For instance, you can annotate the generated Dalvik bytecode with the source program being compiled. Here is one example: for the Java expression 1+2, your code generator may generate this code (expressions generated as comments followed by the instructions generated for it):
      ; 1 + 2
      const v5, 1
      const v6, 2
      add-int v7, v5, v6
    
  • Use Smali's built-in debugging facilities: such as the -T and -V options.
  • Use some third party debuggin tools, such as bytecode-visualizer.
Implement some of the ideas into your Tiger compiler.

Challenge! There is a weird static constraint on the number of available registers in a method, to quote from the Bytecode for the Dalvik VM, General design:

     Because, in practice, it is uncommon for a method to need 
     more than 16 registers, and because needing more than eight 
     registers is reasonably common, many instructions may only 
     address the first 16 registers. When reasonably possible, 
     instructions allow references to up to the first 256 registers. 
     In cases where an instruction variant isn't available to address 
     a desired register, it is expected that the register contents 
     get moved from the original register to a low register (before 
     the operation) and/or moved from a low result register to a high 
     register (after the operation).
that is, the generated Dalvik bytecode for any Java methods can use at most 256 registers. This makes it difficult, if not impossible, to compile a Java method with many variables (for instance, the Monster class from Lab 1). Propose some techniques to make compilation of such methods possible, and try to implement into your Tiger compiler so that your Tiger compiler can compile any Java programs. (You may refer to the DX compiler source code on how Google did this.)

Part D: An x86 Generator

One will get real satisfaction if one can generate code for real raw machine. In this part of the lab, you'll design and implement another code generator targeting the x86 instruction set architecture. We'll give no code here, so it's your duty to write the code from scratch.

Challenge! Write an x86 code generator. To keep things simple, you can use the x86 machine as a stack machine (just like the JVM), so that you can borrow most your code from the 2nd part of the lab here.

Handin

This completes the lab. Remember to hand in your solution to the information system.