"""Calculator An interpreter for a calculator language using prefix-order call syntax. Operator expressions must be simple operator names or symbols. Operand expressions are separated by commas. Examples: calc> mul(1, 2, 3) 6 calc> add() 0 calc> add(2, div(4, 8)) 2.5 calc> add SyntaxError: expected ( after add calc> div(5) TypeError: div requires exactly 2 arguments calc> div(1, 0) ZeroDivisionError: division by zero calc> ^DCalculation completed. """ from ucb import trace, main, interact from functools import reduce from operator import mul try: import readline # Enables access to previous expressions in the REPL except ImportError: pass # Readline is not necessary; it's just a convenience def read_eval_print_loop(): """Run a read-eval-print loop for calculator.""" while True: try: expression_tree = calc_parse(input('calc> ')) print(calc_eval(expression_tree)) except (SyntaxError, TypeError, ZeroDivisionError) as err: print(type(err).__name__ + ':', err) except (KeyboardInterrupt, EOFError): # -D, etc. print('Calculation completed.') return # Eval & Apply class Exp(object): """A call expression in Calculator. >>> Exp('add', [1, 2]) Exp('add', [1, 2]) >>> str(Exp('add', [1, Exp('mul', [2, 3])])) 'add(1, mul(2, 3))' """ def __init__(self, operator, operands): self.operator = operator self.operands = operands def __repr__(self): return 'Exp({0}, {1})'.format(repr(self.operator), repr(self.operands)) def __str__(self): operand_strs = ', '.join(map(str, self.operands)) return '{0}({1})'.format(self.operator, operand_strs) def calc_eval(exp): """Evaluate a Calculator expression. >>> calc_eval(Exp('add', [2, Exp('mul', [4, 6])])) 26 """ if type(exp) in (int, float): return exp if type(exp) == Exp: arguments = list(map(calc_eval, exp.operands)) return calc_apply(exp.operator, arguments) def calc_apply(operator, args): """Apply the named operator to a list of args. >>> calc_apply('+', [1, 2, 3]) 6 >>> calc_apply('-', [10, 1, 2, 3]) 4 >>> calc_apply('*', []) 1 >>> calc_apply('/', [40, 5]) 8.0 """ if operator in ('add', '+'): return sum(args) if operator in ('sub', '-'): if len(args) == 0: raise TypeError(operator + ' requires at least 1 argument') if len(args) == 1: return -args[0] return sum(args[:1] + [-arg for arg in args[1:]]) if operator in ('mul', '*'): return reduce(mul, args, 1) if operator in ('div', '/'): if len(args) != 2: raise TypeError(operator + ' requires exactly 2 arguments') numer, denom = args return numer/denom # Parsing def calc_parse(line): """Parse a line of calculator input and return an expression tree.""" tokens = tokenize(line) expression_tree = analyze(tokens) if len(tokens) > 0: raise SyntaxError('Extra token(s): ' + ' '.join(tokens)) return expression_tree def tokenize(line): """Convert a string into a list of tokens. >>> tokenize('add(2, mul(4, 6))') ['add', '(', '2', ',', 'mul', '(', '4', ',', '6', ')', ')'] """ spaced = line.replace('(',' ( ').replace(')',' ) ').replace(',', ' , ') return spaced.strip().split() known_operators = ['add', 'sub', 'mul', 'div', '+', '-', '*', '/'] def analyze(tokens): """Create a tree of nested lists from a sequence of tokens. Operand expressions can be separated by commas, spaces, or both. >>> analyze(tokenize('add(2, mul(4, 6))')) Exp('add', [2, Exp('mul', [4, 6])]) >>> analyze(tokenize('mul(add(2, mul(4, 6)), add(3, 5))')) Exp('mul', [Exp('add', [2, Exp('mul', [4, 6])]), Exp('add', [3, 5])]) """ assert_non_empty(tokens) token = analyze_token(tokens.pop(0)) if type(token) in (int, float): return token if token in known_operators: if len(tokens) == 0 or tokens.pop(0) != '(': raise SyntaxError('expected ( after ' + token) return Exp(token, analyze_operands(tokens)) else: raise SyntaxError('unexpected ' + token) def analyze_operands(tokens): """Analyze a sequence of comma-separated operands.""" assert_non_empty(tokens) operands = [] while tokens[0] != ')': if operands and tokens.pop(0) != ',': raise SyntaxError('expected ,') operands.append(analyze(tokens)) assert_non_empty(tokens) tokens.pop(0) # Remove ) return operands def assert_non_empty(tokens): """Raise an exception if tokens is empty.""" if len(tokens) == 0: raise SyntaxError('unexpected end of line') def analyze_token(token): """Return the value of token if it can be analyzed as a number, or token. >>> analyze_token('12') 12 >>> analyze_token('7.5') 7.5 >>> analyze_token('add') 'add' """ try: return int(token) except (TypeError, ValueError): try: return float(token) except (TypeError, ValueError): return token @main def run(): read_eval_print_loop() interact()