RPN Calculator in Pop-11

The program consists of two procedures, maptoken and evalrpn, and a main part. The latter is a loop in which the input is being read character-wise, calling charin. End of data is signified by the special value termin appearing as the result of charin.

A sequence of characters is accumulated on the stack until an end-of-line is seen. By counting its members (stacklength), the sequence is packed into a string (consstring), which then is split into a series of non-blank tokens (sys_parse_string), of which, again by counting the members, a list is constructed (conslist). Then, if non-empty, the list is passed to evalrpn for evaluating the RPN expression.

Calling ncmaplist with maptoken, evalrpn transforms each string in the list into a number or an operator. Operators are represented as words, which in Pop's parlance is what other languages call symbols. Once a list of properly typed tokens is obtained, triples x, y, o of types number, number, and word, correspondingly, are searched for in it, for each triple the operation in the word (valof(o)) is performed on the two aguments, and its result replaces the three items by constructing a new, shorter, list from the unused pieces p and q of the current one (:: is the list constructor, and <> is the catenator of sequences). When no further (or any at all) reductions of this kind can be made on the list representing the RPN expression, there must only be a single number in it; otherwise the expression is incorrect.

Note that evalrpn receives its argument on the stack implicitly. When searching for operations that can be computed, the (reference of) the list is duplicated on the stack. One of the copies is consumed by the matching test. If that test succeeds, the second copy is popped off (->) – it is not needed as the constituents of the list are stored in the matching variables. When the match fails, the second copy is used for the final test.

We want to capture and handle possible syntax errors – invalid tokens, or insufficient number of arguments for an operation, or too few operators. That is why evalrpn is called indirectly, within a call to catch: the latter is a library procedure providing a guarded-against-exception context. Calling throw while executing evalrpn generates an exception which is handled using the second and the third arguments of catch. The third argument is a value, serving as a label: the same value must be used as an argument to throw in order the exception to be caught, just like throw is a non-local jump to a point within catch labeled by the value in question. The '' here is chosen arbitrarily, any other one will do. The second argument to catch is the handler itself, which in our case is simply the string value 'error' that gets pushed onto the stack. Thus, any exception occuring in the program results in placing the string 'error' onto the stack.

The outcome produced by catch, be it the result of a successful expression evaluation or 'error', is printed out calling npr. After that, in preparation for reading the next line, the stack contents are purged (the stack may be non-empty in case of erroneous input).

As already pointed out, the role of the maptoken procedure is to tell a string representing a number from one which is an operator, to convert each kind of token into a value of the corresponding type, and to raise an exception when the token appears to be of neither of the two kinds. For producing a number from a string, Pop-11 has a procedure of its own, called strnumber. That procedure, however, does not fit our needs in two ways. On the one hand, it does not handle leading and trailing decimal dots, and it also would not accept numbers with a + sign (although - is accepted). On the other hand, strnumber treats as numbers some strings that, according to our specification, should not be accepted; they represent rational and complex numbers in Pop-11. Consequently, maptoken makes use of strnumber, but only after insuring that its argument is in the expected form for the latter procedure, and getting rid of strings that are not correct numbers for us. To that end: an arithmetic sign, if present, is stored as a factor ±1 and is removed from the string; a leading or a trailing 0 may be added to ‘hide’ a decimal dot; strings containing a _ character (used for both rational and complex numbers in Pop-11) are discarded.

(As a parser of numbers, we could have employed regular expressions, but these are only provided in a restricted form in Pop-11, and in fact, using them would not lead to a simpler solution of the problem.)

A token found incorrect leads to raising an exception in maptoken. Note that a string can be found incorrect by the explicit checks conducted within the procedure, but also by the call of strnumber, which in this case returns false instead of a number. When it does return a number, the condition of the if-statement is true, and the number is multiplied by the arithmetic sign waiting on the stack.

The program exhibits a style of programming that is unique to Pop-11: a blend of traditional, as seen in other languages, procedure calling with explicit arguments, and stack-based, implicit parameter passing. For any procedure, the formal parameters in its definition, the actual arguments of any of its calls, or both may be left implicit. It is due to this implicitness that there are only a few variables in the program: in fact, all but one are the ones used for capturing in the pattern matching. On the other hand, wherever using a value on the stack without losing that value is needed, the dup procedure is called to create a new copy of the value.

Computations with monadic or dyadic operators can also be expressed implicitly with respect to their arguments, an example of which can be seen near the end of maptoken: by the time the multiplication must be performed both its arguments are on the stack already, and consequently the * can be applied.

Also a distinguishing feature of the language, observable in the program, is the syntax variability of procedure calls due to the dot notation. For example, where there is .dup.length, there could have been  dup(); length() or  length(dup()), but the used form is shorter, more easily readable, and perhaps most clearly conveys the idea of function composition. (Speaking of composition, it can also be expressed explicitly by the <> operator, in this case leading us to (dup<>length)() or  .(dup<>length), each one equivalent to the above three expressions.)

The -> operator has a left argument, which is often implicit as the topmost stack element, and an optional right argument. Without the latter, -> simply drops its left argument from the stack. If present, the right argument must be a variable or, possibly, what is called ‘an updater’ of some indirectly referenced value: thus, a -> with a right argument is an assignment operation. Both the forms with and without a right argument are made use of in the program.

define maptoken(s);
  if length(s)=1 and strmember(s(1),'+-*/')
  then return(subword(1,1,s)) endif;
  if s(1)=`+` or s(1)=`-` then
    if s(1)=`-` then ->; -1.0 endif;
    substring(2,length(s)-1,s) -> s
  if strmember(`_`,s) or length(s)=0 or
     not(isnumbercode(s(1))) and (length(s)=1 or s(1)/=`.`)
  then throw('') endif;
  if s(1)=`.` then '0'<>s -> s endif;
  if s.last=`.` then s<>'0' -> s endif;
  if strnumber(s).dup then * else throw('') endif

define evalrpn();
  while .dup matches [??p ?x:isnumber ?y:isnumber ?o:isword ??q]
    do ->; p<>valof(o)(x,y)::q endwhile;
  if matches [?x:isnumber] then x else throw('') endif

until charin().dup=termin do
  until .dup=`\n` do charin() enduntil;
  if .dup.length>0 then catch(evalrpn,'error','').npr endif;