/python/ch0-Arithmetic/v1/expressions.py

https://github.com/ramalho/kaminpy · Python · 189 lines · 187 code · 0 blank · 2 comment · 0 complexity · 866e17d6e9fb43f1cd53959aa1838957 MD5 · raw file

  1. """
  2. Simple interpreter implementing the arithmetic expression subset of
  3. the language described in Chapter 1 of Samuel Kamin's PLIBA book [1].
  4. This Python code is heavily inspired by Peter Norvig's lis.py [2],
  5. but does not try to be as concise.
  6. [1] Samuel Kamin, "Programming Languages, An Interpreter-Based Approach",
  7. Addison-Wesley, Reading, MA, 1990. ISBN 0-201-06824-9.
  8. [2] http://norvig.com/lispy.html
  9. BNF of this mini-language:
  10. <expression> ::= <integer>
  11. | `(` <operator> <expression>* `)`
  12. <operator> ::= `+` | `-` | `*` | `/`
  13. <integer> ::= sequence of digits, possibly preceded by - or +
  14. """
  15. import collections
  16. import operator
  17. import sys
  18. QUIT_COMMAND = '.q'
  19. class InterpreterError(Exception):
  20. """generic interpreter error"""
  21. def __init__(self, value=None):
  22. self.value = value
  23. def __str__(self):
  24. msg = self.__class__.__doc__
  25. if self.value is not None:
  26. msg = msg.rstrip(".")
  27. msg += ": " + repr(self.value) + "."
  28. return msg
  29. class UnexpectedEndOfInput(InterpreterError):
  30. """Unexpected end of input."""
  31. class UnexpectedCloseParen(InterpreterError):
  32. """Unexpected ')'."""
  33. class UnknownOperator(InterpreterError):
  34. """Unknown operator."""
  35. class EvaluationError(InterpreterError):
  36. """Generic evaluation error."""
  37. class OperatorNotCallable(EvaluationError):
  38. """Operator is not callable."""
  39. class NullExpression(EvaluationError):
  40. """Null expression."""
  41. class MissingArgument(EvaluationError):
  42. """Not enough arguments for operator."""
  43. class TooManyArguments(EvaluationError):
  44. """Too many arguments for operator."""
  45. class InvalidOperator(EvaluationError):
  46. """Invalid operator."""
  47. def tokenize(source_code):
  48. """Convert string into a list of tokens."""
  49. return source_code.replace("(", " ( ").replace(")", " ) ").split()
  50. def parse(tokens):
  51. """Read tokens building recursively nested expressions."""
  52. try:
  53. token = tokens.pop(0)
  54. except IndexError as exc:
  55. raise UnexpectedEndOfInput() from exc
  56. if token == "(": # s-expression
  57. ast = []
  58. if len(tokens) == 0:
  59. raise UnexpectedEndOfInput()
  60. while tokens[0] != ")":
  61. ast.append(parse(tokens))
  62. if len(tokens) == 0:
  63. raise UnexpectedEndOfInput()
  64. tokens.pop(0) # pop off ')'
  65. return ast
  66. elif token == ")":
  67. raise UnexpectedCloseParen()
  68. else: # single atom
  69. try:
  70. return int(token)
  71. except ValueError:
  72. return token
  73. Operator = collections.namedtuple("Operator", "symbol function")
  74. OPERATORS = [
  75. Operator("+", operator.add),
  76. Operator("-", operator.sub),
  77. Operator("*", operator.mul),
  78. Operator("/", operator.floordiv),
  79. ]
  80. OPERATOR_MAP = {op.symbol: op for op in OPERATORS}
  81. def evaluate(expression):
  82. """Calculate the value of an expression"""
  83. if isinstance(expression, int): # integer
  84. return expression
  85. elif isinstance(expression, str): # operator
  86. try:
  87. return OPERATOR_MAP[expression]
  88. except KeyError as exc:
  89. raise UnknownOperator(expression) from exc
  90. else: # multi-part expression
  91. if len(expression) == 0:
  92. raise NullExpression()
  93. parts = [evaluate(subexp) for subexp in expression]
  94. op = parts.pop(0)
  95. if isinstance(op, Operator):
  96. if len(parts) == 2:
  97. arg1, arg2 = parts
  98. return op.function(arg1, arg2)
  99. elif len(parts) < 2:
  100. raise MissingArgument(op.symbol)
  101. else:
  102. raise TooManyArguments(op.symbol)
  103. else:
  104. raise InvalidOperator(op)
  105. def repl():
  106. prompt = '>'
  107. pending_lines = []
  108. print(f'To exit, type: {QUIT_COMMAND}', file=sys.stderr)
  109. while True:
  110. # ______________________________ Read
  111. try:
  112. current = input(prompt + ' ').strip(' ')
  113. except EOFError:
  114. break
  115. if current == QUIT_COMMAND:
  116. break
  117. if current == '':
  118. prompt = '...'
  119. continue
  120. pending_lines.append(current)
  121. # ______________________________ Parse
  122. source = ' '.join(pending_lines)
  123. expr = None
  124. try:
  125. expr = parse(tokenize(source))
  126. except UnexpectedEndOfInput:
  127. prompt = '...'
  128. continue
  129. except UnexpectedCloseParen as exc:
  130. print(f'! {exc}')
  131. # ______________________________ Evaluate & Print
  132. if expr is not None:
  133. try:
  134. result = evaluate(expr)
  135. except ZeroDivisionError:
  136. print('! Division by zero.')
  137. except (UnknownOperator, InvalidOperator,
  138. MissingArgument, TooManyArguments,
  139. ) as exc:
  140. print(f'! {exc}')
  141. else:
  142. print(result)
  143. prompt = '>'
  144. pending_lines = []
  145. # ______________________________ Loop
  146. if __name__ == '__main__':
  147. repl()