Computer Science 3 - 2000 - Test on Prac 21

(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.)

1. See if I can catch you out this time (not likely!) What is your name?

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

     PROGRAM Silly;                               0 DSP        9
       VAR                                        2 ADR       -1
         I, List[5], J, K;                        4 ADR       -1
       BEGIN                                      6 VAL
         I := (I + 1) / List[J - K];              7 LIT        1
       END.                                       9 ADD
                                                 10 ADR       -2
                                                 12 ADR       -8
     The only trick here is to arrange           14 VAL
     for the operations to be done               15 ADR       -9
     in the correct order                        17 VAL
                                                 18 SUB
                                                 19 LIT        6
                                                 21 IND
                                                 22 VAL
                                                 23 DVD
                                                 24 STO
                                                 25 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
   };

or, for Pascal speakers, an equivalent declaration:

   TYPE (* machine instructions - order important *)
     OPCODES   = (adr, lit, dsp, brn, bze, prs, add, sub, mul, dvd, eql, neq,
                  lss, geq, gtr, leq, neg, val, sto, ind, stk, hlt, inn, prn,
                  nln, nop, 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. 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;

or, in Pascal

        brn :
          BEGIN CPU.PC := Mem[CPU.PC] END;
        bze :
          BEGIN
            INC(CPU.SP);
            IF Mem[CPU.SP - 1] = 0 THEN CPU.PC := Mem[CPU.PC] ELSE INC(CPU.PC)
          END;

Why do we not also have a BNZ opcode?

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

Most people saw this - of course we don't need BNZ as any test that would be followed by a BNZ could be expressed as the reverse test followed by a BZE, for example

                EQL      NEQ
                BZE      BNZ

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 (inbounds(cpu.sp))
          { if (mem[cpu.sp - 1] == 1) cpu.pc = mem[cpu.pc]; else cpu.pc++; }
          break;

        bnz :
          BEGIN
            INC(CPU.SP);
            IF InBounds(CPU.SP) THEN
              IF Mem[CPU.SP - 1] = 1 THEN CPU.PC := Mem[CPU.PC] ELSE INC(CPU.PC)
          END;

5. The emulator you have been using as been for a machine that only supports "integer" arithmetic. Outline briefly what you would have to do to develop an emulator for a system that (instead) supported only "real" (floating-point) arithmetic.

This was intended to see who had really gained a proper insight into how emulators work. Nobody had a really good answer, though several students observed astutely that the solution would depend on declaring a memory array to be one of reals (floating point) rather than one of integers. The problem with this is that the memory has to store not only data (on the stack), but also code and string literals, and these are not really best stored as real values.

There are two ways around this, commonly found in such interpreters. The one is to have two separate arrays - one for code/strings and the other for the stack of real values. The other is to declare the memory to be an array of what C programmers call unions and Pascal/Modula programmers call variant records, where the same location can be addressed in the simulator in one of two ways. Actually an array of even simpler structures would suffice - here is one possible set of declarations:

    struct WORDS {
      int intVal;
      float floatVal;
    };

    WORDS mem[STKMC_memsize];   // The memory array

and then operations like BRN and VAL get coded up like this

        case STKMC_brn:
          cpu.pc = mem[cpu.pc].intVal; break;

        case STKMC_val:
          if (inbounds(cpu.sp) && inbounds(mem[cpu.sp].intVal))
            mem[cpu.sp].floatVal = mem[mem[cpu.sp].intVal].floatVal;
          break;

or, for those who speak Pascal:

     TYPE
       WORDS = RECORD
                 IntVal : INTEGER;
                 RealVal : REAL;
               END

     VAR
       Mem : ARRAY [0 .. MemSize - 1] OF WORDS

        BRN:
          CPU.PC = Mem[CPU.PC].INTVAL;

        VAL:
          IF InBounds(CPU.SP) AND InBounds(Mem[CPU.SP].IntVal)
            Mem[CPU.SP].RealVal := Mem[Mem[CPU.SP].IntVal].RealVal;

But apart from a lot of changes like this the system is virtually the same (the I/O has to accommodate different calls to I/O primitives, of course).


Computer Science 3 - 2000 - Test on Prac 21

(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.)

1. See if I can catch you out this time (not likely!) What is your name?

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

     PROGRAM Silly;                               0 DSP       14
       VAR                                        2 ADR       -3
         I, J, List[10], K;                       4 ADR       -1
       BEGIN                                      6 VAL
         List[I * K] := I + J/K;                  7 ADR      -14
       END.                                       9 VAL
                                                 10 MUL
                                                 11 LIT       11
                                                 13 IND
     The only trick here is to arrange           14 ADR       -1
     for the operations to be done               16 VAL
     in the correct order                        17 ADR       -2
                                                 19 VAL
                                                 20 ADR      -14
                                                 22 VAL
                                                 23 DVD
                                                 24 ADD
                                                 25 STO
                                                 26 HLT

3. In the prac you were invited to note that sequences like

         ADR  -X          and   ADR  -Y
         VAL                    code
                                STO

occurred frequently, so frequently that it might be worth introducing special opcodes PSH -X and POP -Y instead.

By now you will recognise something of the Terry perversity - maybe the machine is defined to have ADR, VAL and STO opcodes rather than simply having only PSH and POP opcodes for no other reason than that it makes for lots of fun late at night to add these opcodes to the machine. But maybe not? If you have PSH and POP, could you completely remove ADR, VAL and STO from the opcode list, or would you still need to have them available? Justify your answer.

If every VAL was preceded by an ADR one could get away with using PSH instead of ADR + VAL, and if every other use of ADR was followed by a STO one could get away without ADR. But this is not the case. Consider the following code:

           Read(I); List[I] := List[J];

which, even if we use every PSH and POP we can, translates to

           ADR -I
           INN
           ADR -List
           PSH -I
           LIT Size
           IND
           ADR -List
           PSH -J
           LIT Size
           IND
           VAL
           STO

4. A BYP (Bright Young Programmer) came up with the following idea for adding an overflow check to the stack machine interpreter multiplication code:

        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 this did not work, so the BYP tried it in Pascal as follows:

        mul :
          BEGIN
            INC(CPU.SP);
            IF InBounds(CPU.SP) THEN
              Mem[CPU.SP] := Mem[CPU.SP] * Mem[CPU.SP - 1];
              IF Mem[CPU.SP] > MAXINT THEN PS := Overflow
          END;

and found that this did not work either. Be an EBYP (Even Brighter Young Programmer) and show what sort of code we would need to perform this check properly (in either C++ or Pascal, no need to try both)

It was all too obvious who had bothered to do this part of the prac correctly! Here is one possible solution, which does not require the actual machine to have any greater precision available than the toy machine has:

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

or

        mul :
          BEGIN
            INC(CPU.SP);
            IF InBounds(CPU.SP) THEN
              IF ABS(Mem[CPU.SP]) < MAXINT / ABS(Mem[CPU.SP-1])
                THEN Mem[CPU.SP] := Mem[CPU.SP] * Mem[CPU.SP - 1]
                ELSE PS := Overflow
          END;

5. Suppose we wish to add to the machine so that detecting overflow does not change the program status PS and cause the emulator to stop immediately, but sets a processor flag which programmers could test if and when they felt the urge to write safe programs (an urge which I hope this course will cause you to experience ever more frequently).

Now suppose for the moment that the code given earlier had tried to reflect this by changing the obvious line to read

if (mem[cpu.sp] > maxint) cpu.ov = true;

or, of course,

IF Mem[CPU.SP] > MAXINT THEN CPU.OV := TRUE

Leaving aside that the test is not reliable, this in itself would not be very helpful. We would also need some way that a programmer could program in a test for overflow. Assume that this is to be done by adding a STKMC_bov (branch if overflow set) opcode. How would this have to be emulated?

Those who seemed to have some clue about what overflow actually means managed to get part way, but I don't think that anyone saw that one would need to be able to unset the overflow flag. The best place to do this might be just after testing it, as below:

        case STKMC_bov:
          if (cpu.ov) cpu.pc = mem[cpu.pc]; else cpu.pc++;
          cpu.ov = false; // reset the flag
          break;

or

        bov :
          BEGIN
            IF CPU.OV = THEN CPU.PC := Mem[CPU.PC] ELSE INC(CPU.PC);
            CPU.OV := FALSE (* reset the flag *)
          END;

Alternatively one might be tempted to clear the overflow flag at the start of attempting each ADD, SUB, MUL or DVD operation, and then set it if overflow was detected during that operation. However, this might be clumsy to use, as, for example, code like

        X := VeryBig * VeryBig / 1;
        IF (Overflow) THEN begin Write('Overflow occurred'); RETURN END;

might set overflow when the multiplication was attempted, and then unset it when the division occurred, leaving the overflow undetectable at the point where it is finally checked.