3 # convert yacc's verbose output to a dot-language diagram
5 from sys import stdin, stdout, stderr
11 nodesep = 0.2; /* inches */
13 node [shape = record];
30 class action (object) :
32 Parse the text defining a given rule:
33 >>> a = action(' TEXT shift, and go to state 1')
38 >>> a = action(' $default accept')
41 >>> a = action(' $default reduce using rule 1 (sum)')
44 >>> print a.reduce_rule
46 >>> a = action(' PLUS shift, and go to state 3')
49 >>> a = action(' sum go to state 2')
55 def __init__(self, string) :
57 split = string.split()
59 if split[1][-1] == ',' :
60 self.action = split[1][0:-1]
61 elif split[1][0] == '[':
62 self.action = 'IGNORED'
64 self.action = split[1]
66 if self.action == 'reduce' :
67 self.reduce_rule = int(split[-2])
68 elif self.action == 'shift' :
69 self.goto = int(split[-1])
70 elif self.action == 'go' :
71 self.goto = int(split[-1])
73 if self.action == 'go' or self.action == 'shift':
74 details = ', goto %d' % self.goto
75 elif self.action == 'reduce' :
76 details = ', rule %d' % self.reduce_rule
77 elif self.action == 'accept' :
79 elif self.action == 'IGNORED' :
82 raise Exception, 'wierd action %s' % self.action
84 return '<action: %s, %s%s>' % (self.key, self.action, details)
88 class state (object) :
90 Parse the text defining a given state:
91 >>> state_text = '''state 0
93 ... 0 $accept: . authors $end
95 ... TEXT shift, and go to state 1
96 ... LBRACE shift, and go to state 2
98 ... authors go to state 3
99 ... author go to state 4
100 ... text go to state 5
103 >>> s = state(state_text)
107 [(0, '$accept: . authors $end')]
109 [<action: TEXT, shift, goto 1>, <action: LBRACE, shift, goto 2>]
111 [<action: authors, go, goto 3>, <action: author, go, goto 4>, <action: text, go, goto 5>]
112 >>> print s.goes_to()
115 def __init__(self, state_text, debug=False) :
116 # because the output of `yacc -v grammar.y' is so regular,
117 # we'll sneak by with a HACK parsing job ;).
119 self.rules = [] # list of "rule string"s ###(<rule-id>, <target>, [], pos)
123 hadtext = False # so consecutive blank lines don't keep switching lookfor state
124 for line in state_text.splitlines() :
125 if debug : print >> stderr, '%s : %s' % (lookfor, line)
127 if len(split) < 1 : # blank line
128 if hadtext == False :
130 elif lookfor == 'rules' :
133 elif lookfor == 'tok-act' :
136 elif split[0] == 'state' : # 'state <state-id>'
137 assert lookfor == 'state', "Multiple states in '%s'" % state_text
138 assert len(split) == 2, "%d items in state line '%s'" % line
139 self.id = int(split[1]) # store <state-id> as self.id
143 elif lookfor == 'rules' : # e.g ' 2 expr: expr . PLUS expr'
144 rule_id = int(split[0])
145 rule_text = split[1:]
146 if rule_text[0] == '|' : # e.g. second line of
150 prev_rule = self.rules[len(self.rules)-1]
151 p_text_split = prev_rule[1].split()
152 rule_text.insert(0, p_text_split[0])
153 self.rules.append((rule_id, " ".join(rule_text)))
154 elif lookfor == 'tok-act' :
155 self.tok_act.append(action(line))
156 elif lookfor == 'rule-act' :
157 self.rule_act.append(action(line))
159 raise Exception, "Unrecognized lookfor '%s' for line '%s'" % (lookfor, line)
161 def __cmp__(self, other) :
162 return cmp(self.id, other.id)
164 return '<state %d>' % self.id
166 return self.__str__()
168 "Return a list of integer states that this node goes to (not reduces to)."
170 for a in self.tok_act :
171 if hasattr(a, 'goto') :
173 for a in self.rule_act :
174 if hasattr(a, 'goto') :
178 class tnode (object) :
180 Tree structure, with backlinks.
181 >>> root = tnode("root")
182 >>> root.add_child("child0")
183 >>> root.add_child("child1")
184 >>> root.child[0].add_child("child00")
185 >>> root.child[0].add_child("child01")
186 >>> print root.child[0].child
187 [<tnode child00>, <tnode child01>]
188 >>> print root.child[0].parent
190 >>> print root.find("child01")
193 def __init__(self, value=None, parent=None) :
197 def add_child(self, child_value) :
198 self.child.append(tnode(child_value, self))
199 def build(self, fn_get_children) :
200 "use gn_get_children(value) = [values' children] to recursively build the tree"
201 children = fn_get_children(self.value)
202 for child in children :
203 self.add_child(child)
204 for cnode in self.child :
205 cnode.build(fn_get_children)
206 def find(self, value) :
208 if self.value == value :
211 for cnode in self.child :
212 match = cnode.find(value)
217 string = '<tnode %s>' % str(self.value)
220 return self.__str__()
222 class yaccout (object) :
224 Parse the output of `yacc -v file.y'
225 For example, for sum.y =
234 ... 0 $accept: sum $end
239 ... Terminals, with rules where they appear
247 ... Nonterminals, with rules where they appear
252 ... on left: 1, on right: 0
257 ... 0 $accept: . sum $end
259 ... X shift, and go to state 1
261 ... sum go to state 2
266 ... 1 sum: X . PLUS X
268 ... PLUS shift, and go to state 3
273 ... 0 $accept: sum . $end
275 ... $end shift, and go to state 4
280 ... 1 sum: X PLUS . X
282 ... X shift, and go to state 5
287 ... 0 $accept: sum $end .
294 ... 1 sum: X PLUS X .
296 ... $default reduce using rule 1 (sum)
298 >>> y = yaccout(yacc_text)
300 [<state 0>, <state 1>, <state 2>, <state 3>, <state 4>, <state 5>]
301 >>> print y.states[0].tok_act
302 [<action: X, shift, goto 1>]
303 >>> print y.root.child[0].child
305 >>> print y.root.child[0].parent
308 def __init__(self, yacc_text, debug=False):
309 # because the output of `yacc -v grammar.y' is so regular,
310 # we'll sneak by with a HACK parsing job ;).
311 self.read_states(yacc_text, debug=debug)
313 #self.reduce_link(debug=debug)
314 def read_states(self, yacc_text, debug=False):
315 "read in all the states"
317 lookfor = 'first state'
318 for line in yacc_text.splitlines() :
319 split = line.split(' ')
320 if lookfor == 'first state' :
321 if split[0] != 'state' :
328 if split[0] == 'state' : # we've hit the next state
329 self.states.append(state('\n'.join(state_text), debug=debug))
333 state_text.append(line)
334 # end of file, so process the last state
335 self.states.append(state('\n'.join(state_text), debug=debug))
336 def state_goes_to(self, state) :
338 for id in state.goes_to() :
339 for s in self.states :
344 "generate the state dependency tree"
345 self.root = tnode(self.states[0])
346 self.root.build(self.state_goes_to)
347 def reduce_action(self, state, action, debug):
348 if action.action == 'reduce' :
349 if debug : print >> stderr, 'reduce from %s with rule %d::' % (state, action.reduce_rule),
352 for r in state.rules :
353 if r[0] == action.reduce_rule :
354 rule = r[1] # e.g. 'sum: X PLUS X .'
355 if debug : print >> stderr, rule
356 # find the number of args in the rule
358 args = len(split) - 2 # 2 for 'sum:' and '.'
359 # find the state in the tnode tree
360 tnode = self.root.find(state)
361 for i in range(args) : # take arg steps up the tree
363 tstate = tnode.value # reduction target state
364 action.reduce_to = tstate.id # state we reduce to
365 action.reduce_targ = split[0][0:-1] # rule we reduce to 'sum'
367 for a in tstate.tok_act :
368 if a.key == action.reduce_targ :
369 action.reduce_targ_i = i
371 for a in tstate.rule_act :
372 if a.key == action.reduce_targ :
373 action.reduce_targ_i = i
375 if debug : print >> stderr, 'to state %d' % action.reduce_to
376 def reduce_link(self, debug=False):
377 "generate the reduce_to backlinks"
378 for state in self.states :
379 for a in state.tok_act :
380 self.reduce_action(state, a, debug)
381 for a in state.rule_act :
382 self.reduce_action(state, a, debug)
385 def dot_state (state) :
387 Print a dot node for a given state.
388 Node type must be 'record'.
394 >>> state_text = '''state 0
396 ... 0 $accept: . authors $end
398 ... TEXT shift, and go to state 1
399 ... LBRACE shift, and go to state 2
401 ... authors go to state 3
402 ... author go to state 4
403 ... text go to state 5
406 >>> s = state(state_text)
407 >>> print dot_state(s),
408 state0 [label = "{ <s> state 0 | { <a0> TEXT | <a1> LBRACE | <a2> authors | <a3> author | <a4> text } }"];
410 label = '{ <s> state %d | ' % state.id
411 for rule in state.rules :
412 label += '(%d) %s\\n' % (rule[0], rule[1])
415 for action in state.tok_act[:-1] :
416 label += ' <a%d> %s |' % (a, action.key,)
418 if len(state.tok_act) > 0 :
419 label += ' <a%d> %s' % (a, state.tok_act[-1].key)
421 for action in state.rule_act :
422 label += ' | <a%d> %s' % (a, action.key)
426 string = ' state%d [label = "%s"];\n' % (state.id, label)
429 def dot_goto(id, a, action) :
431 Print dot links for a given action (action a for state id).
432 >>> a = action(' TEXT shift, and go to state 1')
433 >>> print dot_goto(5,8,a),
434 state5:a8 -> state1:s;
435 >>> a = action(' $default reduce using rule 1 (sum)')
436 >>> print dot_goto(0,1,a),
438 >>> a = action(' sum go to state 2')
439 >>> print dot_goto(0,1,a),
440 state0:a1 -> state2:s;
442 if hasattr(action, 'goto') :
443 string = ' state%d:a%d -> state%d:s;\n' % (id, a, action.goto)
449 def dot_gotos(state) :
451 Print dot links for a given state.
452 >>> state_text = '''state 0
454 ... 0 $accept: . authors $end
456 ... TEXT shift, and go to state 1
457 ... LBRACE reduce using rule 1 (braces)
459 ... authors go to state 3
460 ... author go to state 4
461 ... text go to state 5
464 >>> s = state(state_text)
465 >>> print dot_gotos(s),
466 state0:a0 -> state1:s;
467 state0:a2 -> state3:s;
468 state0:a3 -> state4:s;
469 state0:a4 -> state5:s;
473 for action in state.tok_act :
474 string += dot_goto(state.id, a, action)
476 for action in state.rule_act :
477 string += dot_goto(state.id, a, action)
481 def dot_reduce(id, a, action) :
483 Print dot reduce links for a reduction action (action a for state id).
485 if action.action == 'reduce' :
486 string = ' state%d:a%d -> state%d:a%d;\n' % (id, a, action.reduce_to, action.reduce_targ_i)
492 def dot_reduces(state) :
494 Print dot reduce links for a given state.
498 for action in state.tok_act :
499 string += dot_reduce(state.id, a, action)
501 for action in state.rule_act :
502 string += dot_reduce(state.id, a, action)
506 def yacc2dot(yaccout) :
509 for state in yaccout.states :
510 string += dot_state(state)
512 string += DOT_GOTO_EDGE
513 for state in yaccout.states :
514 string += dot_gotos(state)
516 string += DOT_REDUCE_EDGE
517 #for state in yaccout.states :
518 # string += dot_reduces(state)
523 def open_IOfiles(ifilename=None, ofilename=None, debug=False):
525 if debug : print >> stderr, "open input file '%s'" % ifilename
526 ifile = file(ifilename, 'r')
530 if debug : print >> stderr, "open output file '%s'" % ofilename
531 ofile = file(ofilename, 'w')
534 return (ifile, ofile)
536 def close_IOfiles(ifilename=None, ifile=stdin,
537 ofilename=None, ofile=stdout,
540 if debug : print >> stderr, "close input file '%s'" % ifilename
543 if debug : print >> stderr, "close output file '%s'" % ofilename
550 if __name__ == "__main__":
551 from optparse import OptionParser
553 parser = OptionParser(usage="usage: %prog [options]", version="%prog 0.1")
555 parser.add_option('-f', '--input-file', dest="ifilename",
556 help="Read input from FILE (default stdin)",
557 type='string', metavar="FILE")
558 parser.add_option('-o', '--output-file', dest="ofilename",
559 help="Write output to FILE (default stdout)",
560 type='string', metavar="FILE")
561 parser.add_option('-t', '--test', dest="test",
562 help="Run the yacc2dot test suite",
563 action="store_true", default=False)
564 parser.add_option('-v', '--verbose', dest="verbose",
565 help="Print lots of debugging information",
566 action="store_true", default=False)
568 (options, args) = parser.parse_args()
571 ifile,ofile = open_IOfiles(options.ifilename, options.ofilename,
578 y = yaccout(text, options.verbose)
582 close_IOfiles(options.ifilename, ifile,
583 options.ofilename, ofile, options.verbose)