At last, in this installment, we've learned how to deal with variables (and literals) of different types. As you can see, it hasn't been too tough. In fact, in some ways most of the code looks even more simple than it does in earlier programs. Only the multiplication and division operators require a little thinking and planning.
The main concept that made things easy was that of converting procedures such as Expression into functions that return the type of the result. Once this was done, we were able to retain the same general structure of the compiler.
I won't pretend that we've covered every single aspect of the issue. I conveniently ignored unsigned arithmetic. From what we've done, I think you can see that to include them adds no new challenges, just extra possibilities to test for.
I've also ignored the logical operators And, Or, etc. It turns out that these are pretty easy to handle. All the logical operators are bitwise operations, so they are symmetric and therefore work in the same fashion as PopAdd. There is one difference, however: if it is necessary to extend the word length for a logical variable, the extension should be done as an unsigned number. Floating point numbers, again, are straightforward to handle … just a few more procedures to be added to the run-time library, or perhaps instructions for a math chip.
Perhaps more importantly, I have also skirted the issue of type checking, as opposed to conversion. In other words, we've allowed for operations between variables of all combinations of types. In general this will not be true … certainly you don't want to add an integer, for example, to a string. Most languages also don't allow you to mix up character and integer variables.
Again, there are really no new issues to be addressed in this case. We are already checking the types of the two operands … much of this checking gets done in procedures like SameType. It's pretty straightforward to include a call to an error handler, if the types of the two operands are incompatible.
In the general case, we can think of every single operator as being handled by a different procedure, depending upon the type of the two operands. This is straightforward, though tedious, to implement simply by implementing a jump table with the operand types as indices. In Pascal, the equivalent operation would involve nested Case statements. Some of the called procedures could then be simple error routines, while others could effect whatever kind of conversion we need. As more types are added, the number of procedures goes up by a square-law rule, but that's still not an unreasonably large number of procedures.
What we've done here is to collapse such a jump table into far fewer procedures, simply by making use of symmetry and other simplifying rules.