RPN Calculator in CLU

The RPN calculator implementation in CLU consists of a main procedure and an iterator. The iterator, gettoken, reads the input and generates a sequence of tokens. The rest is done by the main program. Substantial use is made of exception handling, both for catching errors and for control structuring.

The language definition says that the starting point of the program can be any procedure, but the compiler I used (Portable CLU) requires that the name of the main procedure is start_up, and so it is here.

A dozen of library procedures are used in the program. As all of them belong to specific types, such as int, real, string, stream, etc., according to the language's rules each procedure name is prefixed by the corresponding type name and a $, e.g. string$size.

Type synonyms are introduced for several types by means of the so called ‘equates’. These are useful for brevity of expression. One of the equates is for a sequence, i.e. an immutable array, of procedures. The constant value op of that type holds the procedures for the operators +, -, *, and /, and is used to easily refer to the needed procedure by first mapping a token to the corresponding index within op.

The body of the iterator is an infinite loop, in which the input is being read character by character. Sequences of successive nonblank characters are stored in the array cs. As soon as such a sequence is completed, cs is copied in the string w, which is then yielded by the iterator. An empty string is yielded wherever an end of line is encountered.

The main procedure utilizes the iterator in its for loop. It gets a token s yielded by gettoken, and if s is non-empty, it proceeds to see whether s is a number or an operator, acting accordingly in each case. An array, rs, is used as a stack to accumulate intermediate results as the tokens are processed. Once a complete expression is evaluated, its value is converted to a string by calling real$unparse, because numbers cannot be printed directly.

Upon end of input, an exception is raised from stream$getc within gettoken, which ends the iterator's loop and thus the iterator itself. Ultimately, this leads to ending the main procedure as well.

Statements of the form S except when E: A end, where S is a statement, E is a list of exception names, and A is one or more statements, handle exceptions: if an exception is raised within S whose name is in E, it gets handled by executing A. For example, trying to parse a token as a number (real$parse(s)) raises bad_format if the token is not really a number. Similarly, popping a value off rs (ar$remh(rs)) raises bounds if rs is empty. bounds is also raised if we try to index op with an invalid number.

The exit statement raises an exception explicitly. A user chosen name must be provided for the exception. Here, the name error is used at several places.

When an error is detected in the course of evaluating an expression, the main (and only) loop of the main procedure has to skip some tokens without interpreting them, until an empty one is encountered, signalling end of the current line. Then it prints an error message and continues its normal work. Whether tokens are interpreted or just skipped is controlled by the value of the variable skip.

ac = array[char]
ai = array[int]
ar = array[real]
ap = sequence[proctype(real,real) returns(real)

start_up = proc()
  op:ap := ap$[real$add,real$sub,real$mul,real$div]
  ous:stream := stream$primary_output()
  rs:ar := ar$new()
  skip:bool := false
  for s:string in gettoken() do
    if skip then exit error
    elseif string$empty(s) then
      if 1<ar$size(rs) then exit error
      elseif ~ar$empty(rs) then
        y := ar$remh(rs)
      x := real$parse(s)
      except when bad_format:
        if 1=string$size(s) then
          y,x := ar$remh(rs),ar$remh(rs)
          x := op[string$indexc(s[1],"+-*/")](x,y)
        else exit error end
    except when error,bounds,overflow,underflow,zero_divide:
      skip := ~string$empty(s)
      if ~skip then
end start_up

gettoken = iter() yields (string)
  ins:stream := stream$primary_input()
  cs:ac := ac$new()
  tk:string  c:char := ' '
  while true do
    while c=' ' cor c='\t' do
      c := stream$getc(ins)
    if c='\n' then c := ' ' end
    while 0=string$indexc(c," \t\n") do
      c := stream$getc(ins)
    tk := string$ac2s(cs)
    yield (tk)
  except when end_of_file: end
end gettoken