parser.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. """
  2. This parser will parse the given input and build an expression tree. Grammar
  3. file for the supported mathematical expressions.
  4. """
  5. import os.path
  6. PYBISON_BUILD = os.path.realpath('build/external/pybison')
  7. EXTERNAL_MODS = os.path.realpath('external')
  8. import sys
  9. sys.path.insert(0, PYBISON_BUILD)
  10. sys.path.insert(1, EXTERNAL_MODS)
  11. from pybison import BisonParser, BisonSyntaxError
  12. from graph_drawing.graph import generate_graph
  13. from node import ExpressionNode as Node, ExpressionLeaf as Leaf, OP_MAP, \
  14. TOKEN_MAP, TYPE_OPERATOR, OP_COMMA, OP_NEG, OP_MUL, Scope
  15. from rules import RULES
  16. from possibilities import filter_duplicates, pick_suggestion, apply_suggestion
  17. import Queue
  18. # Check for n-ary operator in child nodes
  19. def combine(op, op_type, *nodes):
  20. # At least return the operator.
  21. res = [op]
  22. for n in nodes:
  23. # Merge the children for all nodes which have the same operator.
  24. if n.type == TYPE_OPERATOR and n.op == op_type:
  25. res += n.nodes
  26. else:
  27. res.append(n)
  28. return res
  29. class Parser(BisonParser):
  30. """
  31. Implements the calculator parser. Grammar rules are defined in the method
  32. docstrings. Scanner rules are in the 'lexscript' attribute.
  33. """
  34. # Output directory of generated pybison files, including a trailing slash.
  35. buildDirectory = PYBISON_BUILD + '/'
  36. # ----------------------------------------------------------------
  37. # lexer tokens - these must match those in your lex script (below)
  38. # ----------------------------------------------------------------
  39. # TODO: add a runtime check to verify that this token list match the list
  40. # of tokens of the lex script.
  41. tokens = ['NUMBER', 'IDENTIFIER', 'NEWLINE', 'QUIT', 'RAISE', 'GRAPH', \
  42. 'LPAREN', 'RPAREN'] + TOKEN_MAP.values()
  43. # ------------------------------
  44. # precedences
  45. # ------------------------------
  46. precedences = (
  47. ('left', ('COMMA', )),
  48. ('left', ('MINUS', 'PLUS')),
  49. ('left', ('TIMES', 'DIVIDE')),
  50. ('left', ('NEG', )),
  51. ('right', ('POW', )),
  52. )
  53. interactive = 0
  54. def __init__(self, **kwargs):
  55. BisonParser.__init__(self, **kwargs)
  56. self.interactive = kwargs.get('interactive', 0)
  57. self.timeout = kwargs.get('timeout', 0)
  58. self.reset()
  59. def reset(self):
  60. self.read_buffer = ''
  61. self.read_queue = Queue.Queue()
  62. #self.subtree_map = {}
  63. self.root_node = None
  64. self.possibilities = self.last_possibilities = []
  65. def run(self, *args, **kwargs):
  66. self.reset()
  67. return super(Parser, self).run(*args, **kwargs)
  68. # Override default read method with a version that prompts for input.
  69. def read(self, nbytes):
  70. if self.file == sys.stdin and self.file.closed:
  71. return ''
  72. if not self.read_buffer and not self.read_queue.empty():
  73. self.read_buffer = self.read_queue.get_nowait() + '\n'
  74. if self.read_buffer:
  75. read_buffer = self.read_buffer[:nbytes]
  76. self.read_buffer = self.read_buffer[nbytes:]
  77. return read_buffer
  78. try:
  79. read_buffer = raw_input('>>> ' if self.interactive else '') + '\n'
  80. except EOFError:
  81. return ''
  82. self.read_buffer = read_buffer[nbytes:]
  83. return read_buffer[:nbytes]
  84. def hook_read_before(self):
  85. if self.possibilities:
  86. if self.interactive: # pragma: nocover
  87. print 'possibilities:'
  88. items = filter_duplicates(self.possibilities)
  89. self.last_possibilities = self.possibilities
  90. if self.interactive: # pragma: nocover
  91. print ' ' + '\n '.join(map(str, items))
  92. def hook_read_after(self, data):
  93. """
  94. This hook will be called when the read() method returned. The data
  95. argument points to the data read by the read() method. This hook
  96. function should return the data to be used by the parser.
  97. """
  98. if not data.strip():
  99. return data
  100. self.possibilities = []
  101. import re
  102. # TODO: remove this quick preprocessing hack. This hack enables
  103. # concatenated expressions, since the grammar currently does not
  104. # support those. This workaround will replace:
  105. # - ")(" with ")*(".
  106. # - "a(" with "a*(".
  107. # - ")a" with ")*a".
  108. # - "ab" with "a*b".
  109. # - "4a" with "4*a".
  110. # - "a4" with "a^4".
  111. pattern = ('(?:(\))\s*(\()' # match: )( result: ) * (
  112. + '|([a-z0-9])\s*(\()' # match: a( result: a * (
  113. + '|(\))\s*([a-z0-9])' # match: )a result: ) * a
  114. + '|([a-z])\s*([a-z]+)' # match: ab result: a * b
  115. + '|([0-9])\s*([a-z])' # match: 4a result: 4 * a
  116. + '|([a-z])\s*([0-9])' # match: a4 result: a ^ 4
  117. + '|([0-9])\s+([0-9]))') # match: 4 4 result: 4 * 4
  118. def preprocess_data(match):
  119. left, right = filter(None, match.groups())
  120. # Filter words (otherwise they will be preprocessed as well)
  121. if left + right in ['graph', 'raise']:
  122. return left + right
  123. # If all characters on the right are numbers. e.g. "a4", the
  124. # expression implies exponentiation. Make sure ")4" is not
  125. # converted into an exponentiation, because that's multiplication.
  126. if left != ')' and not 48 <= ord(left) < 58 \
  127. and all(map(lambda x: 48 <= ord(x) < 58, list(right))):
  128. return '%s^%s' % (left, right)
  129. # match: ab | abc | abcd (where left = "a")
  130. return '*'.join([left] + list(right))
  131. # Iteratively replace all matches.
  132. while True:
  133. data_after = re.sub(pattern, preprocess_data, data)
  134. if data == data_after:
  135. break
  136. if self.verbose: # pragma: nocover
  137. print 'hook_read_after() modified the input data:'
  138. print 'before:', data.replace('\n', '\\n')
  139. print 'after :', data_after.replace('\n', '\\n')
  140. data = data_after
  141. return data
  142. def hook_handler(self, target, option, names, values, retval):
  143. if target in ['exp', 'line', 'input'] or not retval:
  144. return retval
  145. if not retval.negated and retval.type != TYPE_OPERATOR:
  146. return retval
  147. if retval.type == TYPE_OPERATOR and retval.op in RULES:
  148. handlers = RULES[retval.op]
  149. else:
  150. handlers = []
  151. if retval.negated:
  152. handlers = RULES[OP_NEG]
  153. for handler in handlers:
  154. possibilities = handler(retval)
  155. self.possibilities.extend(possibilities)
  156. return retval
  157. def display_hint(self):
  158. print pick_suggestion(self.last_possibilities)
  159. def display_possibilities(self):
  160. print '\n'.join(map(str, self.last_possibilities))
  161. def rewrite(self):
  162. suggestion = pick_suggestion(self.last_possibilities)
  163. if self.verbose:
  164. print 'applying suggestion:', suggestion
  165. if not suggestion:
  166. return self.root_node
  167. expression = apply_suggestion(self.root_node, suggestion)
  168. if self.verbose:
  169. print 'After application, expression=', expression
  170. self.read_queue.put_nowait(str(expression))
  171. return expression
  172. #def hook_run(self, filename, retval):
  173. # return retval
  174. # ---------------------------------------------------------------
  175. # These methods are the python handlers for the bison targets.
  176. # (which get called by the bison code each time the corresponding
  177. # parse target is unambiguously reached)
  178. #
  179. # WARNING - don't touch the method docstrings unless you know what
  180. # you are doing - they are in bison rule syntax, and are passed
  181. # verbatim to bison to build the parser engine library.
  182. # ---------------------------------------------------------------
  183. # Declare the start target here (by name)
  184. start = 'input'
  185. def on_input(self, target, option, names, values):
  186. """
  187. input :
  188. | input line
  189. """
  190. if option == 1:
  191. # Interactive mode is enabled if the term rewriting system is used
  192. # as a shell. In that case, it is useful that the shell prints the
  193. # output of the evaluation.
  194. if self.interactive and values[1]: # pragma: nocover
  195. print values[1]
  196. return values[1]
  197. def on_line(self, target, option, names, values):
  198. """
  199. line : NEWLINE
  200. | exp NEWLINE
  201. | debug NEWLINE
  202. | HINT NEWLINE
  203. | POSSIBILITIES NEWLINE
  204. | REWRITE NEWLINE
  205. | RAISE NEWLINE
  206. """
  207. if option == 1: # rule: EXP NEWLINE
  208. self.root_node = values[0]
  209. return values[0]
  210. if option == 2: # rule: DEBUG NEWLINE
  211. self.root_node = values[0]
  212. return values[0]
  213. if option == 3: # rule: HINT NEWLINE
  214. self.display_hint()
  215. return
  216. if option == 4: # rule: POSSIBILITIES NEWLINE
  217. self.display_possibilities()
  218. return
  219. if option == 5: # rule: REWRITE NEWLINE
  220. self.root_node = self.rewrite()
  221. return self.root_node
  222. if option == 6:
  223. raise RuntimeError('on_line: exception raised')
  224. def on_debug(self, target, option, names, values):
  225. """
  226. debug : GRAPH exp
  227. """
  228. if option == 0:
  229. print generate_graph(values[1])
  230. return values[1]
  231. raise BisonSyntaxError('Unsupported option %d in target "%s".'
  232. % (option, target)) # pragma: nocover
  233. def on_exp(self, target, option, names, values):
  234. """
  235. exp : NUMBER
  236. | IDENTIFIER
  237. | function
  238. | LPAREN exp RPAREN
  239. | unary
  240. | binary
  241. | nary
  242. """
  243. # | concat
  244. if option == 0: # rule: NUMBER
  245. # TODO: A bit hacky, this achieves long integers and floats.
  246. value = float(values[0]) if '.' in values[0] else int(values[0])
  247. return Leaf(value)
  248. if option == 1: # rule: IDENTIFIER
  249. return Leaf(values[0])
  250. if option == 2: # rule: function
  251. return values[0]
  252. if option == 3: # rule: LPAREN exp RPAREN
  253. return values[1]
  254. if option in [4, 5, 6]: # rule: unary | binary | nary
  255. return values[0]
  256. raise BisonSyntaxError('Unsupported option %d in target "%s".'
  257. % (option, target)) # pragma: nocover
  258. def on_unary(self, target, option, names, values):
  259. """
  260. unary : MINUS exp %prec NEG
  261. """
  262. if option == 0: # rule: NEG exp
  263. # Add negation to the left-most child
  264. if values[1].is_leaf or values[1].op != OP_MUL:
  265. values[1].negated += 1
  266. else:
  267. child = Scope(values[1])[0]
  268. child.negated += 1
  269. return values[1]
  270. raise BisonSyntaxError('Unsupported option %d in target "%s".'
  271. % (option, target)) # pragma: nocover
  272. def on_binary(self, target, option, names, values):
  273. """
  274. binary : exp PLUS exp
  275. | exp TIMES exp
  276. | exp DIVIDE exp
  277. | exp POW exp
  278. | exp MINUS exp
  279. """
  280. if 0 <= option < 4: # rule: exp {PLUS,TIMES,DIVIDES,POW} exp
  281. return Node(values[1], values[0], values[2])
  282. if option == 4: # rule: exp MINUS exp
  283. node = values[2]
  284. # Add negation to the left-most child
  285. if node.is_leaf or node.op != OP_MUL:
  286. node.negated += 1
  287. else:
  288. node = Scope(node)[0]
  289. node.negated += 1
  290. # Explicit call the hook handler on the created unary negation.
  291. node = self.hook_handler('binary', 4, names, values, node)
  292. return Node('+', values[0], values[2])
  293. raise BisonSyntaxError('Unsupported option %d in target "%s".'
  294. % (option, target)) # pragma: nocover
  295. def on_nary(self, target, option, names, values):
  296. """
  297. nary : exp COMMA exp
  298. """
  299. if option == 0: # rule: exp COMMA exp
  300. return Node(*combine(',', OP_COMMA, values[0], values[2]))
  301. raise BisonSyntaxError('Unsupported option %d in target "%s".'
  302. % (option, target)) # pragma: nocover
  303. def on_function(self, target, option, names, values):
  304. """
  305. function : function_name LPAREN arglist RPAREN
  306. """
  307. if option == 0: # rule: function_name LPAREN arglist RPAREN
  308. print 'FUNCTION:', values[0], values[2]
  309. return Node(values[0], *values[2])
  310. raise BisonSyntaxError('Unsupported option %d in target "%s".'
  311. % (option, target)) # pragma: nocover
  312. def on_function_name(self, target, option, names, values):
  313. """
  314. function_name : SQRT | SIN | COS | TAN | INT | SOLVE
  315. """
  316. if 0 <= option <= 5: # rule: SQRT | SIN | COS | TAN | INT | SOLVE
  317. return values[0]
  318. #return TOKEN_OP_MAP[values[0]]
  319. raise BisonSyntaxError('Unsupported option %d in target "%s".'
  320. % (option, target)) # pragma: nocover
  321. def on_arglist(self, target, option, names, values):
  322. """
  323. arglist : arglist COMMA exp
  324. | exp
  325. """
  326. if option == 0: # rule: arglist COMMA exp
  327. return values[0] + [values[2]]
  328. if option == 1: # rule: exp
  329. return [values[0]]
  330. raise BisonSyntaxError('Unsupported option %d in target "%s".'
  331. % (option, target)) # pragma: nocover
  332. # -----------------------------------------
  333. # operator tokens
  334. # -----------------------------------------
  335. operators = ''
  336. for op_str, op in OP_MAP.iteritems():
  337. operators += '"%s"%s{ returntoken(%s); }\n' \
  338. % (op_str, ' ' * (8 - len(op_str)), TOKEN_MAP[op])
  339. # -----------------------------------------
  340. # raw lex script, verbatim here
  341. # -----------------------------------------
  342. lexscript = r"""
  343. %top{
  344. #include "Python.h"
  345. }
  346. %{
  347. #define YYSTYPE void *
  348. #include "tokens.h"
  349. extern void *py_parser;
  350. extern void (*py_input)(PyObject *parser, char *buf, int *result,
  351. int max_size);
  352. #define returntoken(tok) \
  353. yylval = PyString_FromString(strdup(yytext)); return (tok);
  354. #define YY_INPUT(buf,result,max_size) { \
  355. (*py_input)(py_parser, buf, &result, max_size); \
  356. }
  357. int yycolumn = 0;
  358. #define YY_USER_ACTION \
  359. yylloc.first_line = yylloc.last_line = yylineno; \
  360. yylloc.first_column = yycolumn; \
  361. yylloc.last_column = yycolumn + yyleng; \
  362. yycolumn += yyleng;
  363. %}
  364. %option yylineno
  365. %%
  366. [0-9]+"."?[0-9]* { returntoken(NUMBER); }
  367. [a-zA-Z] { returntoken(IDENTIFIER); }
  368. "(" { returntoken(LPAREN); }
  369. ")" { returntoken(RPAREN); }
  370. """ + operators + r"""
  371. "raise" { returntoken(RAISE); }
  372. "graph" { returntoken(GRAPH); }
  373. "quit" { yyterminate(); returntoken(QUIT); }
  374. [ \t\v\f] { }
  375. [\n] { yycolumn = 0; returntoken(NEWLINE); }
  376. . { printf("unknown char %c ignored.\n", yytext[0]); }
  377. %%
  378. yywrap() { return(1); }
  379. """