parser.py 15 KB

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