Computer Science 3 - 2000

Programming Language Translation


Practical for Week 21, beginning 18 September 2000 - Solutions

There were some very good solutions submitted, and some very energetic ones too - clearly a lot of students had put in many hours developing their code. This is very encouraging. Do learn to put your names into the introductory comments of programs that you write.

Full source for the solutions summarized here can be found in the ZIP files on the Web pages - PRAC21CA.ZIP (C++ version) or PRAC21PA.ZIP (Pascal version).


Task 1

Most people had seen at least one improvement that could be made to the palindrome checker to ensure that the loop terminated as quickly as possible. Here are some suggestions (there are even more ways of course):

    IsPalindrome := TRUE;               (* optimist *)
    Low := 0; High := N - 1;            (* initial indices *)
    WHILE Low < High DO BEGIN           (* sweep through the List *)
      IF List[Low] <> List[High] THEN IsPalindrome := FALSE; (* bad luck *)
      Low := Low + 1; High := High - 1; (* adjust indices *)
    END;

    IsPalindrome := TRUE;               (* optimist *)
    Checking := TRUE;                   (* start search *)
    Low := 0; High := N - 1;            (* initial indices *)
    WHILE Checking = TRUE DO BEGIN           (* sweep through the List *)
      IF List[Low] <> List[High] THEN BEGIN
        IsPalindrome := FALSE; (* bad luck *)
        Checking := FALSE (* no need to search further *)
      END;
      Low := Low + 1; High := High - 1; (* adjust indices *)
      IF Low >= High THEN Checking := FALSE (* reached middle *)
    END;

    IsPalindrome := TRUE;               (* optimist *)
    Low := 0; High := N - 1;            (* initial indices *)
    WHILE Low < High DO BEGIN           (* sweep through the List *)
      IF List[Low] <> List[High] THEN BEGIN
        IsPalindrome := FALSE; (* bad luck *)
        Low := High (* to abort the loop *)
      END;
      Low := Low + 1; High := High - 1; (* adjust indices *)
    END;

    IsPalindrome := TRUE;               (* optimist *)
    Low := 0; Mid := N/2;               (* initial indices *)
    WHILE Low < Mid DO BEGIN            (* sweep through the List *)
      IF List[Low] <> List[N - 1 - Low] THEN BEGIN
        IsPalindrome := FALSE; (* bad luck *)
        Low := Mid (* to abort the loop *)
      END;
      Low := Low + 1; (* adjust indices *)
    END;

One way that requires a little more coding is to use a function that returns as soon as it can:

  FUNCTION IsPalindrome(List[], N);
    VAR
      Low, High;     (* indices of items to be compared *)
    BEGIN
      Low := 0; High := N - 1;            (* initial indices *)
      WHILE Low < High DO BEGIN           (* sweep through the List *)
        IF List[Low] <> List[High] THEN RETURN FALSE; (* bad luck *)
        Low := Low + 1; High := High - 1; (* adjust indices *)
      END;
      RETURN TRUE
    END;

  BEGIN
    IF IsPalindrome(List, N) = TRUE THEN Write('Palindromic sequence');
    IF IsPalindrome(List, N) = FALSE THEN Write('Non-palindromic sequence');
  END.


Task 4

Most people seemed to get to a solution, or close to a solution. Here is one that matches the original Clang algorithm (which is actually what you were asked to do!

; comments are allowed on lines starting with ;
; program to check a sequence for being palindromic
; P.D. Terry, Rhodes University, 2000

   0   DSP      106 ;  BEGIN                          73   BZE     121  ;   WHILE Low < N - 1 DO BEGIN
   2   ADR      -1  ;                                 75   ADR      -6  ;
   4   LIT       0  ;                                 77   ADR      -2  ;
   6   STO          ;    N := 0                       79   VAL          ;
   7   ADR      -4  ;                                 80   LIT     101  ;
   9   INN          ;    Read(Item)                   82   IND          ;
  10   ADR      -4  ;                                 83   VAL          ;
  12   VAL          ;                                 84   ADR      -6  ;
  13   LIT       0  ;                                 86   ADR      -3  ;
  15   NEQ          ;                                 88   VAL          ;
  16   BZE      44  ;    WHILE Item <> 0 DO BEGIN     89   LIT     101  ;
  18   ADR      -6  ;                                 91   IND          ;
  20   ADR      -1  ;                                 92   VAL          ;
  22   VAL          ;                                 93   NEQ          ;
  23   LIT     101  ;                                 94   BZE     101  ;     IF List[Low} <> List[High] THEN
  25   IND          ;                                 96   ADR      -5  ;
  26   ADR      -4  ;                                 98   LIT       0  ;
  28   VAL          ;                                100   STO          ;       IsPalindrome := FALSE
  29   STO          ;      List[N] := Item           101   ADR      -2  ;
  30   ADR      -1  ;                                103   ADR      -2  ;
  32   ADR      -1  ;                                105   VAL          ;
  34   VAL          ;                                106   LIT       1  ;
  35   LIT       1  ;                                108   ADD          ;
  37   ADD          ;                                109   STO          ;       Low := Low + 1
  38   STO          ;      N := N + 1                110   ADR      -3  ;
  39   ADR      -4  ;                                112   ADR      -3  ;
  41   INN          ;      Read(Item)                114   VAL          ;
  42   BRN      10  ;    END                         115   LIT       1  ;
  44   ADR      -5  ;                                117   SUB          ;
  46   LIT       1  ;                                118   STO          ;       High := High - 1
  48   STO          ;    IsPalindrome := TRUE        119   BRN      63  ;   END
  49   ADR      -2  ;                                121   ADR      -5  ;
  51   LIT       0  ;                                123   VAL          ;
  53   STO          ;    Low := 0                    124   LIT       1  ;
  54   ADR      -3  ;                                126   EQL          ;
  56   ADR      -1  ;                                127   BZE     132  ;   IF IsPalindrome = TRUE THEN
  58   VAL          ;                                129   PRS  'Palindromic sequence'
  59   LIT       1  ;                                131   NLN          ;
  61   SUB          ;                                132   ADR      -5  ;
  62   STO          ;    High := N = 1               134   VAL          ;
  63   ADR      -2  ;                                135   LIT       0  ;
  65   VAL          ;                                137   EQL          ;
  66   ADR      -1  ;                                138   BZE     143  ;   IF IsPalinDrome = FALSE THEN
  68   VAL          ;                                140   PRS  'Non-palindromic sequence'
  69   LIT       1  ;                                142   NLN          ;
  71   SUB          ;                                143   HLT          ; END
  72   LSS          ;


Task 5

Checking sentences for palindromes can use essentially the same algorithm, but care has to be taken to apply it only to the alphanumeric characters. If one develops simple minded operations for reading and writing characters, as most people successfully did, then one is rather forced to try to write code that corresponds to the following (where, of course, one has to use the magic ASCII numbers for the characters limiting the ends of the valid ranges):

PROGRAM Palindrome;
(* Read a sequence of characters and report whether they form a palindromic
   sentence (one that reads the same from either end)
   Examples:   Madam, I'm Adam. is palindromic
               Pat Terry.  is non-palindromic
   P.D. Terry, Rhodes University, 2000 *)

  CONST
    TRUE = 1;
    FALSE = 0;
  VAR
    N,               (* number of items *)
    Low, High,       (* indices of items to be compared *)
    Item,            (* latest item read *)
    IsPalindrome,    (* Boolean flag *)
    List[100];       (* the list of items *)

  BEGIN
    N := 0;
    ReadChar(Item);
    WHILE Item <> '.' DO BEGIN
      IF Item >= 'A' THEN
        IF Item <= 'Z' THEN BEGIN List[N] := Item; N := N + 1 END;
      IF Item >= 'a' THEN
        IF Item <= 'z' THEN BEGIN List[N] := Item; N := N + 1 END;
      IF Item >= '0' THEN
        IF Item <= '9' THEN BEGIN List[N] := Item; N := N + 1 END;
      ReadChar(Item)
    END;
    IsPalindrome := TRUE;               (* optimist *)
    Low := 0; High := N - 1;            (* initial indices *)
    WHILE Low < High DO BEGIN           (* sweep through the List *)
      IF List[Low] <> List[High] THEN IsPalindrome := FALSE; (* bad luck *)
      Low := Low + 1; High := High - 1; (* adjust indices *)
    END;
    IF IsPalindrome = TRUE THEN Write('Palindromic sequence');
    IF IsPalindrome = FALSE THEN Write('Non-palindromic sequence');
  END.

This is all rather tedious to code up, and several people tried to do clever things with the interpreter to cut out having to write all those nasty IF statements out in detail. However, I suspect it is not very clever to restrict the operation for reading characters to handle only letters, or only upper case letters, or to force the characters to be stored in upper case, as some people did. Rather introduce further opcodes for doing this. Although this was not suggested in the prac sheet, the solution below shows how these could have been defined; in terms of them (and other extensions) a rather shorter palindrome checker might be coded as follows:

; comments are allowed on lines starting with ;
; program to check a sentence for being palindromic
; P.D. Terry, Rhodes University, 2000
     0 DSP      106                                   57 POP       -3    Low := N - 1;
     2 LIT        0 BEGIN                             59 PSH       -2
     4 POP       -1    N := 0                         61 PSH       -3
     6 ADR       -4                                   63 LSS
     8 INA             ReadChar(Item)                 64 BZE       97   WHILE Low < High DO BEGIN
     9 PSH       -4                                   66 ADR       -6
    11 LIT       46                                   68 PSH       -2
    13 NEQ                                            70 LIT      101
    14 BZE       44    WHILE Item <> '.' DO BEGIN     72 IND
    16 PSH       -4                                   73 VAL
    18 UPC                                            74 ADR       -6
    19 POP       -4      Item := UpCase(Item)         76 PSH       -3
    21 PSH       -4                                   78 LIT      101
    23 ALN                                            80 IND
    24 BZE       39      IF AlNum(Item) THEN BEGIN    81 VAL
    26 ADR       -6                                   82 NEQ
    28 PSH       -1                                   83 BZE       89     IF List[Low] <> List[High] THEN
    30 LIT      101                                   85 LIT        0
    32 IND                                            87 POP       -5       IsPalinDrome := FALSE;
    33 PSH       -4                                   89 ADR       -2
    35 STO                 List[N] := Item;           91 INC              Low := Low + 1;
    36 ADR       -1                                   92 ADR       -3
    38 INC                 N := N + 1                 94 DEC              High := High - 1;
    39 ADR       -4      END;                         95 BRN       50   END;
    41 INA               ReadChar(Item);              97 PSH       -5
    42 BRN        9    END;                           99 LIT        1
    44 LIT        1                                  101 EQL            IF IsPalindrome THEN
    46 POP       -5    IsPalindrome := TRUE;         102 BZE      108
    48 LIT        0                                  104 PRS   'Palindromic sequence'
    50 POP       -2    High := 0;                    106 BRN      110     ELSE
    52 PSH       -1                                  108 PRS   'Non-palindromic sequence'
    54 LIT        1                                  110 NLN
    56 SUB                                           111 HLT          END.


Task 6

Some people forgot to introduce the PSH and POP references everywhere. They are needed in the switch statements in

   void STKMC::listcode(char *filename, STKMC_address codelen)

and in

   void STKMC::trace(FILE *results, STKMC_address pcnow)

as well as in the obvious one in the emulator.

Checking for overflow in multiplication was not well done. Here is one way of doing it - note the check to prevent a division by zero. This does not use any precision greater than that of the simulated machine itself.

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

Some students used an intermediate long variable (most of them forgot that they should use the abs function as well!)

        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] *= mem[cpu.sp - 1];
          }
          break;

Pushing and popping are actually very easy. There were some curious mistakes, showing that no much had been tested properly! Note the form that the inbounds checks take - many people got these wrong:

        case STKMC_psh:
          cpu.sp--;
          if (inbounds(cpu.sp) && inbounds(cpu.bp + mem[cpu.pc]))
            { mem[cpu.sp] = mem[cpu.bp + mem[cpu.pc]]; cpu.pc++; }
          break;

        case STKMC_pop:
          cpu.sp++;
          if (inbounds(cpu.sp) && inbounds(cpu.bp + mem[cpu.pc]))
            { mem[cpu.bp + mem[cpu.pc]] = mem[cpu.sp - 1]; cpu.pc++; }
          break;

Reading and writing characters was trivially easy, being essentially a simple variation on the cases for numeric input and output. However, the output of numbers was arrranged to have a leading space; this is not as pretty when you see i t a p p l i e d t o c h a r a c t e r s , i s i t ? And note the use of the modulo arithmetic to ensure that only sensible ASCII characters will be printed:

        case STKMC_ina:
          if (inbounds(cpu.sp) && inbounds(mem[cpu.sp]))
          { if (fscanf(data, "%c", &mem[mem[cpu.sp]]) == 0) ps = baddata;
            else cpu.sp++; }
          }
          break;

        case STKMC_pra:
          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;

The opcodes for the equivalent of a ++ or -- operation produced interesting answers. There are clearly two approaches that could be used: either increment the value at the top of the stack, or increment the variable whose address is at the top of the stack. I suspect the latter is more useful if you are to have but one of these (one could, of course, provide both versions of the opcodes). Here is my suggestion:

        case STKMC_inc:
          if (inbounds(cpu.sp) && inbounds(mem[cpu.sp])) mem[mem[cpu.sp]]++;
          cpu.sp++;
          break;
        case STKMC_dec:
          if (inbounds(cpu.sp) && inbounds(mem[cpu.sp])) mem[mem[cpu.sp]]--;
          cpu.sp++;
          break;

With these, one can translate statements like

         A++       or       List[4]--

into very short sequences

         ADR -A             ADR -List
         INC                LIT 4
                            LIT Size
                            IND
                            DEC

The functions for ABS, ODD, IsAlNum and ToUpper are realised by very simple opcodes that do not bump the stack pointer at all:

        case STKMC_abs:
          if (inbounds(cpu.sp)) mem[cpu.sp] = abs(mem[cpu.sp]);
          break;

        case STKMC_odd:
          if (inbounds(cpu.sp)) mem[cpu.sp] = (mem[cpu.sp] % 2 != 0);
          break;

        case STKMC_aln:
          if (inbounds(cpu.sp)) mem[cpu.sp] = isalnum(mem[cpu.sp]);
          break;

        case STKMC_upc:
          if (inbounds(cpu.sp)) mem[cpu.sp] = toupper(mem[cpu.sp]);
          break;

Finally, it is necessary to add the menmonics and the opcodes into the look up tables, but obviously everyone had realised this. It is important to leave STKMC_nul as the last opcode in the enumeration, as most people must have realised (or were just lucky, as the quiz question caught a whole lot of you out!)

In terms of these new opcodes programs can become much shorter. Here are some examples:

; comments are allowed on lines starting with ;
; program to find the smallest and largest of a stream of integers
; P.D. Terry, Rhodes University, 2000 (modified to use pop/psh)

   0 DSP   3                A is at BP-1, Largest at BP-2, Smallest at BP-3
   2 PRS   'Supply list of numbers terminated with 0 '
   4 ADR   -1
   6 INN                    read A
   7 PSH   -1
   9 POP   -3               SMALLEST := A
  11 PSH   -1
  13 POP   -2               LARGEST := A
  15 PSH   -1
  17 LIT   0
  19 NEQ
  20 BZE   49               while A <> 0
  22 PSH   -1
  24 PSH   -2
  26 GTR
  27 BZE   33                 if A > LARGEST
  29 PSH   -1
  31 POP   -2                   LARGEST := A
  33 PSH   -1
  35 PSH   -3
  37 LSS
  38 BZE   44                 if A < SMALLEST
  40 PSH   -1
  42 POP   -3                   SMALLEST := A
  44 ADR   -1
  46 INN                      read A
  47 BRN   15
  49 NLN                    output answers
  50 PSH   -3
  52 PRN                    print SMALLEST
  53 PRS   ' is the smallest and '
  55 PSH   -2
  57 PRN                    print LARGEST
  58 PRS   ' is the largest'
  60 NLN
  61 HLT

; comments are allowed on lines starting with ;
; program to examine the hailstone sequence
; P.D. Terry, Rhodes University, 2000
   0  DSP        3       Term is at Mem[BP-1], N is at Mem[BP-2]
   2  PRS   'What is the first number? '
   4  NLN
   5  ADR       -1
   7  INN                Read(Term)
   8  PSH       -1
  10  ABS
  11  POP       -1       Term := ABS(Term)
  13  LIT        1
  15  POP       -2       N := 1
  17  PSH       -1
  19  PRN                Write(Term)
  20  NLN                WriteLn
  21  PSH       -1
  23  LIT        1
  25  GTR
  26  BZE       61       WHILE Term > 1 DO BEGIN
  28  ADR       -2
  30  INC                  N := N + 1;
  31  PSH       -1
  33  ODD
  34  BZE       48         IF ODD(Term)
  36  LIT        3           THEN
  38  PSH       -1
  40  MUL
  41  LIT        1
  43  ADD
  44  POP       -1             Term := 3 * Term + 1
  46  BRN       55
  48  PSH       -1           ELSE
  50  LIT        2
  52  DVD
  53  POP       -1             Term := Term / 2;
  55  PSH       -1
  57  PRN                  Write(Term);
  58  NLN                  WriteLn;
  59  BRN       21      END;
  61  PSH       -2
  63  PRN               Write(Terms)'
  64  PRS   ' terms'    Write(' terms');
  66  NLN               WriteLn;
  67  HLT             END.


Task 7

Although two groups guessed correctly what MYSTERY.STK did, I am not convinced that they really worked out how it worked. It was none other than that old favourite, the Sieve of Eratosthenes, written to do the same thing over and over again, 200 times in fact.

Several people did the obvious thing - simply knocked out (commented out) the calls to the inbounds function to produce a dangerous, but faster interpreter. One can knock out the calls to the tracing routine, and speed up the IND opcode as well. Note that maximum speed is obtained by knocking out the calls to the function, not by calling a function that simply returns immediately, as one or two groups tried.

Results are quite interesting. On my system (a 200MHz Pentium) I got the following timings. OPTIMUM.STK was a version with PSH/POP operations which resulted in even less code. The times are given in seconds, and one can see that omitting the calls to inbounds speeds things up by about 30%:

C++ versions

           Mystery.stk        Optimum.stk

1000

full checks      7.37            5.47          0.74
no stack chk     5.09            3.61          0.71

2000
                10.14            7.22          0.71

Pascal code (or at least when Paranoid Pat compiles it) incorporates checks that array bounds are not exceeded. It is possible to tune the Pascal compiler to omit these checks (C++ compilers, of course, never build in such checks; they cannot, because arrays in C++ are really just "syntactic sugar" around unguarded pointers). Now here is a thing! When I did this I found the Pascal version of the interpreter to be about 220% faster than the C++ version. Since the source code for the C++ and Pascal versions is actually very similar. This difference is thus almost entirely due to the very high quality of translation to 8086 machine code achieved by Turbo Pascal. The version we use is about 10 years old, but it knocks spots off almost every other DOS based compiler I have ever seen - and it compiles Pascal to this very efficient code very quickly too.

Pascal versions

           Mystery.stk        Optimum.stk

1000

full checks     16.09           11.49          0.71
no stack chk     9.53            6.67          0.69
fastest          2.40            1.65          0.69

2000
                 4.54            3.16