Project 2

Deadline Monday, October 23, 11:59pm
Name on Marmoset P2
To Submit Standard Format: asm.cc OR asm.rkt
Prescanned Format: asm-prescanned.cc OR asm-prescanned.rkt
Marking Scheme 30 marks for release tests, 70 marks for secret tests

MIPS Assembler

In this project, you will write a MIPS assembler, that is, a program which converts MIPS assembly language into MIPS machine language.

Your program should be equivalent in functionality and behaviour to the course tool cs241.binasm. It should produce identical machine language output, and it should detect the same errors (though the error messages do not have to be identical).

Your assembler should be reasonably efficient. It does not need to be highly optimized but it should not take quadratic time in the size of the input.

Important Reminders: Secret tests, C++ notes, Racket notes

Input & Output

There are two input formats for this assignment.
Which format should I use?

If you did not complete Part A of Project 1, you will need to use the Prescanned Format. Otherwise, you can choose which you prefer. The marking scheme is identical for both formats, and the Standard Format is arguably the more difficult option, since you need to ensure your scanner does not have bugs. The main reasons to choose the Standard Format are:

The bottom section of this page gives a detailed specification of MIPS assembly language, describing the requirements for a valid MIPS assembly program. Anything that does not follow these requirements is invalid.

Input/Output Example: Valid Program

Given the following input, in Standard Format:

lis $3
.word 241
jr $31
Or in Prescanned Format:
ID lis
REGISTER $3
NEWLINE
DOTID .word
DECINT 241
NEWLINE
ID jr
REGISTER $31
NEWLINE
Run your program to generate a file containing the machine language output. (For Racket, replace ./asm with racket asm.rkt.)
./asm < input.asm > output.mips
The output is machine language, so examining it directly on your terminal (which will try to interpret it as ASCII characters) will not generally be productive. To verify the output is correct, you could use a hex dump command like xxd, the course tool cs241.binview. a graphical hex editor, or whatever other method you prefer for examining the binary data in a file. (Think back to how you solved Question 1.)

Note that you can run cs241.binasm with the Standard Format input to produce an expected output file:

cs241.binasm < input.asm > expected.mips
You can then examine this file through whatever means you prefer to see what the output is supposed to look like, or you could directly compare it to the output you produced using the diff command.
Input/Output Example: Invalid Program

Given the following input, in Standard Format:

jar $31
Or in Prescanned Format:
ID jar
REGISTER $31
NEWLINE
Your program should print an error message containing ERROR in ALL CAPS to standard error. For example:
./asm < input.asm > output.mips
ERROR: Invalid instruction jar
The error message does not need to be informative, but it is recommended you make them informative, as this will help with debugging. The error message also does not need to match the ones produced by cs241.binasm. It just needs to contain ERROR and be printed to standard error. For error tests, it is okay to produce partial output on standard output as long as you properly produce an error.

Stepping Stones

You may find the following steps useful if you are having trouble figuring out where to start.

It is recommended you submit to Marmoset after completing each stepping stone! In this project, you can earn part marks for an incomplete program.

Step 1: Supporting .word Directives

First, write code to process the scanned input line by line.

If using the Prescanned Format, create a struct or class for tokens that contains a kind and lexeme field. Read the tokens from the input and separate the input into lines (sequences of tokens). For example, maybe you could create a vector of vectors of tokens, where the inner vectors are individual lines, and the outer vector represents the whole program. When you encounter a NEWLINE token, this indicates the end of the current line and the start of a new line.

If using the Standard Format together with your own scanner, you have a little more flexibility, but in either case, set up your program so you can read the input line by line, and examine the tokens on each line.

Now, implement support for the .word directive, ignoring labels for now (so only decimal and hexadecimal integers are allowed as the operand).

For each line, first make sure the line is non-empty (it has at least one token on it). If the line is empty, just skip it.

Otherwise, check if the first token is a DOTID token with lexeme .word. If so, verify that the line has exactly one other token on it, and that this token is a DECINT or HEXINT token. If these conditions are not met, produce an error.

If the line is valid, convert the DECINT or HEXINT token into a numeric value and output the 32-bit binary encoding of this value. Repeat until all lines are processed.

Step 2: Supporting Labels

Supporting labels requires structuring your assembler to perform two passes over the input, so it's best to add support for labels early on. This way you won't have to make major structural changes later on when your assembler is more complicated.

Split up your loop from Step 1 that processes the scanned lines into two loops. Move the part of the code that checks for syntax errors to the first loop. Move the part of the code that outputs the 32-bit binary encoding to the second loop.

Now add support for labels in .word directives.

Step 3: The add, sub, slt, and sltu Instructions

First implement support for the add instruction. Check the instruction syntax in the first loop, and output the encoding in the second loop.

The other three instructions all have the same syntax as add: they take three register operands. The only difference is that the function bits in the machine language encodings are different for each instruction.

Think about how to structure your code to handle these instructions in a uniform way (as opposed to just copying and pasting your add code and changing it).

Remember to do error checking. Each of the instructions should have exactly three operands, which should be REGISTER tokens, and the operands should be separated by COMMA tokens. Any deviation from this is a syntax error.

Step 4: The beq and bne Instructions

Once you've figured out how to implement one instruction in your assembler, most of the remaining instructions will be easier. However, the branch instructions are a little more difficult to handle than something like add:

A lot of things are different between add and branch instructions. How much of your code can you reuse between instructions with different syntax, encodings, and error checking requirements? Think about the overall design of your assembler before you move on to the last step.
Step 5: The Rest of the Assembler

Implement support for the remaining instructions.

Most of the remaining instructions are similar to add, but with different numbers and positions of the register operands. Pay attention to the positioning when encoding.

The memory access instructions lw and sw are like beq and bne in terms of encoding. However, the syntax is totally different. You fortunately don't need to support label operands for lw and sw, but you do need to do range checking on the immediate operand.

MIPS Assembly Language Specification

This specification describes the structure of a valid MIPS assembly program. Refer to the MIPS Reference Sheet for the machine language encodings of each instruction.

A MIPS assembly language program is a text file containing a sequence of lines. Each line has the general format:

labels instruction comment
Each of these components – labels, instruction and comment – is optional; a particular line may have all three, any two, any one, or none at all. The components, if they appear, must appear in the given order, e.g., labels cannot come after the instruction on a line.

A line is called null if the instruction component is absent. There is a one-to-one correspondence between non-null lines and 32-bit words in the machine language output of the assembler. That is, each non-null line translates to a word, and nothing is output for null lines.

In the following, we describe what each component looks like after the line has been scanned into tokens. For quick reference, the specification of MIPS tokens is reproduced below.

If a scanning failure or other error occurs when performing scanning using the simplified maximal munch algorithm and the token specification below, the MIPS program is invalid.

MIPS Assembly Token Specification

Kind Description of Associated Lexemes
ID Identifiers: Strings starting with an alphabetic character (a-z or A-Z), followed by zero or more alphanumeric characters (a-z or A-Z or 0-9).
DOTID Dot-Identifiers: An . character followed by an ID lexeme.
LABELDEF Label Definitions: An ID lexeme followed by a : character.
DECINT Decimal Integers: Strings which optionally start with a - character, followed by one or more decimal digits (0-9).
HEXINT Hexadecimal Integers: Strings starting with 0x, followed by one or more hexadecimal digits (0-9 or a-f or A-F).
REGISTER Registers: A $ character followed by one or more decimal digits (0-9). The numeric value of the string of decimal digits cannot exceed 31.
COMMA Comma: A , character.
LPAREN Left Parenthesis: A ( character.
RPAREN Right Parenthesis: A ) character.
NEWLINE Either a line feed character (ASCII 0x0a), or the two-character sequence carriage return, line feed (ASCII 0x0d, ASCII 0x0a).
When printing this token, do not print the lexeme. Print a line containing only the kind.
?WHITESPACE Whitespace: A sequence of one or more space (ASCII 0x20) or tab (ASCII 0x09) characters.
?COMMENT Comment: A ; character, followed a sequence of zero or more ASCII characters that does not contain a line feed (ASCII 0x0a) or carriage return (ASCII 0x0d).
(In other words, once a ; character is encountered, everything up to the next line feed or carriage return is a ?COMMENT.)

Restrictions:

Labels Component

The labels component is a sequence of one or more LABELDEF tokens. For example:
label: cs241: HelloWorld:
Each LABELDEF token defines a label whose name is everything up to, but not included, the colon : at the end of the lexeme. The above example defines three labels called label, cs241, and HelloWorld.

All label names must be unique. The same name cannot be used twice.

Comment Component

The comment component consists of a ?COMMENT token. For example:
; to be or not to be
These tokens are filtered out by the scanner, so after scanning is complete, no extra work is needed to deal with comments.

Each label has an associated address, which is 4 times the number of non-null lines preceding the line with the label definition.

Instruction Component

The instruction component starts with either an ID or DOTID token. If it is an ID token, the lexeme must be one of the 17 supported instructions:
add sub mult multu div divu mfhi mflo lis slt sltu jr jalr beq bne lw sw
If it is a DOTID token, the lexeme must be .word.

Following the first token is a sequence of tokens representing operands for the instruction/directive. Operands in general can be:

However, each instruction has its own requirements for operands. Operands are typically separated by punctuation tokens: COMMA, LPAREN, or RPAREN.

The expected sequence of operand and punctuation tokens for each instruction/directive is given below. An expression in square brackets like [DECINT or HEXINT] means a single token is expected, but it can be one of several kinds.

Three Register Operands

add  $d, $s, $t
sub  $d, $s, $t
slt  $d, $s, $t
sltu $d, $s, $t
The expected sequence of tokens after the ID is:
REGISTER COMMA REGISTER COMMA REGISTER

Two Register Operands

mult  $s, $t
multu $s, $t
div   $s, $t
divu  $s, $t
The expected sequence of tokens after the ID is:
REGISTER COMMA REGISTER

One Register Operand

mfhi $d
mflo $d
lis  $d
jr   $s
jalr $s
The expected token kind after the ID is:
REGISTER

Note that jr and jalr are encoded differently from mfhi, mflo and lis: for the jump instructions, the single register operand is $s, while for the others, it's $d.

Branch Instructions (Two Registers, One Immediate)

beq $s, $t, i
bne $s, $t, i
The expected sequence of tokens after the ID is:
REGISTER COMMA REGISTER COMMA [DECINT or HEXINT or ID]

If the immediate operand i is an ID, it must be the name of a label defined elsewhere in the program. The label is converted to a numeric value as follows: let address be the address of the label, and let PC denote the address of the line after the branch instruction (that is, 4 times the number of non-null lines up to and including the line with the branch instruction). Then the numeric value is (address-PC)/4.

The immediate operand i must be encoded as a 16-bit two's complement integer, so it has the following restrictions:

Memory Access Instructions (Register, Immediate, Register)

lw $t, i($s)
sw $t, i($s)
The expected sequence of tokens after the ID is:
REGISTER COMMA [DECINT or HEXINT] LPAREN REGISTER RPAREN

The immediate operand i must be encoded as a 16-bit two's complement integer, so it has the following restrictions:

Word Directive

.word i
The expected token kind after the DOTID is:
[DECINT or HEXINT or ID]

If the immediate operand i is an ID, it must be the name of a label defined elsewhere in the program. The numeric value corresponding to the label is simply the address of the label.

Encoding Instructions and Directives

Encoding is done after replacing all label operands with their corresponding numeric values.

Each line containing an instruction should be encoded as a 32-bit word, using the machine language encodings on the MIPS Reference Sheet.

Each line containing a .word directive is encoded a 32-bit word as follows, depending on the kind of operand.