############################################################################ # # File: icalc.icn # # Subject: Program to simulate infix desk calculator # # Author: Stephen B. Wampler # # Date: January 3, 1994 # ############################################################################ # # This file is in the public domain. # ############################################################################ # # This is a simple infix calculator with control structures and # compound statements. It illustrates a technique that can be # easily used in Icon to greatly reduce the performance cost # associated with recursive-descent parsing with backtracking. # There are numerous improvements and enhancements that can be # made. # # Features include: # # - integer and real value arithmetic # - variables # - function calls to Icon functions # - strings allowed as function arguments # - unary operators: # + (absolute value), - (negation) # - assignment: # := # - binary operators: # +,-,*,/,%,^, # - relational operators: # =, !=, <, <=, >, >= # (all return 1 for true and 0 for false) # - compound statements in curly braces with semicolon separators # - if-then and if-then-else # - while-do # - limited form of multiline input # # The grammar at the start of the 'parser' proper provides more # details. # # Normally, the input is processed one line at a time, in calculator # fashion. However, compound statements can be continued across # line boundaries. # # Examples: # # Here is a simple input: # # { # a := 10; # while a >= 0 do { # write(a); # a := a - 1 # }; # write("Blastoff") # } # # (execution is delayed until entire compound statement is entered) # # Another one: # # write(pi := 3.14159) # write(sin(pi/2)) # # (execution done as each line is entered) # ############################################################################ invocable all # the types for parse tree nodes: record trinary(op,first,second,third) record binop(op,left,right) record unary(op,opnd) record id(name) record const(value) # a global table for holding variable values: global sym_tab procedure main() local line, sline sym_tab := table() every line := getbs() do { # a 'line' may be more # than one input line if *(sline := trim(line)) > 0 then { # skip empty lines process(parse(sline)) } } end ### Input routines... ## getbs - read enough input to ensure that it is # balanced with respect to curly braces, allowing # compound statements to extend across lines... # This can be made considerably more sophisticated, # but handles the more common cases. # procedure getbs() static tmp initial tmp := (("" ~== |read()) || " ") | fail repeat { while not checkbal(tmp,'{','}') do { if more('}','{',tmp) then break tmp ||:= (("" ~== |read()) || " ") | break } suspend tmp tmp := (("" ~== |read()) || " ") | fail } end ## checkbal(s) - quick check to see if s is # balanced w.r.t. braces or parens # procedure checkbal(s,l,r) return (s ? 1(tab(bal(&cset,l,r)),pos(-1))) end ## more(c1,c2,s) - succeeds if any prefix of # s has more characters in c1 than # characters in c2, fails otherwise # procedure more(c1,c2,s) local cnt cnt := 0 s ? while (cnt <= 0) & not pos(0) do { (any(c1) & cnt +:= 1) | (any(c2) & cnt -:= 1) move(1) } return cnt >= 0 end ### Parser routines... Implementing an efficient recursive-descent ### parser with backtracking. # Parser -- Based on following CFG, but modified to # avoid useless backtracking... (see comments # preceding procedures 'save' and 'restore') # Statement ::= Expr | If | While | Compound # # Compound ::= {Statement_list} # # Statement_list ::= Statement | Statement ; Statement_list # # If ::= if Expr then Statement Else # # Else ::= else Statement | "" # # While ::= while Expr do Statement # # Expr ::= R | Id := Expr # # R ::= X [=,!=,<,>,>=,<=] X | X # # X ::= T [+-] X | T # # T ::= F [*/%] T | F # # F ::= E ^ F | E # # E ::= L | [+,-] L # # L ::= Func | Id | Constant | ( Expr ) | String # # Func ::= Id ( Arglist ) # # Arglist ::= "" | Expr | Expr , arglist # # Note, this version correctly handles left-associativity # despite the fact that the above grammar doesn't # handle it correctly. (Cannot embed left-associativity # into a recursive descent parser!) # procedure parse(s) # must match entire line local tree if s ? ((tree := Statement()) & (ws(),pos(0))) then { return tree } write("Syntax error.") end procedure Statement() suspend If() | While() | Compound() | Expr() end procedure Compound() suspend unary("{",2(litmat("{"),Statement_list(),litmat("}"))) end procedure Statement_list() local t t := scan() suspend binary(save(Statement,t), litmat(";"), Statement_list()) | restore(t) end procedure If() suspend trinary(keymat("if"),Expr(),2(keymat("then"),Statement()), 2(keymat("else"),Statement())|&null) end procedure While() suspend binary(2(keymat("while"),Expr()),"while",2(keymat("do"),Statement())) end procedure Expr() suspend binary(Id(),litmat(":="),Expr()) | R() end procedure R() local t t := scan() suspend binary(save(X,t),litmat(!["=","!=","<=",">=","<",">"]),X()) | restore(t) end procedure X() local t t := scan() suspend binary(save(T,t),litmat(!"+-"),X()) | restore(t) end procedure T() local t t := scan() suspend binary(save(F,t),litmat(!"*/%"),T()) | restore(t) end procedure F() local t t := scan() suspend binary(save(E,t),litmat("^"),F()) | restore(t) end procedure E() suspend unary(litmat(!"+-"),L()) | L() end procedure L() # keep track of fact expression was parenthesized, # so we don't accidently override the parens when # handling left-associativity suspend Func() | Id() | Const() | unary("(",2(litmat("("), Expr(), litmat(")"))) | String() end procedure Func() suspend binary(Id(),litmat("("),1(Arglist(),litmat(")"))) end procedure Arglist() local a a := [] suspend (a <- ([Expr()] | [Expr()] ||| 2(litmat(","),Arglist()))) | a end procedure Id() static first, rest initial { first := &letters ++ "_" rest := first ++ &digits } suspend 2(ws(),id(tab(any(first))||tab(many(rest)) | tab(any(first)))) end procedure Const() local t t := scan() suspend 2(ws(),const((save(digitseq,t)||="."||digitseq()) | restore(t))) end procedure digitseq() suspend tab(many(&digits)) end procedure String() # can be MUCH smarter, see calc.icn (by Ralph Griswold) for # example of how to do so... suspend 2(litmat("\""),tab(upto('"')),move(1)) end procedure litmat(s) suspend 2(ws(),=s) end procedure keymat(key) suspend 2(ws(),key==tab(many(&letters))) end procedure ws() static wsp initial wsp := ' \t' suspend ""|tab(many(wsp)) end procedure binary(l,o,r) local lm # if operator is left-associative, then alter tree to # reflect that fact, since it isn't parsed that way # (this isn't the most efficient way to do this, but # it is a simple way...) if (type(r) == "binop") & samelop(o,r.op) then { # ok, have to add node to far left end of chain for r # ...do so by first finding leftmost node of chain for r lm := r while (type(lm.left) == "binop") & samelop(o,lm.left.op) do { lm := lm.left } # ...add new node as new left-most node in chain lm.left := binop(o,l,lm.left) # ...and return original right child as root of tower return r } # nothing to do, just return 'normal' tree return binop(o,l,r) end procedure samelop(o1,o2) # both operators are left associative at the same precedence level return (any('+-',o1) & any('+-',o2)) | (any('*/%',o1) & any('*/%',o2)) end ## Speed up tools for recursive descent parsing... # # The following two routines make it possible to 'defer' # the backtracking into a parsing procedure (at least # so far as restoring &pos). This makes it easy to # reuse the result of a parsing procedure if needed. # # For example, the grammar rules: # # X := T | T + F # # can be processed as: # # X := save(T,t) | restore(t) + F # # The net effect is a very substantial speedup in processing # such rules. # record scan(val,pos) # used to avoid repeating a successful scan # (see the use of save() and restore()) # save the current scanning position and result of parsing procedure P # and then prevent backtracking into P # procedure save(P,t) return (t.pos <- &pos, t.val := P()) end # # if t has in it the saved result of a parsing procedure, then # suspend it. if backtracked into reset position back to # start of original call to that parsing procedure. # procedure restore(t) suspend \t.val &pos := \t.pos end ### execution of infix expression... ## process -- given an expression tree - walk it to produce a result # # The only tricky part is in the assignment operator. # Here, since we know the left-hand side is an identifier # We avoid processing it, since process(id(name)) will # return the value of id(name), not it's address. # This version just relies upon the icon interpreter to # catch runtime errors. It would be better to catch them # here. procedure process(t) local a, val return case type(t) of { "trinary" : case t.op of { # has to be an 'if'! "if": if process(t.first) ~= 0 then process(t.second) else process(t.third) } "binop" : case t.op of { # the relation operators "=" : if process(t.left) = process(t.right) then 1 else 0 "!=": if process(t.left) ~= process(t.right) then 1 else 0 "<=": if process(t.left) <= process(t.right) then 1 else 0 ">=": if process(t.left) >= process(t.right) then 1 else 0 "<" : if process(t.left) < process(t.right) then 1 else 0 ">" : if process(t.left) > process(t.right) then 1 else 0 # the arithmetic operators "+" : process(t.left) + process(t.right) "-" : process(t.left) - process(t.right) "*" : process(t.left) * process(t.right) "/" : process(t.left) / process(t.right) "%" : process(t.left) % process(t.right) "^" : process(t.left) ^ process(t.right) # assignment ":=": sym_tab[t.left.name] := process(t.right) # statements in a statement list ";" : { process(t.left) process(t.right) } # while loop "while" : while process(t.left) ~= 0 do process(t.right) # function calls "(" : t.left.name ! process(t.right) } "unary" : case t.op of { "-" : -process(t.opnd) "+" : if val := process(t.opnd) then return if val < 0 then -val else val # parenthesized expression "(" : process(t.opnd) # compound statement "{" : process(t.opnd) } "id" : \sym_tab[t.name] | (write(t.name," is undefined!"),&fail) "const" : numeric(t.value) "list" : { # argument list for function call # evaluate each argument into a new list a := [] every put(a,process(!t)) a } default: t # anything else (right now, just strings) } end