How a Compiler Works

See all articles from T-S Horizons n7

A compiler is a program that takes a high level language like BASIC and converts it to machine code. The miracle of a compiler is that people who just have a slight knowledge of machine code can write complex programs that execute much faster than ordinary BASIC programs.

The compiler I have written is itself a BASIC program. It cannot convert itself, however, to machine code, because only certain commands are handled by the compiler and many other commands beside these are used in the process.

The machine code that is produced is simple to understand. The Z80 has over 700 instructions and the compiler probably uses less than 20 of them. In fact, the whole process of compiling is more a matter of record keeping than anything else. I’ve compiled BASIC for years by hand with pencil and paper.

The ability of the compiler to keep track of things really becomes apparent when you consider instructions like “GOTO 100”. In assemble language this might look like “JP 64000”, which is a simple instruction that just takes three bytes. I say “might” because it is a tricky business to know where line 100 would be in memory. It might not be a location 64000, but instead at 62000 or 60000 etc.

To solve this problem the compiler makes two passes through, the BASIC, or to put it another way, it compiles the program twice. The first time through it remembers where each line is compiled to in memory. The second time through it fills in the “JP” instructions with appropriate addresses.

But what about the actual BASIC instructions such as LET, POKE, PRINT etc?

An instruction like…

LET A=B

…is easy to translate to machine code and would look like this on my assembler:

ld hl, (B loc)
ld (A loc),hl

The contents of the B variable are first loaded into the hl register and then the hl register is loaded into the A variable. This is not very difficult for the microprocessor to do and requires only about 7 microseconds to accomplish.

What IS difficult about the above instruction is that the compiler has to know where to find the variables A & B. This ability of the compiler to keep track of details helps a great deal in the translation process.

As an important note I will also mention that the above instruction is written in BASIC backwards compared with the way it is executed in machine code. The BASIC expression puts “A” before “B”, but in machine code the contents of the B variable have to be found before it can be transferred to the A variable.

More complex “LET” statements involve the use of more than one register. Consider the following example:

LET A=B+100

In assembly language this would look like:

ld hl,(B loc)
ld de,100
add hl,de
ld (A loc),hl

This is exactly the same as before except two instructions have been added in the middle.

The first three assembly language instructions above constitute the math expression “B+100”. The last assembly language instruction makes up the actual “LET” command. The ability of my compiler to evaluate math expressions is used with almost every command translated. It makes very little difference what the last instruction was. The first three instructions would have been about the same.

Commands like “POKE” are a bit more complex since two math expressions can follow it instead of the one used after “LET”. The translation of “POKE B+100,X+y” will look like this:

ld hl,(B loc)     Evaluate
ld de,100 first expression
add hl,de and store on ma-
push hl chine stack.
ld hl, (X loc) Evaluate
ld de, (Y loc) second expres-
add hl,de sion and store
ex de,hl in de.
pop hl Recover first
ld (hl),e value and do a
poke instruction.

The actual POKE instruction above only takes one command and all the other commands before it are used to evaluate the expressions.

This would have been much more complex if our instruction had been a “BEEP” or a “PLOT”, because these two instructions use the 2068 operation system. Instead of just having one instruction, there would have been at least six following the expression evaluation.

Not all of the commands, however, are that complicated. For instance the “CLS” command just looks like this:

call CLS

In this case CLS is just a routine found in the 2068 ROM.

String handling is easier to do than one might think. Consider the following expression:

LET A$(K)=B$(L)

Which in assembly language is…

ld hl,B$ loc
ex de,hl
ld hl, (L loc)
add hl,de
ld a,(hl)

This is just the first part that gets the character into the register “a”. Only “B$(L)” has been evaluated so far. Now for the second part which will put the character into “A$(K)”…

ld hl,A$ loc
ex de,hl
ld hl,(K loc)
add hl,de
ld (hl),a

It should be evident that the compiler has a great many things to keep track of here. In order to create the above instructions, it must know the location of A$, B$, K, and L.

The above are just a few examples from a very complex instruction set. Most of the compiler commands such as “PRINT” use the 2068 operating system but still have the same simple pattern of expressions first – instructions last.

If you have never used machine code before, but can understand the above examples, then you are well on your way to creating machine code programs.

If you already know how to use machine code then maybe the above will serve as example of how machine code can be used to imitate BASIC.

John Coffey

JRC‘s Integer BASIC Compiler/Assembler is available for $24.95 from JRC, P.O.Box 448, Scottsburg, IN 47170.