parser.py 17 KB

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