Computer Science 3 - 2009 - Test on Prac 19

This should take no longer than 30 minutes and is simply intended to probe whether you have understood the material in the prac questions and the early part of the course. Answer on the question sheet (4 sides).

1. You should get this right! Mine is Pat Terry, 63T0844. What is yours? [1]

Pat Terry, 63T0844

2. Some translators are called assemblers, some are called compilers. How are they distinguished? [2]

Assemblers translate low level (assembler) code to machine code. Compilers generally translate high-level code to machine code. See page 8 of the text.

3. What happens in the front end of a compiler, and what happens in the back end of a compiler? [2]

The front end performs "analysis" - lexical, syntactic, semantic. It generally results in the production of an implicit or explicit decorated tree representation of the program. The back end uses performs "synthesis" - it uses this representation to generate object code for the program. See pages 10 - 14 of the text.

4. How might a concrete syntax tree differ from an abstract syntax tree? Illustrate by suggesting what these trees might look like for the statement [5]

if (i > 10) i = 10;

A concrete syntax tree contains all the tokens of each statement in its leaves. An abstract syntax tree discards all but the essential information needed for the back end to be able to synthesize object code. There isn't a huge amount of difference!

Concrete:                                                Abstract:



             IfStatement                                             IfStatement
                   |                                                       |
    .--------------+---------------------.                        .---------------------.
    |     |        |      |              |                        |                     |

    if    (   Expression  )          Statement                Expression             Statement
                   |                     |                        |                     |
               .---+---.                 |                    .---+---.                 |
               |   |   |                                      |   |   |
                                     Assignment                                     Assignment
             Term  <  Term               |                    i   <   10                |
               |        |        .----------------------.                      .------------------.
               |        |        |     |      |         |
                                                                             Target  Assign   Expression
               i       10      Target  =  Expression    ;                      |
                                 |            |                                |                  |

                                 i           Term                              i                  10
                                              |

                                              10

5. Suggest, with brief reasons, three features of Java syntax that you suggest have been defined in the way they are so as to make the compilation of Java programs simpler than it might otherwise be. [3]

Some submissions confused "make life easier for the compiler" with "make it easier to develop and
debug programs". It's not quite the same thing. In fact, it probably complicates a compiler to arrange
for it to insert run-time error checks and so on. But there are definite little quircks of Java (and,
indeed of any language) that make the parsing process simpler. For example:

(a) variables must be declared before use
(b) each statement ends with a semicolon
(c) key words
(d) case sensitive identifiers
(e) while and do-while and if statements parenthesize their Boolean expressions
(f) no goto statements other than break
(g) operators like ++, += and so on suggest particularly efficient code sequences
(h) source code requires white space to appear between "word" tokens like "public static void main"
where there are no other recognizable delimiters.

6. Some C compilers achieve their goals by compiling first to assembly code and then using a complementary assembler to finish the job. (GCC can work in this way, I understand). Complete the T diagram representation of how such an arrangement would handle the compilation of a simple program. [5]

      .--------------------------.          .--------------------------.          .--------------------------.
      |         Simple.C         |          |        Simple.ASM        |          |        Simple.EXE        |
      |                          |          |                          |          |                          |
      | Input  -------->  Output |          | Input  -------->  Output |          | Input  -------->  Output |
      `-------.          .--------------------------.          .--------------------------.          .-------'
              |          |         CtoASM.EXE       |          |        ASMto8086.EXE     |          |
              |          |                          |          |                          |          |
              |    C     |   C    --------->  ASM   |   ASM    |  ASM   --------->  8086  |   8086   |
              `------------------.          .--------------------------.          .------------------'
                                 |          |                          |          |
                                 |   8086   |                          |   8086   |
                                 |          |                          |          |
                                 `----------'                          `----------'
                                 .----------.                          .----------.
                                 |   8086   |                          |   8086   |
                                 `----------'                          `----------'

7. Here's a really simple Parva program - the sort you might find a first year student writing:

    bool b(int n) {
      return n == i(n);
    }

    int i(int n) {
      int r = 0;
      while (n != 0) {
        r = 10 * r + n % 10;
        n = n / 10;
      }
      return r;
    }

    void main() {
      int n;
      read("Supply n (0 stops) ", n);
      while (n != 0) {
        write(b(n));
        read(n);
      }
    }

Simplicity is sometimes deceptive!

So it appeared! It was depressing how few people could "decode" this simple algorithm. Have you not learned how to "play computer".

(a) What do you suppose the program is meant to achieve? [2]

It reads a list of integers and tells you whether each is a palindrome (like 12321 or 44 or 686)

(b) Suggest better names for the identifiers used for the functions/methods. [2]

Function b would be better named isPalindrome
Function i would be better named reverse

(c) Why will this code not compile? [2]

Because in Parva all identifiers, even methods, must be declared before use, and because there is no
% operator - as you should have discovered in the recent prac. In fact many submissions recognized
this, but I don't know that anyone spotted that the "declare before use" requirement" had not been
met. Surely you knew this from the Parva programs you had been asked to write? There were some
interesting wrong answers (guesses). One does not need parentheses around the expression in a
return statement, for example.

(d) How could you correct it so that it would compile and execute satisfactorily? No, you don't have to rewrite it completely. The answer is very simple! [2]

       int reverse(int n) {
       // Returns the number formed by reversing the decimal digits of n
         int r = 0;
         while (n != 0) {
           r = 10 * r + n - n / 10 * 10; // order important!
           n = n / 10;
         }
         return r;
       }

       bool isPalindrome(int n) {
       // Returns true if and only if n is a decimal palindrome like 123231
         return n == reverse(n);
       }

       void main() {
       // Examines a list of integers to see which are palindromes
         int n;
         read("Supply n (0 stops) ", n);
         while (n != 0) {
           write(isPalindrome(n));
           read(n);
         }
       }

(e) What do you suppose is the Big Idea I am trying to get across to you in this question? [1]

That all programs should have adequate commentary and well chosen identifiers. I am sure that had
I added commentary and given the methods decent names you would immediately have "understood"
what was intended.

8. A little assembler course is being tutored by a future famous computer scientist in a building not a million kilometres away from Hamilton. The course requires students to be able to write code like the following:

         BEG
         INI               ;  read a value into accumulator
         SUB    AGE        ;  subtract value stored at AGE
         BZE    EXIT       ;
         OTI               ;  if non-zero, output as unsigned number
   EXIT  HLT               ;  terminate execution
   AGE   DC     64         ;  now you know
         END

but the tutor does not have a real machine on which to run this. So he has been developing a little emulator for the machine, using Java as the host language. He got as far as the following code when he was distracted by the other activities on Wednesday night.

How do you suppose the code should be completed for emulating the missing opcodes (that is, complete the "case" arms for OTI, SUB, and BZE and correct the arm for INI which may look correct, but is not) [8]

  class Processor {  // simulated 8-bit processor; two's complement
    int
      a,        // accumulator
      pc,       // program counter
      sp,       // stack pointer
      ir;       // instruction register
    boolean
      z,        // Z flag (last result was zero)
      n,        // N flag (last result was negative)
      running;  // running/halted
    }

    static Processor cpu = new Processor;           // the processor itself
    static int[] mem = new int[256];                // 256 bytes of simulated memory

    static void setFlags(int x) {                   // set condition flags
      cpu.z = x == 0;
      cpu.n = x >= 128;
    } // setFlags

    static void interpret () {
      cpu.pc = 0;
      cpu.a  = 0;
      cpu.sp = 0;
      cpu.running = true;
      while (cpu.running) {
        cpu.ir = mem[cpu.pc++];                      // fetch; bump program counter
        switch (cpu.ir) {                            // dispatch; execute
          case HLT :                                 // halt
            cpu.running = false;
            break;
          case INI :                                 // read integer from standard input
            cpu.a = IO.readInt();
/* */       cpu.a = (cpu.a % 256 + 256) % 256;       // force into range (including negative)
            setFlags(cpu.a);
            break;
          case OTC :                                 // write unsigned integer to standard output
            IO.write(cpu.a);
            break;
          case OTI :                                 // write signed integer to standard output
/* */       if (cpu.a <= 127) IO.writeInt(cpu.a);
/* */       else IO.write(cpu.a - 256);
            break;
          case ADI :                                 // add immediately to accumulator
            cpu.a = (cpu.a + mem[cpu.pc++]) % 256;
            setFlags(cpu.a);
            break;
          case SUB :                                 // subtract variable from accumulator
/* */       cpu.a = (256 + cpu.a - mem[mem[cpu.pc++]]) % 256;
/* */       setFlags(cpu.a);
            break;
          case BRN :                                 // unconditional branch
            cpu.pc = mem[cpu.pc];
            break;
          case BZE :                                 // conditional branch on zero
/* */       if (cpu.z) cpu.pc = mem[cpu.pc];
/* */       else cpu.pc++;
            break;
        }
      }
    } // interpret

It is worth noting that the use of cpu.pc++ in the above code is highly dangerous. If cpu.pc reached the value 255, then cpu.pc++ would take it to 256, which would be "out of range". It is left as an easy exercise to suggest how the code should be modified.

This question was very badly answered, which was disappointing, considering that we had discussed the little machine briefly in class. Several people confused it with a stack machine, for example.

Admittedly the tricks one needs to ensure that the value of CPU.A remains on the range 0..255, and that the range 128..255 represents "negative" values, are a little obtuse. Study the solution carefully, and if you don't understand any of it, come to ask. Many submissions had SUB badly wrong - they were close to SBI not to SUB, and I don't know that anyone had worked out how to get the negative result properly mapped. The code for SBI would have been

          case SBI :                                 // subtract variable from accumulator
/* */       cpu.a = (256 + cpu.a - mem[cpu.pc++]) % 256;
            setFlags(cpu.a);
            break;

Another common mistake was to write

          case BZE :                                 // conditional branch on zero
/* */       if (cpu.a == 0) cpu.pc = mem[cpu.pc];
            else cpu.pc++;
            break;

But in this system BZE acts on the value of the last calculation, not on the value in the accumulator (which does not always hold the value of the last calculation - remember the DEX and CMP opcodes for example?


Home  © P.D. Terry