Lab #7: Compiling Simple Expressions

Assigned: Thu 18 Nov 2002
Due: ?? May 2002


The problem description

You should write a compiler that reads in an infix expression in the simple language we have been working on for the last couple of labs. Your compiler should produces PC-231 assembly code (or any other assembly or machine code of your choice) that will evaluate the expression when run. If you use PC-231 assembly language, you may use my assembler (asm231) to convert the assembly code into machine language and my simulator (sim231) to evaluate the resulting machine code (these are command-line calls on Gemini and Hudson).

If you use any other assembly or machine language, you should have a simulator (or chip!) available from the Collins 411 lab on which to run demos.

You should already have a parser and static analyzer (type and variable-initialization checker) written from the last two assignments. These should produce a type-decorated tree (for valid inputs) which you can traverse in order to produce assembly code (this is the "back end" of the compiler). I give detailed hints on how to approach the problem below.

It may be difficult to complete the compiler for the whole language all at once. I recommend taking a progressive approach, so that you implement only a few language features at a time. Adding fancier features in "layers" will allow you to keep a more-or-less complete working program at various checkpoints along the way, so that you have something to turn in should a deadline arrive.

See this section of Lab 2 for the expression grammar and this section of Lab #5 for typing rules.

More informal semantics

We took a brief look at informal semantics for each of the different expression forms in Lab #2. Here is a more detailed description of each, along with some remarks relevant to code generation (all remarks are made under the assumption that you'll use the PC-231 for your compilation target):

Some sample expressions

Here is a set of sample expressions you might want to test your compiler on, along with the results they should produce. Note that several of the expressions involve the READ construct, and therefore depend on the values that a user types in at run time.

Code generation

Code generation is largely a process of traversing the parse tree in a bottom-up, right-to-left order, producing code to implement each of the operators, literals or variable references encountered along the way.

You will also need some general strategies for handling registers and RAM locations. In order to generate code for an integer literal, you will need to put it into some register, and you will need to know which ones are free for this purpose. Assuming that register R3, for example, is free, you might generate code like this for the integer literal 5:

You will also need to know which register the result of a given sub-tree is stored in, so that the node above it can access it properly. For example, if the integer literal 5 from above occurs as a sub-tree of an addition operator node, you will want to generate code such as the following:
	DATA 5		; from above
	COPY DR,R3	; from above
	ADD R2,R3	; assuming other argument was in R2
			; result of the "+" node is in R3
			; and now R2 is free again
As mentioned in above, you can use a free register list in order to keep track of register usage. This list can then be modified as you pass from node to node in the tree. In the example above, you would remove R3 from the list as you passed through the node for the integer literal 5, and then add R2 back into the free list after passing through the addition operator node.

It is possible that a straight-forward use of registers will leave you with no free registers at certain points in the process, especially for larger expressions. If this happens, make sure you are using the free list properly, but you need not go the length of "spilling" registers out into RAM locations. In other words, it is OK if your algorithm fails for some large expressions, as long as you are doing your best to free registers as you go.

Variables can be handled by setting aside a RAM location for them and then loading them into a register and proceeding as above (i.e., noting the register used in the node somehow). For example, an expression such as "x=(x+2)" might compile as follows:

			; code for int lit "2"
	DATA 2		; int literal 2
	COPY DR,R0	; R0 now in use
			; code for var "x"
	DATA #x		; label for var x storage
	LOAD R1,DR	; load x using DR indirectly
			; code for op "+"
	ADD R0,R1	; free up R0, result in R1
			; code for assignment to "x"
	DATA #x		; set up for store using DR
	STORE R1,DR	; store x, result in R1
	. . .		; etc., etc.
	HALT		; end of program
#x	CONST 0		; placeholder for x
#y	CONST 0		; placeholder for y
	. . .		; etc., etc.

Handling the multiplication operation will be a bit tougher than the rest: you will need to jump out of the main line of evaluation to a "subroutine" which implements multiplication for you, and then jump back when it is finished. I have a multiplication routine in this file which you can use to get started, but note that you will need to do some extra work to make sure that you have left registers for the multiplier to use, a register for the result, and a couple of jump registers for the "call" and "return".

The various other kinds of expressions are much easier to handle. You should probably try generating machine code for some simple expressions by hand until you are comfortable with the techniques before trying to code it up for the compiler. I will add some more detailed hints on code generation strategies for the various expression types, including multiplication, later in the week.

At the root of your tree, you should remember to generate a WRITE instruction which will print out the final value you have computed. Forgetting this step leads to a disappointing anti-climax in which you may have computed the right value, but can't tell, since it never gets printed!