COSC 4400
Assignment #6 - ToddlerGustave
NEW Due date: April 29, 2019


GOAL: Add features to your BabyGustave compiler so that it can also handle all of the ToddlerGustave language. This involves expanding your code generation. Your compiler should be able to handle method declarations, require and ensure clauses, parameter passing, local and instance variables, return values, and simple recursion (a method calling itself).

METHOD: First you need to read through the program tree and fill in the symbol table and method table with information about types of variables and prototypes of methods. I'VE WRITTEN this DeclReader and you can find it on the Class Demos page. To store the method information, I would suggest a entries in the symbol table indexed by method names and containing a MethodEntry object for each method. My MethodEntry class looks like:

  public class MethodEntry extends SymEntry{
	String start_label;
	HashMap locals; // this will hold local vars and params
	int localsize;  // number of words needed (to build activation record)
	Type return_type;

The declaration reader fills in the start_addr (by just getting a new Label), return_type, and formals and sets up the HashMap for locals. The locals HashMap has a VarEntry for each formal parameter and each local variable. YOU'LL ALSO probably want to include an entry for "Result" given the type of the return value (if there is one). My DeclReader code DOESN'T DO THIS.

Your scanner and parser should already handle all of the required syntax. You need to make changes in the code generator to handle method declarations (generating the code for the method body) and method calls. Parameter passing and local variables will be handled on the stack, using the ARM C standards discussed in class. Attributes (instance variables) should be located in a block of memory. This could be statically allocated with the format strings or dynamically allocated in the heap.

One change you must make in existing code is to move the starting and ending instructions out of prefix.txt and into the code generation for RootClass. This will make sure that the return occurs right after the main method and not after a bunch of other methods.

As the DeclReader reads method declarations, it assigns a Label for the corresponding code. That label is stored in the MethodEntry and you should be sure to emit that Label at the beginning of the code generated for the method. You might also want to include a comment in the assembly code saying which method (from which class) is starting here. Those comments can be a big help when debugging. Similarly, you'll want to look up that Label when you're generating code for a Call, to know which address to use in the call instruction.

In the Coder, as you work through the various method declarations, you'll find that it's convenient to keep track of the local symbol table for the current method. You'll need to be especially careful to distinguish between method variables (params and locals) which are located on the stack and instance variables (attributes) which are not. Your code generation for IdExpr will need to figure out which case you're in and generate the appropriate code. Also remember that an IdExpr might actually be a call to a method without parameters.

Similarly, the left hand side of an Assign tree might be a local or instance variable.

Here's a rough description of what the code for a method declaration should look like:

  • Emit the Label for the method (from MethodEntry).
  • The first statements in the body of the method need to be code to save the old frame pointer and return address. We also want to set the (new) frame pointer to the current stack pointer value:
    	push	{fp, lr}
    	mov	fp, sp
    
    Then we need to set aside enough stack space for the locals and temps. The local size will be stored in the MethodEntry, but we won't know how many temps we need until we've generated code for the method body. For this reason, I'd suggest storing the code for the method body in an ArrayList<String>. Then you can print this stack space line before actually printing the code for the body. Assuming the number of temps used is in "ntemps" and "me" is set to the appropriate MethodEntry, this line will look like:
      "\tsub\tsp, sp, #"+((4*(me.localSize + ntemps))&-8)+"\n"
    
  • At this point we now want to translate the body of the method. The require clause (if present) becomes code to evaluate the expression and if not true, print error message and branch to end of method (or call exit() ?). Similarly at end for ensure clause.
  • Finally, we emit the wrap-up code for the method.
  • For the Access (and AccessStmt) tree, we need to translate each of the parameter expressions, keeping track of the returned temporaries (probably in an array). We then move the temporaries into the appropriate spots on the stack by pushing them on the stack in the correct order.

    Our parameter passing will always pass the value of the actual argument expression. This is how Java works also. The effect is that primitive types (INTEGER, BOOLEAN) will be passed by value, whereas arrays and objects are passed by reference (since the corresponding variables hold references).

    Next we emit the call to the appropriate Label, which is found in the MethodEntry.

    Once the call is generated we move the returned result (if there is one) from the stack (just past the end at [sp,-4]) to a new temporary (which will be returned as the result of the Access code generation).

    TESTING: Following progs in sample-programs only use Toddler features:

      call.e, fib.e, sort.e, test_gcd.e, queens.e
    
    It also doesn't hurt to make sure that the baby_progs programs still work. As always, you should write some test programs yourself as needed.

    HAND-IN: Be sure and send me a *complete* working copy of your compiler. Submit all the directories, source files, makefile, etc. needed to create your compiler. A zip file is a great way to do this. Include a README file to highlight important features.