Project 5

Deadline Tuesday, December 5, 11:59pm
Name on Marmoset P5
Bonus component: P5Bonus
To Submit Standard Format: wlp4gen.cc OR wlp4gen.rkt
Prescanned Format: wlp4gen-prescanned.cc OR wlp4gen-prescanned.rkt
Preanalyzed Format: wlp4gen-preanalyzed.cc OR wlp4gen-preanalyzed.rkt
Marking Scheme 60 marks for release tests, 40 marks for secret tests
Bonus component: +0.25% on final grade

WLP4 Code Generator

In this project, you will implement the code generation phase of compilation for the WLP4 programming language.

At this point, the WLP4 program has passed the scanning, parsing, and semantic analysis phases and is free of compile-time errors, meaning it is a valid program that meets all the requirements of the WLP4 language specification. The program has been converted to a parse tree, and the parse tree has been annotated with information about types of expressions.

Your goal is to generate MIPS assembly code that implements the functionality of the WLP4 program.

Resources

We provide the following resources.

Using the print.merl library

The library exports a procedure called print. To use this procedure, your generated MIPS assembly code must include the directive .import print at the top of the file.

The print procedure expects a parameter in $1. It interprets this parameter as a two's complement integer, formats the integer in base 10, and prints the base 10 representation to standard output, followed by a newline.

The procedure preserves the values of all registers. It modifies the stack, but only at locations above the stack pointer $30. As long as you are not storing important data above the stack pointer (which is improper use of the stack) it will not overwrite your data.

The procedure is called like any other MIPS procedure, by using jalr. Remember to save $31 before making the call, as jalr will modify $31.

Assemble your generated code with cs241.linkasm, then use cs241.linker to link the assembled MERL file with print.merl and produce a combined MERL file that you can execute. The "Testing Your Code Generator" section farther down the page gives a more detailed sequence of commands to use.

Using the alloc.merl library

The library exports three procedure called init, new and delete. To use these procedures, your generated MIPS assembly code must include the following directives at the top of the file:

.import init
.import new
.import delete

Initialization

The init procedure sets up the heap. It must be called before any calls to new or delete, and it must not be called more than once.

The init procedure expects two parameters, in $1 and $2.

Allocation (New)

The new procedure expects a parameter in $1, the size of the array to allocate. It attempts to allocate an array of the given size and returns a value in $3:

Note that your code generator is expected to return NULL on a failed allocation, and it is expected to use 1 (not 0) as the representation of NULL, so you will need to detect the case where new returns 0 and change the value in $3 to 1 instead.

Deallocation (Delete)

The delete procedure expects a parameter in $1, the address of the array to deallocate. This must be the address of a currently allocated array as returned by new, or else the behaviour is undefined. The procedure does not return a value; it simply deallocates the given array.

Note that in WLP4, deleting a NULL pointer is supposed to do nothing. However, passing the address 1 (which represents NULL in our course convention) to the delete procedure will cause an error. Thus, your code generator needs to detect the case where the pointer to delete is NULL and avoid making the call to delete.

Other Information

The init and delete procedures preserve the values of all registers, and the new procedure preserves the values of all registers except $3. All three procedures modify the stack, but only at locations above the stack pointer $30. As long as you are not storing important data above the stack pointer (which is improper use of the stack) it will not overwrite your data.

The procedures are called like any other MIPS procedure, by using jalr. Remember to save $31 before making the call, as jalr will modify $31.

Assembly & Linking

Assemble your generated code with cs241.linkasm, then use cs241.linker to link the assembled MERL file with alloc.merl and produce a combined MERL file that you can execute. The "Testing Your Code Generator" section farther down the page gives a more detailed sequence of commands to use.

Note that alloc.merl must appear as the last command line argument to the linker (that is, it must come after all other MERL files passed to the linker). This is because the heap is allocated after the end of alloc.merl, so if it is not the last argument, the heap will overwrite other linked code.

Input Formats

There are three input formats. Which ones you can use depends on which previous projects you have completed.

Like semantic analysis, the code generation phase requires examining a parse tree that represents the input WLP4 program. To generate correct code for certain constructs, you will need the information about types that was computed during semantic analysis.
Reconstructing the Tree (from Preanalyzed Format)

Your tree should have five string fields:

Additionally, it should have a field rhs which is a vector or list of strings, and stores the symbols on the right-hand side of the CFG rule in a nonterminal node.

This is just a very basic tree structure that gives you what you need to get started with the project. You are free modify this algorithm and your tree structure to to do extra processing and store additional useful information in the tree.

  1. Read one line from standard input. The line consists of a number of whitespace-separated strings, which we will refer to as "parts" of the line. You can process the line part-by-part in C++ by creating a stringstream from the line; in Racket, you can use string-split to split the line on whitepace.
  2. Initialize the rule field to an empty string. We will build up this string as we read each part of the line (which represents a CFG rule, optionally with a type annotation at the end).
  3. The first part of the line is the left-hand side of the rule. Store this part in the lhs field, and append this part to the rule field.
  4. Read the remaining parts of the line one at a time.
    • If the part is the special string .EMPTY, the CFG rule has an empty right-hand side, and there is no type annotation. Break and continue to the next step.
    • If the part is a colon (":"), then the next part after is a type annotation, int or int*. Read the next part and store it in the type field, then break and continue to the next step.
    • Otherwise, the part is a symbol from the right-hand side of the CFG rule. Add the symbol to the rhs vector. Append a space to the rule field, and then append the symbol.
  5. Now, loop over the symbols in the rhs vector or list (if it is nonempty) and add one child to the root for each symbol. Determine whether the symbol is a terminal or nonterminal. In the WLP4 grammar, terminals are in ALL CAPS, and nonterminals are in lowercase, so you can just check whether the first character of the symbol is uppercase or lowercase to determine this.
    • If the symbol is a terminal, the child should be a leaf node storing the terminal. Read the next line of standard input, which is guaranteed to represent a terminal token if you have implemented the rest of the algorithm correctly.
      • Split the line into parts as before.
      • The first part is the kind of the token and the second part is the lexeme.
      • Create a leaf node and store the kind of the token in the leaf node's kind field, and the lexeme in the leaf node's lexeme field. The leaf node should be created directly rather than through a recursive call.
      • Type information for terminal nodes is not needed during code generation, so you can ignore the rest of the line (even if there are more parts).
    • If the symbol is a nonterminal, construct the child by recursively calling this algorithm.

The rule field in each nonterminal node should contain a representation of the CFG rule with no extraneous whitespace (there should be only a single space between each symbol, and no leading or trailing whitespace). This is important since it will allow you to do string comparisons to determine the rule at a node.

Output Requirements

The output of your code generator should be a MIPS assembly language program that implements the functionality of the input WLP4 program. Producing MIPS machine code is not required or expected (which means you do not need to hook up your code generator to your MIPS assembler from Project 2.)

In previous projects, there was generally a unique "correct output" you were expected to produce, and a reference implementation that let you confirm the expected output. This is not the case for a code generator! There are infinitely many possible ways to generate a MIPS program that implements the behaviour of a particular WLP4 program.

Marmoset will test your code generator by running the MIPS program you generate with the appropriate MIPS emulator. It will check that the return value in $3 is correct, and that any output produced by the println statement matches the expected output of the WLP4 program.

Output Example

Consider the following input WLP4 program:

int wain(int a, int b) {
  return a+b+241;
}

The MIPS program you generate should assume the two parameters of wain are stored in $1 and $2. One possible correct output for your code generator would be:

add $3, $1, $2
lis $5
.word 241
add $3, $3, $5
jr $31

While this output is a correct MIPS program that returns the value a+b+241 in $3, it may be difficult to write a code generator that produces output like this. Below is another valid MIPS program for this WLP4 program, which may be closer to what your code generator would produce if you are following the conventions from lectures.

; wain prologue
lis $4
.word 4
sw $1, -4($30)
sub $30, $30, $4
sw $2, -4($30) 
sub $30, $30, $4
sub $29, $30, $4 

; wain body
lw $3, 8($29)
sw $3, -4($30)
sub $30, $30, $4
lw $3, 4($29)
add $30, $30, $4
lw $5, -4($30)
add $3, $5, $3
sw $3, -4($30)
sub $30, $30, $4
lis $3
.word 241
add $30, $30, $4
lw $5, -4($30)
add $3, $5, $3

; wain epilogue
add $30, $30, $4
add $30, $30, $4
jr $31

Notice that the code generator produced some comments and blank lines. These do not affect the behaviour of the MIPS program, so you are allowed to add them. They are not required but may make your generated code easier to read and debug.

Here is one more example of a valid possible output, although it is not clear how or why a code generator would produce output like this. It is included just to illustrate that any MIPS program which has the same visible behaviour as the WLP4 program is acceptable output.

add $5, $1, $0
add $7, $2, $0
lis $24
.word 240
lis $1
.word 1
add $14, $5, $7
add $23, $24, $1
add $3, $14, $23
jr $31

A WLP4 program is a valid fragment of C++ code; in fact, aside from the use of new and delete instead of malloc and free, WLP4 essentially only uses features from C. For the most part, you should be able to use your understanding of C and C++ to reason about how WLP4 programs should behave, and therefore how your generated MIPS code should behave.

However, there are some circumstances and edge cases where the behaviour expected by Marmoset for a WLP4 program may be non-obvious or slightly different from C++. These are outlined below.

Unusual or Special WLP4 Implementation Requirements

Testing Your Code Generator

As explained above, there is not a unique "correct output" for this project, which makes testing complicated. To test your code generator, you need to run the generated MIPS code and see if it produces the expected results.

Running Your Generated MIPS Code (basic version, no external libraries)

Generate the MIPS assembly code with your code generator, assemble it with cs241.binasm, then run it with either mips.twoints or mips.array (depending on if the WLP4 program takes two int parameters, or int* and int).

Assuming mips.twoints is used:

./wlp4gen < program.wlp4 > program.asm
cs241.binasm < program.asm > program.mips
mips.twoints program.mips
Or in one line:
mips.twoints <(./wlp4gen < program.wlp4 | cs241.binasm)

Once you implement the println statement, or memory allocation with new and delete, your code generator will need to rely on external libraries that implement the printing and memory allocation procedures. This complicates the process of running your generated code since you now need to use an assembler that supports .import directives and produces MERL, and you need to use a linker to combine the MERL file with the external libraries.

Running Your Generated MIPS Code (with external libraries)

Make sure you have downloaded print.merl and alloc.merl and placed them in the same directory as your code generator.

You will need to use cs241.linkasm to assemble your output into MERL, and then cs241.linker to link the files together. Note that alloc.merl must be the last argument to the linker.

Assuming mips.twoints is used:

./wlp4gen < program.wlp4 > program.asm
cs241.linkasm < program.asm > program.merl
cs241.linker program.merl print.merl alloc.merl > linked.merl
mips.twoints linked.merl
Or in one massive line:
mips.twoints <(cs241.linker <(./wlp4gen < program.wlp4 | cs241.linkasm) print.merl alloc.merl)

There is a reference compiler for WLP4, wlp4c, but it just produces one possible correct MIPS program. You cannot directly compare your code generator's output to the reference compiler's output, but you can run the reference compiler's generated MIPS program to confirm the expected behaviour of the WLP4 program.

Note that wlp4c produces MIPS machine code, while your code generator is expected to produce MIPS assembly language.

Comparing With The Reference Compiler

You can use the reference compiler to compile a WLP4 progam directly to MIPS machine code as follows:

wlp4c < program.wlp4 > reference.mips

You can run the MIPS program generated by the reference compiler with mips.twoints or mips.array to confirm the expected behaviour of the WLP4 program (return value in $3 and output from println). You can then compare it with the behaviour of your own generated MIPS programs.

Doing these comparisons can be awkward and time-consuming because there are multiple steps involved in compiling and running a WLP4 program. The runtests.bash script mentioned below gives you a more automated way of doing this.

Note that unless the WLP4 program does not use the input parameters (and thus always produces the same result when executed), it is not feasible to determine with 100% accuracy whether your MIPS program matches the behaviour of the reference compiler's MIPS program. You can run both MIPS programs with various different inputs and check that the results are the same, but you generally cannot confirm this for all possible inputs.

This essentially means there are two "layers" of testing: you have to come up with WLP4 programs as test inputs for your code generator, but you also need to come up with different inputs to test the generated MIPS programs.

A Bash script called runtests.bash is provided to simplify testing. You create WLP4 programs and input files for the WLP4 programs, and it performs all the steps described above and shows you the test results. See the PDF manual for details on how to use the script.

Stepping Stones

Step 1: Setup

Make sure your parse tree is ready to go. If you did not complete Project 4 and you are using the Preanalyzed Format, follow the instructions in the "Reconstructing the Tree (from Preanalyzed Format)" drop-down at the end of the "Input Formats" section higher up the page. Print out your tree after constructing it to ensure it contains all the correct data (not just the rules/tokens but also type information).

If you haven't already, implement the getChild function from the first step in the Project 4 Stepping Stones. It makes traversing the tree easier.

In this project, you will have to output a lot of MIPS code to standard output. It will be annoying and tedious to do this by writing std::cout << ... << "\n" or (displayln ...) over and over (or whatever output-printing functions you prefer).

We suggest writing one helper function for each MIPS instruction, which simply prints the instruction. Here are some examples in C++.

void Add(int d, int s, int t) { 
  std::cout << "add $" << d << ", $" << s << ", " << t << "\n"; 
}
void Beq(int s, int t, std::string label) { 
  std::cout << "beq $" << s << ", $" << t << ", " + label + "\n"; 
}
void Jr(int s) { 
  std::cout << "jr $"  << s << "\n"; 
}

Some other helpers might be useful too, like for .word directives, or for printing a label definition:

void Word(int i) {
  std::cout << ".word " << i << "\n";
}
void Word(std::string label) {
  std::cout << ".word " + label + "\n";
}
void Label(std::string name) {
  std::cout << name + ":\n";
}

The up-front work of defining these helper functions will be a little tedious, but then in the rest of the code generator, you can write your MIPS code very concisely, like this:

Add(3, 1, 0);
Label("loop");
Beq(3, 2, "end");
Add(3, 3, 1);
Beq(0, 0, "loop");
Label("end");
Jr(31);

Once you have written helper functions for printing MIPS code, define three more helper functions:

Note that you must generate code that stores the constant 4 in $4 before calling push or pop or they will not work as expected.

You are free to define more helpers as you see fit. Another one that might be useful is a helper constant which generates code to store a constant value in $3 (using lis and .word).

Step 2: Accessing Variables

In this step, we will implement code generation for programs that only contain the wain procedure, and don't do anything except return the value of one of wain's local variables, such as this program:

int wain(int one, int two) {
  int three = 3;
  int four = 4;
  return three;
}

For programs that only contain wain, the top of the parse tree has the following form:

start
├─BOF BOF
├─procedures
│ ╰─main
│   ╰─...
╰─EOF EOF
All the code generation work is done in the main node, which corresponds to this rule:
main → INT WAIN LPAREN dcl COMMA dcl RPAREN LBRACE dcls statements RETURN expr SEMI RBRACE

Traverse the tree to access the main node. After finding this node, begin generating a block of code that implements the wain procedure.

Start by generating code that loads the value 4 into $4. This is important to do right at the start of the program, or else your push and pop helpers will not work.

Constructing the Offset Table

The following instructions assume you have attended the lectures or read the course notes and are familiar with the concept of a frame pointer and using precomputed offsets from the frame pointer to access stack-allocated variables. If you do not understand the instructions, review this material before proceeding.

We will use the convention from the lectures and notes that the frame pointer separates the parameters from the non-parameters:

For a procedure with n parameters, the first parameter is at offset 4n. Offsets decrease by 4 for each new variable that you process. So for example, the second parameter is at offset 4n - 4, the third parameter is at 4n - 8, and so on. The first non-parameter variable is at offset 0, the second is at offset -4, the third is at offset -8, and so on.

In the case of wain, there are always exactly two parameters, so the first offset is 8 (4n where n = 2) and each subsequent offset decreases by 4.

Examine the first dcl child of the main subtree. It has this form:

dcl
├─type
│ ╰─...
╰─ID firstParamName

The type subtree is not relevant here. Look at the ID child to get the name of the variable. Add the variable to the offset table, and generate code that pushes $1 to the stack.

Repeat for the second dcl child (the second parameter of wain) except in this case you should generate code that pushes $2 to the stack.

Set the frame pointer by generating an instruction sub $29, $30, $4.

Finally, traverse the dcls subtree. This subtree contains declarations and initializations of non-parameter local variables. Add each variable to the offset table. Instead of pushing $1 or $2 to the stack, load the initial value of the variable into a register and push that value to the stack.

Null Pointer Note: A variable can be initialized to a NUM (an integer) but it can also be initialized to NULL (a null pointer). You should use the constant 1 to represent NULL.

For the example WLP4 program above, the dcls tree looks like this:

dcls
├─dcls
│ ├─dcls
│ ├─dcl
│ │ ├─type
│ │ │ ╰─INT int
│ │ ╰─ID three
│ ├─BECOMES =
│ ├─NUM 3
│ ╰─SEMI ;
├─dcl
│ ├─type
│ │ ╰─INT int
│ ╰─ID four
├─BECOMES =
├─NUM 4
╰─SEMI ;

Ensure you process the declarations in the correct order. You should process the declaration of "three" by descending into the inner "dcls" tree, then afterwards process "four". The initial value of "three" is 3, and the initial value of "four" is 4, which you can determine by examining the relevant NUM child.

You should now have a complete offset table for the local variables of wain. For the example program above, it would look like this:

Name Offset
one 8
two 4
three 0
four -4

Generating Code For The Return Expression

In this stepping stone, we are assuming that the wain procedure doesn't contain any statements, and doesn't have any complex expressions involving arithmetic or other operations – it simply returns one of its local variables.

Thus, generating code for the return expression is straightforward. Traverse the expr subtree of the main node to figure out which variable is returned, and generate a lw instruction that loads the variable's value from the appropriate offset and places it in $3, the return register.

For our example program, the expr subtree looks like this:

expr
╰─term
  ╰─factor
    ╰─ID three

Your code generator might process this tree as follows:

Cleaning Up The Stack And Returning

Call your pop() helper one time for each local variable that you pushed, to clean up the stack. This is technically not required for wain, but it is a good practice as it is required for other procedures.

Then, end the program with jr $31!

The following steps do not provide as much detail as Step 2. Our hope is that with the foundation from Step 2, you can complete the rest of the code generator with less guidance. Part of the fun of code generation is figuring out for yourself how to handle different language features!

Disclaimer: All "difficulty ratings" provided below are random guesses and may not end up corresponding to your experience.

These three steps can be completed in any order.

[Difficulty: ★ ★ ☆ ☆ ☆] Step 3A: Arithmetic Expressions With Integers

Implement support for the following rules:

expr → term
expr → expr PLUS term
expr → expr MINUS term
term → factor
term → term STAR factor
term → term SLASH factor
term → term PCT factor
factor → ID
factor → NUM
factor → LPAREN expr RPAREN

In other words, your code generator should support arithmetic expressions involving variables, numbers, addition, subtraction, multiplication, division, modulo, and parentheses.

Use the technique from the lectures and notes of using the stack to store temporary expression values.

Assume that all expressions have integer type (pointer arithmetic is implemented in Step 4A).

[Difficulty: ★ ★ ★ ☆ ☆] Step 3B: Basic Assignment Statements

Implement support for the following rules:

statements → statements statement  
statements →
statement → lvalue BECOMES expr SEMI
lvalue → ID  
lvalue → LPAREN lvalue RPAREN

In other words, your code generator should support assignment statements with a variable on the left-hand side.

Note that the variable can be optionally wrapped in parentheses, like ((x)) = y; This doesn't change the meaning of the statement, it's just a consequence of the fact that you can add parentheses wherever you want.

[Difficulty: ★ ★ ☆ ☆ ☆] Step 3C: Println Statement

Implement support for the following rules:

statements → statements statement
statements →
statement → PRINTLN LPAREN expr RPAREN SEMI

In other words, your code generator should support println statements. The println function prints the value of the given expr as a base 10 (decimal) integer, followed by a newline.

One valid way of implementing this is to just take your entire solution to Question 6 and make your code generator output this print procedure alongside the rest of your generated code. When you encounter a println statement, call your print procedure.

However, complicated procedures like this are often compiled separately and linked externally. We provide this as an option for implementing println (and it is the only option you can use if you have not done Question 6).

If you choose the linking option, download the print.merl library and follow the instructions for "Using the print.merl library" in the Resources section at the top of the page.

The following three steps can be completed in any order, but depend on earlier steps.

[Difficulty: ★ ★ ☆ ☆ ☆] Step 4A: Pointer Arithmetic (requires Step 3A)

Extend your support for the following rules to handle expressions of pointer (int*) type.

expr → expr PLUS term
expr → expr MINUS term

Additionally, you should implement support for the rule factor → NULL if you have not already (allowing NULL to appear in expressions).

You will need to update your handling of addition and subtraction to check the types of the left and right subexpressions. Depending on which expressions have int* type, you will need to modify the code you output.

You will likely need to implement Step 4B as well before you can write test programs that do meaningful things with pointer arithmetic, and test Steps 4A and 4B together.

[Difficulty: ★ ★ ★ ☆ ☆] Step 4B: Pointer Operations & Assignment (requires Step 3B)

Implement support for the following rules:

factor → NULL
factor → AMP lvalue
factor → STAR factor
lvalue → ID  
lvalue → STAR factor
lvalue → LPAREN lvalue RPAREN

In other words, your code generator should support the use of NULL in expressions, the pointer dereference operator, and the address-of operator. Additionally, assignment statements should permit not only assigning to a variable, but also a dereferenced pointer expression.

There are two different cases for pointer dereference, depending on if it occurs in an lvalue context or not. By "an lvalue context" we mean the dereference is occuring on the left-hand side of an assignment, or immediately following an address-of operator. In an lvalue context, the generated code should produce the address being dereferenced, while in other contexts, the generated code should produce the value at the address being dereferenced.

For address-of and assignment, the generated code depends on what type of lvalue you are taking the address of or assigning to. Review the lectures or notes for guidance.

Once you have implemented both Step 4A and Step 4B, you can write programs that freely manipulate the elements of an array. Without Step 4A, you can only really manipulate individual variables, or the first element of an array.

[Difficulty: ★ ★ ★ ★ ☆] Step 4C: Control Structures (Steps 3A, 3B, and 3C recommended)

Implement support for the following rules:

statements → statements statement
statements →
statement → IF LPAREN test RPAREN LBRACE statements RBRACE ELSE LBRACE statements RBRACE
statement → WHILE LPAREN test RPAREN LBRACE statements RBRACE
test → expr EQ expr  
test → expr NE expr  
test → expr LT expr 
test → expr LE expr  
test → expr GE expr  
test → expr GT expr

In other words, your code generator should support if statements and while loops. Note that both these control structures can have more statements (including additional if statements and while loops) nested inside them! Be sure to test with nested structures.

The generated code for the comparison tests should compute the values of the left and right expressions, then perform a comparison between them that produces a boolean value (0 or 1).

Note that if the expressions have pointer (int*) type, you should use unsigned comparisons, that is, use sltu instead of slt. This is because pointers (memory addresses) cannot be negative and using slt may produce wrong results for large addresses.

Once comparisons are done, think about how to replicate the behaviour of an if statement or a while loop in MIPS assembly using branch instructions and labels. Have your code generator output MIPS code that implements the control structure, with appropriate calls mixed in to generate code for the comparison test and the nested statements.

The final issue to solve is the fact that all label names used in your generated code have to be unique. A suggestion is to write a helper function called generateLabel; each time you call it, it returns a unique label name that is guaranteed to have never been used before in your code. You could implement this by, for example, having the function increase a global counter every time it is called (and this global counter is not modified by any other function).

The reason we suggest completing Steps 3A, 3B, and 3C first is because it will enable you to write much more interesting test programs involving if statements and while loops:

The following step, implementing new and delete, can technically be completed at any time after Step 2. However, you can't really write interesting test programs that use new and delete without implementing Steps 4A and 4B, because you need pointer arithmetic and assignment-to-deference to actually modify the arrays you allocate. Step 4C is also helpful because it lets you write programs that loop over an array, or check (with if) whether an allocation failed or succeeded.

[Difficulty: ★ ★ ★ ☆ ☆] Step 5: Memory Management (Steps 4A, 4B, and 4C recommended)

Implement support for the following rules:

factor → NEW INT LBRACK expr RBRACK
statements → statements statement
statements →
statement → DELETE LBRACK RBRACK expr SEMI

In other words, your code generator should support allocating memory with new in expressions, and deallocating memory with delete in statements.

Writing your own memory allocation routines is tricky, and writing them in MIPS assembly is even harder. So, we provide memory allocation routines through an external library, which you can link your generated code with.

Download the alloc.merl library and follow the instructions for "Using the alloc.merl library" in the Resources section at the top of the page. It is similar to using the print.merl library for println, but the instructions are more complicated and have more steps. Read and follow the instructions carefully.

Pay particular attention to:

The following step can be completed at any time after Step 2, but it is probably the hardest part of the code generator to implement correctly. Depending on what kind of person you are, you may wish to do it early, or you may wish leave it until the end.

[Difficulty: ★ ★ ★ ★ ★] Step ??: Non-Wain Procedures

Implement support for the following rules:

procedures → procedure procedures
procedure → INT ID LPAREN params RPAREN LBRACE dcls statements RETURN expr SEMI RBRACE
params →
params → paramlist
paramlist → dcl
paramlist → dcl COMMA paramlist
factor → ID LPAREN RPAREN
factor → ID LPAREN arglist RPAREN
arglist → expr
arglist → expr COMMA arglist

In other words, your code generator should support declaring procedures other than wain, which can have an arbitrary number of parameters of arbitrary types. Additionally, it should support procedure calls occurring in expressions.

Note that in a WLP4 program, wain is always the last procedure, but it is the first procedure to actually be executed at runtime because it is the entry point for the program. Ensure your generated code is set up so that wain is executed first.

Some notes about procedures:

Calling Procedures

There are many ways to implement procedures in a code generator. Generally one defines a calling convention, describing how arguments are passed to a procedure and how values are returned from a procedure, and the implementation must adhere to this convention consistently. Other aspects of a calling convention include which registers are preserved by a procedure call.

Our suggested calling convention in this course is to pass all parameters on the stack. The procedure, once called, sets up its frame pointer and retrieves the parameters using positive offsets from the frame pointer. Values are returned in $3 (which conveniently is where our code generator places expression results). In terms of preserving registers, each procedure is responsible for preserving its own frame pointer ($29) and return address ($31); for other registers it is your decision if and how to preserve them.

For a call with arguments (the rule factor → ID LPAREN arglist RPAREN), these are the appropriate steps to implement this calling convention:

A call with no arguments (the rule factor → ID LPAREN RPAREN) is similar, but the steps about pushing and popping arguments are omitted.

Generating Code For Procedures

Generating code for each procedure itself (the nodes corresponding to the rule procedure → INT ID LPAREN params RPAREN LBRACE dcls statements RETURN expr SEMI RBRACE) is similar to generating code for wain.

You may want to focus first on getting procedures with no parameters and calls with no arguments working. Once you are confident in your implementation, move on to procedures with parameters and calls with arguments.

Bonus: Compiler Optimization Challenge

You can earn bonus marks (+0.25% on your final course grade) by reducing the size (measured in bytes of machine) of the code generated by your compiler.

(The goal of compiler optimization is usually to produce faster code, rather than smaller code, but accurately measuring the speed of your generated code is difficult due to the fact that we are using MIPS emulators instead of real hardware. Thus, we use size as a very rough approximation.)

The P5Bonus project on Marmoset will run your compiler against a series of hidden test cases involving large, complex, and unusual programs. If you pass all the test cases, the total size in bytes of the code you generated for the test cases is displayed.

If your total size in bytes is 180,000 bytes or less, you will earn the bonus marks.

An anonymized scoreboard is available showing the scores (byte counts) of all students who have passed the bonus test cases (even if your score is not under 180,000 bytes). The scoreboard is updated automatically every 5 minutes.

Notes:

Hint: The code generation techniques discussed in class make heavy use of the stack, and each push/pop uses two instructions. Optimizations that reduce the number of pushes/pops, such as register allocation, will be very effective and you should consider implementing them first. In fact, a good register allocator alone may be enough to get under the 180,000 byte threshold for the bonus marks.

After the Project 5 deadline, you can no longer earn bonus marks, but the scoreboard will continue to update. A score of under 100,000 bytes is very good. Only a handful of students have scored under 70,000 before.