RPN Calculator in OmniMark

The main procedure of the program is the process rule. The inner repeat scanagain loop in it calls the function rpn which reads a line of text trying to match its contents against the syntax of an RPN expression and simultaneously evaluate the expression. Reading and matching take place in pieces. In contrast to many other implementations of the RPN calculator, there is no separate phase of tokenizing the input.

Individual tokens are extracted by means of pattern matching against regular expressions. In particular, numbers are matched by the nump pattern, defined as a macro. We make sure that each token is preceded by at least one blank character, with the possible exception of a number starting right at the beginning of a line.

rpn is a ‘switch’ function. This word designates the Boolean type in OmniMark, just as ‘stream’ means ‘a string’ and ‘shelf’ translates as ‘list’ or ‘array’. rpn repetitively scans a line, trying to find a number or an arithmetic operation, in the succession specified by the match clauses. A number token is captured in the string sn and then the number itself is obtained (calling bcd) and pushed on the stack contained within the nums shelf, which by declaration is of variable size. Tokens that match one of +, -, *, and / lead to evaluating the corresponding operation on the top two elements of the stack. Matching anything else, or finding the stack empty when trying to pop a value from it throws the programmer-defined syntax exception.

If no cause for throwing an exception is found, when rpn reaches the end of a line it returns true. Otherwise the exception is handled by the catch statement, where reading the current line is completed and a false is returned, meaning that rpn ‘did not match’.

If the process rule gets a false from rpn, the application of the next match clause within repeat scan is forced. It re-reads the line rejected (and thus considered unread) by rpn and throws the syntax exception. That exception is also thrown when rpn did match but nums contains more than one number left on it. In the catch clause, the error situation is handled by printing a message and clearing nums in preparation for a new evaluation. The outer repeat loop in the process rule ensures that reading and evaluation continues. Without it, the program would have been terminated once an exception occurs and gets handled by the catch.

Note that end-of-input is not recognized by an explicit check. Instead, when the input is all through, the repeat scan in the rpn function gets exited without doing anything, and thus the halt action is executed to terminate the program.

Non-integer numbers in OmniMark are library-provided and of two types: floating-point and BCD (binary coded decimal). The program uses those of the latter kind. BCD supports unlimited magnitude and limited but large precision of fractional numbers.

include ""

declare #main-input has unbuffered
declare #main-output has unbuffered

macro dgs is (digit+) macro-end
macro nump is (["+-"]? ((dgs ("." dgs?)?) | ("." dgs))) macro-end

declare catch syntax

global bcd nums variable initial-size 0

define function push(value bcd x) as  set new nums to x

define bcd function pop() as
  local bcd x
  throw syntax when number of nums = 0
  set x to nums  remove nums
  return x

define switch function rpn as
  local stream sn
  repeat scan #current-input
    match (line-start | blank) blank* nump => sn
    match blank+ "+"   push(pop()+pop())
    match blank+ "-"   push(0-pop()+pop())
    match blank+ "*"   push(pop()*pop())
    match blank+ "/"   push(1/pop()*pop())
    match blank* "%n"  return true
    match any-text     throw syntax
  catch syntax
    do scan #current-input  match any-text* "%n"  done
    return false

process  repeat
  repeat scan #main-input
    match rpn
      local integer n  set n to number of nums
      do when n = 1  output "d" % pop() || "%n"
      else when n > 1  throw syntax
    match any-text*  throw syntax
  catch syntax
    output "error%n"
    clear nums