PLACE
spec
solutions
PostScript

RPN Calculator in PostScript

At first sight, implementing an RPN calculator in PostScript should be very straightforward. Expressions in PostScript are themselves RPN, and strings can be evaluated as programs. What can be simpler than reading lines and directly evaluating each one? In reality, however, we have to parse the input and ensure that it is syntactically correct. Besides, in place of +, -, *, and / PostScript uses add, sub, mul, and div, so even a correct RPN expression cannot be readily evaluated as a PostScript program. Unfortunately, text processing is only barely supported in PostScript. In particular, there is no direct means to split a string into non-blank pieces. Moreover, it is impossible to read entire lines of unbound length. All this means that, despite the apparent structural similarity between the RPN expressions and the implementation language, our original expectation for a very short solution of the problem is unrealistic.

Indeed, half of this RPN calculator implementation deals with reading a text line from the standard input, extracting the non-blank portions of it, making up a string of each such portion, and creating an array of these strings. This is what the read-line and mk-string procedures do.

First, the standard input stream must be opened, using the file command. The handle that file generates is given the name cin, so that it is then used to read bytes one at a time and places the respective values on the data stack until an end-of-line (10) is encountered. Spaces (32) and tabs (9) are skipped, and all else is stored on the data stack. As there is no character datatype in the language, all ‘characters’ that are read are actually ASCII codes, i.e. integers.

A series of non-blank characters (codes) is collected on the stack. A mark object is left by the [ command so that it ts known where such a series begins. As soon as a series is complete, a string is assembled from it by mk-string. The latter procedure creates a string only when the series is non-empty. It also leaves the mark on top of the stack, thus making it possible that another series starts accumulating.

The already built up strings also get collected on the stack, and another mark is used in order to know where the sequence of strings starts. Upon end-of-line an array is created from all the strings by using the ] command, and then the call of read-line concludes.

The parse procedure is responsible for creating actual numbers and operations from the strings that result from reading a line, and for checking the correctness of the RPN expression to be evaluated. If the expression is correct in every respect, a new array is created out of the respective numbers and operators: one that can be executed as a PostScript procedure that computes the RPN expression.

For getting a number or an operator from a string parse makes use of the token procedure. The latter first applies cvr on the string, which tries parsing that string as a number. Calling cvr is done in a guarded manner: if cvr does not accept the string as representing a number, the procedure {cvr} that calls cvr ends by generating an error. In PostScript terms, it gets stopped. The command stopped then checks if this is the case, and if so, the procedure that follows is executed. There, the string that proved to be not a number is used as a key in a get command to fetch an item from the ops dictionary, which maps operator designating strings to the names of the respective PostScript operators. If the call of get is successful, the fetched name is acted upon by load, thus obtaining the callable operator itself. If get fails, again an error is generated. As this means that the tested string happens to read neither as a number nor as an operator, the error condition is reissued by calling stop explicitly, so that token itself ends in a stopped condition.

Emitting a stopped condition from token results in parse also ending immediately as stopped. parse also signals an error by explicitly executing a stop when the sequence of tokens is not correctly formed. Thus all kinds of erroneous input eventually lead to parse ending in a stopped condition.

The calculator commences execution by running the bottommost part of the program, when all needed definitions are in effect. A blank line on input results in an empty array produced by read-line and gets skipped. A non-blank line is parsed, and in the absence of errors the resulting array of numbers and operators receives an executable status through using cvx, and is finally executed. The result — that of evaluating the RPN expression — is output by the = command. If the input line does not contain a correct RPN expression, a stopped condition emerges and = outputs error instead. flushing the buffer of the standard output is necessary, as otherwise the actual printing will be put off instead of taking place right at the moment.

/cin (%stdin) (r) file def

/read-line {                                              % Returns either an array of strings and true, or false.
  [ [                                                     % Start collecting strings, start collecting charcodes.
  { cin read not {2 {cleartomark} repeat false exit} if   % Read a charcode; on EOF return false, removing whatever in the line.
    dup dup dup 32 eq exch 9 eq or exch 10 eq or {        % If char is any of ‘ ’, ‘\t’, or EOL:
      10 eq {                                             %   If char is EOL:
        mk-string pop ] true exit                         %     possibly create a string, return array of strings.
      } {                                                 %   If char is ‘ ’ or ‘\t’:
        mk-string                                         %     possibly create a string.
      } ifelse
    } if
  } loop                                                  % Go to reading another charcode.
} def

/mk-string {                                              % Given [ charcodes, builds up and returns a string.
  counttomark
  dup 0 eq {                                              % If no charcodes collected:
    pop                                                   %   do nothing but remove the counter 0.
  } {                                                     % Otherwise:
    dup string exch                                       %   make an empty string of the needed size
    1 sub -1 0 {1 index exch 4 -1 roll put} for           %   and fill-in the charcodes.
    exch                                                  % Keep the marker atop the resulting array in the stack.
  } ifelse
} def

/parse {                                                  % Parse the line, generating a stop on error detection.
  [ 0 3 -1 roll {                                         % Start an array, set a zero counter, loop over tokens.
    token dup type /operatortype eq {-1} {1} ifelse       % Get a token, ensure that
    3 -1 roll add dup 0 le {pop stop} if                  %   there are more numbers than operations.
  } forall
  1 ne {stop} if                                          % Ensure that after evaluating there remains one number.
  ]                                                       % Return an array of numbers and operations.
} def

/token {                                                  % Given a string, tries to produce a number or an operator.
  {cvr} stopped {                                         % Try parsing a number. If unsuccessful:
    {ops exch get load} stopped {pop pop stop} if         %   Try parsing an op, return an operator, or signal a failure.
  } if
} def

/ops << (+) /add (-) /sub (*) /mul (/) /div >> def

{ read-line not {exit} if                                 % Read a line. On EOF, end the program.
  dup length 0 eq {                                       % If the line is empty,
    pop                                                   %   do nothing.
  } {                                                     % Otherwise
    {parse cvx exec} stopped                              %   parse and execute,
    {cleartomark (error)} if = flush                      %   print result or error message.
  } ifelse
} loop                                                    % Go to reading another line.

boykobbatgmaildotcom