Computer Science 3 - 2001 - Test on Prac 16

(This should take no longer than 20 minutes and is simply intended to probe whether you have understood the
material in the prac questions. You may use your textbook if you think it will help.  Answer on the questions 
sheet.) 

THURSDAY TEST

1. You'd think I'd have learned by now!  What is your surname?

2. Write down the stack assembler code that corresponds to this (silly) program

     PROGRAM Silly;
       VAR
         I, List[20], K;
       BEGIN
         List[3+K] := List[I + K/4]
       END.

This was very badly done, which was surprising as you had been doing several
of these in the prac.  Puzzle it out!  Incidentally, several students tried to
solve this by storing intermediate values in top of K, which would be a
horrible side effect!

         DSP    23
         ADR    -2
         LIT     3
         ADR   -23
         VAL
         ADD
         LIT    21
         IND
         ADR    -2
         ADR    -1
         VAL
         ADR   -23
         VAL
         LIT     4
         DVD
         ADD
         LIT    21
         IND
         VAL
         STO
         HLT

3. In the header for the stack machine emulator you will find the lines

   // machine instructions - order important
   enum STKMC_opcodes {
     STKMC_adr, STKMC_lit, STKMC_dsp, STKMC_brn, STKMC_bze, STKMC_prs, STKMC_add,
     STKMC_sub, STKMC_mul, STKMC_dvd, STKMC_eql, STKMC_neq, STKMC_lss, STKMC_geq,
     STKMC_gtr, STKMC_leq, STKMC_neg, STKMC_val, STKMC_sto, STKMC_ind, STKMC_stk,
     STKMC_hlt, STKMC_inn, STKMC_prn, STKMC_nln, STKMC_nop, STKMC_nul
   };

   Why on earth is the "order important"?

Amazingly, very few people got this right, and yet they must surely have known
that the valid opcodes all come before "nul", otherwise the simple search in
the assembler that associated mnemonics with opcodes will fail. Looking
further we see that all the two-word operations are defined ahead of the
one-word operations.  Although this is not important in the rest of my code,
it just might be useful to stick with that convention.

The most frequent "wrong" answer was that the order of the enumeration had to
match the order of the labels in the switch/case statement.  I am not sure
where this misconception came from.  The order of the labels in a switch/case
statement is unimportant.  What is important, of course, is that the labels
must be unique - something like

              switch (exp) {
                case 1: something(); break;
                case 2: somethingelse(); break;
                case 1: somethingdifferent(); break;
              }
should not be allowed.

4. A BYP (Bright Young Programmer), noting that the emulator checked for
   division by zero, came up with the following idea for adding another
   overflow check, namely for attempting to multiply large numbers whose
   product would be too large to store:

        case STKMC_mul:
          cpu.sp++;
          if (inbounds(cpu.sp)) {
            mem[cpu.sp] *= mem[cpu.sp - 1];
            if (mem[cpu.sp] > maxint) ps = overflow;
          }
          break;

   but found that this did not work. Be an EBYP (Even Brighter Young
   Programmer) and show what sort of code we would need to perform this check
   properly.

Rather few people saw this.  The catch is that the overflow would happen
before it could be detected.  There are several possibilities:

(a) Use a variable of LONG type to do the multiplication and then check the
result (remembering that the result could be positive or negative; I don't
think anyone saw that).  This sort of trick is only possible if the
interpreter can do arithmetic to greater precision that the machine it is
trying to simulate.

In C++:

        case STKMC_mul:
          cpu.sp++;
          if (inbounds(cpu.sp)) {
            long temp = mem[cpu.sp] * mem[cpu.sp - 1];
            if (abs(temp) > maxint) ps = overflow; else mem[cpu.sp] = temp;
          }
          break;


(b) If we do not have extended precision available on the "real" machine we
have to be rather more clever.  We can, however, use a check on division rather
than on multiplication.  If A * B < C then it also follows that A < C / B,
and of course C/B is <= C if B is an integer.  So we can keep within the
bounds of the simulated data type!

We might think we could get away with

        case STKMC_mul:
          cpu.sp++;
          if (inbounds(cpu.sp)) {
            if (abs(maxint / mem[cpu.sp]) > abs(mem[cpu.sp - 1]))
              ps := overflow;
            else mem[cpu.sp] *= mem[cpu.sp - 1];
          }
          break;

Actually this is inadequate; it would collapse if mem[cpu.sp] = 0.  But that
is easily fixed:

        case STKMC_mul:
          cpu.sp++;
          if (inbounds(cpu.sp)) {
              if mem[cpu.sp] != 0
                if abs(maxint / mem[cpu.sp]) > abs(mem[cpu.sp - 1]))
                  ps := overflow;
                  else mem[cpu.sp] *= mem[cpu.sp - 1];
                else /* the product would be zero anyway */
          }
          break;

The point to be noted is that interpretation/simulation has to be very
carefully done if it is not to go wrong at crucial times.

FRIDAY TEST

1. You'd think I'd have learned by now!  What is your surname?


2. Write down the stack assembler code that corresponds to this (silly) program

     PROGRAM Silly;
       VAR
         I, List[20], K;
       BEGIN
         I := List[K+List[K]]
       END.

This was very badly done, which was surprising as you had been doing several
of these in the prac.  Puzzle it out!  Incidentally, several students tried to
solve this by storing intermediate values in top of K, which would be a
horrible side effect!

         DSP    23
         ADR    -1
         ADR    -2
         ADR   -23
         VAL
         ADR    -2
         ADR   -23
         VAL
         LIT    21
         IND
         VAL
         ADD
         LIT    21
         IND
         VAL
         STO
         HLT

3. The machine currently supports the input and output of integer data only,
   using the INN and PRN opcodes. What changes (additions) would you have to
   make to allow for opcodes INC and PRC that would read and write a single
   character of data?

Most students had a good idea about how to do this.  We have to add two
opcodes to the machine (they would have to be added into various tables too).
The code in the CASE/SWITCH statements would be a simple variation on the
extant code for PRN and INN, for example (note how I have used a MOD 256
operation to make sure that the character to be printed comes from the ASCII
set)

        case STKMC_inc:
          if (inbounds(cpu.sp) && inbounds(mem[cpu.sp]))
          { char temp;
            if (fscanf(data, "%c", &mem[mem[cpu.sp]]) == 0) ps = baddata;
            else cpu.sp++;
          }
          break;
        case STKMC_prc:
          if (tracing) fputs(BLANKS, results);
          cpu.sp++;
          if (inbounds(cpu.sp)) fprintf(results, "%c", abs(mem[cpu.sp - 1]) % 256);
          if (tracing) putc('\n', results);
          break;

4. The stack machine supports two branch instructions BZE and BRN, with
   emulation on the lines of

        case STKMC_brn:
          cpu.pc = mem[cpu.pc]; break;
        case STKMC_bze:
          cpu.sp++;
          if (mem[cpu.sp - 1] == 0) cpu.pc = mem[cpu.pc]; else cpu.pc++;
          break;


   Why do we not also have a BNZ opcode?

Because, as most people had noticed, we don't need it.  Code like

       NEQ
       BNZ   X

is easily rewritten as

       EQL
       BZE   X

Alternatively, code like

       BNZ      Somewhere
       CarryOn

could be written

       BZE      CarryOn
       BRN      Somewhere
       CarryOn

   If we wished to add it, what would we have to add to the interpreter?

A trivial change to the code for bze is all that is needed to be added, plus
of course the changes needed in the lists of opcodes and mnemonics:

        case STKMC_bnz:
          cpu.sp++;
          if (mem[cpu.sp - 1] != 0) cpu.pc = mem[cpu.pc]; else cpu.pc++;
          break;