| Deadline |
Tuesday, December 5, 11:59pm |
| Name on Marmoset |
P5 Bonus component: P5Bonus |
| To Submit |
Standard Format: wlp4gen.cc OR wlp4gen.rktPrescanned Format: wlp4gen-prescanned.cc OR wlp4gen-prescanned.rktPreanalyzed 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 |
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.
We provide the following resources.
print.merl
which exports a procedure called print.
This library can be used to implement the functionality of the println statement.
See the instructions below for how to use the library.alloc.merl
which exports three procedures called init, new, and
delete. This library can be used to implement the functionality of the new operator
(memory allocation)
and the delete statement (memory deallocation). See the instructions below for how to use the library.
runtests.bash
which can be used to more easily test your code generator.
See the
PDF manual
for usage instructions.
The library exports a procedure called
The
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
Assemble your generated code with
The library exports three procedure called
The
The
The
Note that your code generator is expected to return
The
Note that in WLP4, deleting a
The
The procedures are called like any other MIPS procedure, by using
Assemble your generated code with
Note that Using the
print.merl libraryprint.
To use this procedure, your generated MIPS assembly code must include the directive .import print
at the top of the file.
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.
jalr.
Remember to save $31 before making the call, as jalr will modify $31.
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 libraryinit, 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
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.
init procedure expects two parameters, in $1 and $2.
wain has type int*, meaning the MIPS program is to
be run with mips.array, pass the starting address of the input array in $1 and
the size of the input array in $2. Note that these are the default values stored in $1 and $2
when using mips.array. In other words, to pass the correct parameters,
it suffices that you do not modify $1 or $2 before calling init.
wain has type int, meaning the MIPS program is to
be run with mips.twoints, pass the value 0 in $2 (indicating that there is no input array).
The value in $1 does not matter in this case.
Allocation (New)
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:
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)
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.
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
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.
jalr.
Remember to save $31 before making the call, as jalr will modify $31.
Assembly & Linking
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.
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.
There are three input formats. Which ones you can use depends on which previous projects you have completed.
wlp4scan, which is a sequence of tokens for the WLP4 program.
wlp4scan,
and then through
wlp4parse,
and finally through
wlp4type.
The result is a text representation of the WLP4 program's parse tree, with annotations
indicating the type (int or int*) associated with certain nodes.
This format is also known as a WLP4 Typed Intermediate (.wlp4ti) file.
Your tree should have five string fields:
rule, which is only used in nonterminal nodes, and stores the CFG rule used to expand the node.lhs, which is only used in nonterminal nodes, and stores the left-hand side of the CFG rule.kind and lexeme, which are only used in terminal nodes, and store the kind and lexeme
of the token, respectively.
type which stores the type of the node (int or int*) if the node has a type
(otherwise, it just stores an empty string).
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.
stringstream from the line; in Racket, you can use string-split to split the line on whitepace.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).lhs field, and append this
part to the rule field..EMPTY, the CFG rule has an empty right-hand side, and there is no type annotation.
Break and continue to the next step.":"), 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.rhs vector.
Append a space to the rule field, and then append the symbol.
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.
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.
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.
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.
Consider the following input WLP4 program:
The MIPS program you generate should assume the two parameters of While this output is a correct MIPS program that returns the value
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.
Output Example
int wain(int a, int b) {
return a+b+241;
}
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
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
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
NULL should be represented by the value 1, rather than 0. If NULL is deferenced,
your generated MIPS program must crash with an error (Marmoset will actually explicitly check
that your program crashes).new fails to allocate an array of the desired size, it should return NULL, that is, 1. (In C++,
this would throw an exception, but WLP4 does not have exceptions.) Note that the provided new
procedure will actually return 0 on a failed allocation.
Your generated code should detect this and return NULL (1) instead.
delete is called on NULL, nothing should happen.
(This is actually how C++ behaves too.) Note that
the provided delete procedure will crash if it is passed the value 1, so your
generated code should detect this case and actually avoid making the call.
add and sub instructions in MIPS, and
when you use mult followed by mflo, so you don't need to do anything special to ensure
this happens.
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.
Generate the MIPS assembly code with your code generator,
assemble it with
Assuming Running Your Generated MIPS Code (basic version, no external libraries)
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).
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
Make sure you have downloaded
You will need to use
Assuming 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)
print.merl
and
alloc.merl
and placed them in the same directory as your code generator.
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.
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
You can use the reference compiler to compile a WLP4 progam directly to MIPS machine code as follows:
You can run the MIPS program generated by the reference compiler
with
Doing these comparisons can be awkward and time-consuming because there are multiple steps involved in compiling and running
a WLP4 program.
The
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.
wlp4c produces MIPS machine code,
while your code generator is expected to produce MIPS assembly language.
Comparing With The Reference Compiler
wlp4c < program.wlp4 > reference.mips
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.
runtests.bash script mentioned below gives you a more automated
way of doing this.
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.
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:
push(s) outputs the following two-instruction sequence (push the value in $s to the stack):
sw $s, -4($30) sub $30, $30, $4
pop(d) outputs the following two-instruction sequence (pop the top item of the stack into $d):
add $30, $30, $4 lw $d, -4($30)
pop() with no argument outputs the following instruction (pop the top item of the stack and discard it):
add $30, $30, $4
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).
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 EOFAll 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.
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 |
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:
expr → term by recursively generating code for term.term → factor by recursively generating code for factor.factor → ID by looking up the ID's lexeme in the offset table,
which for this example returns offset 0. Output the instruction: lw $3, 0($29)
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.
Implement support for the following rules:
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).
Implement support for the following rules:
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
Implement support for the following rules:
In other words, your code generator should support
One valid way of implementing this is to just take your entire solution to
Question 6
and make your code generator output this
However, complicated procedures like this are often compiled separately and linked externally.
We provide this as an option for implementing
If you choose the linking option,
download the
[Difficulty: ★ ★ ☆ ☆ ☆] Step 3A: Arithmetic Expressions With Integers
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
[Difficulty: ★ ★ ★ ☆ ☆] Step 3B: Basic Assignment Statements
statements → statements statement
statements →
statement → lvalue BECOMES expr SEMI
lvalue → ID
lvalue → LPAREN lvalue RPAREN
((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
statements → statements statement
statements →
statement → PRINTLN LPAREN expr RPAREN SEMI
println statements. The println
function prints the value of the given expr as a base 10 (decimal) integer, followed by a newline.
print procedure alongside the rest of your generated code.
When you encounter a println statement, call your print procedure.
println (and it is the only option you can use if you have
not done
Question 6).
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.
Extend your support for the following rules to handle expressions of pointer (
Additionally, you should implement support for the rule
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
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.
Implement support for the following rules:
In other words, your code generator should support the use of
There are two different cases for pointer dereference, depending on if it occurs in an
For address-of and assignment, the generated code depends on what type of
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.
Implement support for the following rules:
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 (
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
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:
[Difficulty: ★ ★ ☆ ☆ ☆] Step 4A: Pointer Arithmetic (requires Step 3A)
int*) type.
expr → expr PLUS term
expr → expr MINUS term
factor → NULL if you have not already (allowing
NULL to appear in expressions).
int* type, you will need to modify the code you output.
[Difficulty: ★ ★ ★ ☆ ☆] Step 4B: Pointer Operations & Assignment (requires Step 3B)
factor → NULL
factor → AMP lvalue
factor → STAR factor
lvalue → ID
lvalue → STAR factor
lvalue → LPAREN lvalue RPAREN
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.
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.
lvalue you are taking the address of
or assigning to. Review the lectures or notes for guidance.
[Difficulty: ★ ★ ★ ★ ☆] Step 4C: Control Structures (Steps 3A, 3B, and 3C recommended)
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
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.
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 following step, implementing
Implement support for the following rules:
In other words, your code generator should support allocating memory with
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
Pay particular attention to:
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)
factor → NEW INT LBRACK expr RBRACK
statements → statements statement
statements →
statement → DELETE LBRACK RBRACK expr SEMI
new in expressions,
and deallocating memory with delete in statements.
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.
init and the fact that there are different requirements
depending on the type of the first parameter of wain.
new fails to allocate (return NULL, not 0).
delete is called on NULL (do nothing, don't crash).
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.
Implement support for the following rules:
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,
Some notes about 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
A call with no arguments
(the rule
Generating code for each procedure itself (the nodes corresponding to the rule
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.
[Difficulty: ★ ★ ★ ★ ★] Step ??: Non-Wain Procedures
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
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.
else1 (a perfectly legal WLP4 procedure name)
the label you generate
could be Pelse1: (P for procedure). This will avoid conflicts
with the labels you generate for if statements and while loops (as long as they
don't also start with P).wain's.
At runtime, each procedure call has its own frame pointer
that it uses to access its local variables using the offsets.
When a procedure p calls a procedure q, to avoid losing track of its
frame pointer, the procedure p saves $29 on the stack, and restores it once q
returns.Calling Procedures
factor → ID LPAREN arglist RPAREN), these are the appropriate
steps to implement this calling convention:
arglist and push each expr argument to the stack,
in left-to-right order.
jalr.
factor → ID LPAREN RPAREN) is similar, but the steps
about pushing and popping arguments are omitted.
Generating Code For Procedures
procedure → INT ID LPAREN params RPAREN LBRACE dcls statements RETURN expr SEMI RBRACE)
is similar to generating code for wain.
params and dcls. The process is very much like
for wain, except:
wain always has 2).
params subtree to build the offset table,
but do not push anything to the stack (the caller pushes the parameters
to the stack).
statements and the return expr.
jr $31.
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:
print.merl and alloc.merl, the size of those libraries is not included in your byte count.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.