14.10. Additive Expressions

If you've been following this series from the beginning, I'm sure you know what's coming next: We'll expand the form for an expression to handle first additive expressions, then multiplicative, then general expressions with parentheses.

The nice part is that we already have a pattern for dealing with these more complex expressions. All we have to do is to make sure that all the procedures called by Expression (Term, Factor, etc.) always return a type identifier. If we do that, the program structure gets changed hardly at all.

The first step is easy: We can rename our existing function Expression to Term, as we've done so many times before, and create the new version of Expression:

{ Parse and Translate an Expression }
function Expression: char;
var Typ: char;
   if IsAddop(Look) then
      Typ := Unop
      Typ := Term;
   while IsAddop(Look) do begin
      case Look of
       '+': Typ := Add(Typ);
       '-': Typ := Subtract(Typ);
   Expression := Typ;

Note in this routine how each procedure call has become a function call, and how the local variable Typ gets updated at each pass.

Note also the new call to a function Unop, which lets us deal with a leading unary minus. This change is not necessary … we could still use a form more like what we've done before. I've chosen to introduce UnOp as a separate routine because it will make it easier, later, to produce somewhat better code than we've been doing. In other words, I'm looking ahead to optimization issues.

For this version, though, we'll retain the same dumb old code, which makes the new routine trivial:

{ Process a Term with Leading Unary Operator }
function Unop: char;
   Unop := 'W';

Procedure Push is a code-generator routine, and now has a type argument:

{ Push Primary onto Stack }
procedure Push(Size: char);
   Move(Size, 'D0', '-(SP)');

Now, let's take a look at functions Add and Subtract. In the older versions of these routines, we let them call code generator routines PopAdd and PopSub. We'll continue to do that, which makes the functions themselves extremely simple:

{ Recognize and Translate an Add }
function Add(T1: char): char;
   Add := PopAdd(T1, Term);

{ Recognize and Translate a Subtract }
function Subtract(T1: char): char;
   Subtract := PopSub(T1, Term);

The simplicity is deceptive, though, because what we've done is to defer all the logic to PopAdd and PopSub, which are no longer just code generation routines. They must also now take care of the type conversions required.

And just what conversion is that? Simple: Both arguments must be of the same size, and the result is also of that size. The smaller of the two arguments must be "promoted" to the size of the larger one.

But this presents a bit of a problem. If the argument to be promoted is the second argument (i.e. in the primary register D0), we are in great shape. If it's not, however, we're in a fix: we can't change the size of the information that's already been pushed onto the stack.

The solution is simple but a little painful: We must abandon that lovely “pop the data and do something with it” instructions thoughtfully provided by Motorola.

The alternative is to assign a secondary register, which I've chosen to be R7. (Why not R1? Because I have later plans for the other registers.)

The first step in this new structure is to introduce a Pop procedure analogous to the Push. This procedure will always Pop the top element of the stack into D7:

{ Pop Stack into Secondary Register }
procedure Pop(Size: char);
   Move(Size, '(SP)+', 'D7');

The general idea is that all the “Pop-Op” routines can call this one. When this is done, we will then have both operands in registers, so we can promote whichever one we need to. To deal with this, procedure Convert needs another argument, the register name:

{ Convert a Data Item from One Type to Another }
procedure Convert(Source, Dest: char; Reg: String);
   if Source <> Dest then begin
      if Source  = 'B' then
         EmitLn('AND.W #$FF,' + Reg);
      if Dest = 'L' then
         EmitLn('EXT.L ' + Reg);

The next function does a conversion, but only if the current type T1 is smaller in size than the desired type T2. It is a function, returning the final type to let us know what it decided to do:

{ Promote the Size of a Register Value }
function Promote(T1, T2: char; Reg: string): char;
var Typ: char;
   Typ := T1;
   if T1 <> T2 then
      if (T1 = 'B') or ((T1 = 'W') and (T2 = 'L')) then begin
         Convert(T1, T2, Reg);
         Typ := T2;
   Promote := Typ;

Finally, the following function forces the two registers to be of the same type:

{ Force both Arguments to Same Type }
function SameType(T1, T2: char): char;
   T1 := Promote(T1, T2, 'D7');
   SameType := Promote(T2, T1, 'D0');

These new routines give us the ammunition we need to flesh out PopAdd and PopSub:

{ Generate Code to Add Primary to the Stack }
function PopAdd(T1, T2: char): char;
   T2 := SameType(T1, T2);
   PopAdd := T2;

{ Generate Code to Subtract Primary from the Stack }
function PopSub(T1, T2: char): char;
   T2 := SameType(T1, T2);
   PopSub := T2;

After all the buildup, the final results are almost anticlimactic. Once again, you can see that the logic is quite simple. All the two routines do is to pop the top-of-stack into D7, force the two operands to be the same size, and then generate the code.

Note the new code generator routines GenAdd and GenSub. These are vestigial forms of the original PopAdd and PopSub. That is, they are pure code generators, producing a register-to-register add or subtract:

{ Add Top of Stack to Primary }
procedure GenAdd(Size: char);
   EmitLn('ADD.' + Size + ' D7,D0');

{ Subtract Primary from Top of Stack }
procedure GenSub(Size: char);
   EmitLn('SUB.' + Size + ' D7,D0');
   EmitLn('NEG.' + Size + ' D0');

OK, I grant you: I've thrown a lot of routines at you since we last tested the code. But you have to admit that each new routine is pretty simple and transparent. If you (like me) don't like to test so many new routines at once, that's OK. You can stub out routines like Convert, Promote, and SameType, since they don't read any inputs. You won't get the correct code, of course, but things should work. Then flesh them out one at a time.

When testing the program, don't forget that you first have to declare some variables, and then start the “body” of the program with an upper-case 'B' (for BEGIN). You should find that the parser will handle any additive expressions. Once all the conversion routines are in, you should see that the correct code is generated, with type conversions inserted where necessary. Try mixing up variables of different sizes, and also literals. Make sure that everything's working properly. As usual, it's a good idea to try some erroneous expressions and see how the compiler handles them.