35c40eee3801b10a844c5e72aad45808e45a25ee
[mw2txt.git] / posts / yacc2dot / yacc2dot
1 #!/usr/bin/python
2 #
3 # convert yacc's verbose output to a dot-language diagram
4
5 from sys import stdin, stdout, stderr
6
7 DOT_HEAD = """
8 digraph yacc {
9
10   epsilon = 0.01;
11   nodesep = 0.2;  /* inches */
12
13   node [shape = record];
14
15 """
16
17 DOT_GOTO_EDGE = """
18   edge [color = blue];
19 """
20
21 DOT_REDUCE_EDGE = """
22   edge [color = red];
23 """
24
25 DOT_TAIL = "}\n"
26
27
28
29
30 class action (object) :
31     """
32     Parse the text defining a given rule:
33     >>> a = action('    TEXT    shift, and go to state 1')
34     >>> print a.key
35     TEXT
36     >>> print a.action
37     shift
38     >>> a = action('    $default  accept')
39     >>> print a.action
40     accept
41     >>> a = action('    $default  reduce using rule 1 (sum)')
42     >>> print a.action
43     reduce
44     >>> print a.reduce_rule
45     1
46     >>> a = action('    PLUS  shift, and go to state 3')
47     >>> print a.goto
48     3
49     >>> a = action('    sum  go to state 2')
50     >>> print a.action
51     go
52     >>> print a.goto
53     2
54     """
55     def __init__(self, string) :
56         self.rule = string
57         split = string.split()
58         self.key = split[0]
59         if split[1][-1] == ',' :
60             self.action = split[1][0:-1]
61         elif split[1][0] == '[':
62             self.action = 'IGNORED'
63         else :
64             self.action = split[1]
65
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])
72     def __str__(self) :
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' :
78             details = ''
79         elif self.action == 'IGNORED' :
80             details = ''
81         else :
82             raise Exception, 'wierd action %s' % self.action
83
84         return '<action: %s, %s%s>' % (self.key, self.action, details)
85     def __repr__(self) :
86         return self.__str__()
87
88 class state (object) :
89     """
90     Parse the text defining a given state:
91     >>> state_text = '''state 0
92     ... 
93     ...     0 $accept: . authors $end
94     ... 
95     ...     TEXT    shift, and go to state 1
96     ...     LBRACE  shift, and go to state 2
97     ... 
98     ...     authors  go to state 3
99     ...     author   go to state 4
100     ...     text     go to state 5
101     ... 
102     ... '''
103     >>> s = state(state_text)
104     >>> print s.id
105     0
106     >>> print s.rules
107     [(0, '$accept: . authors $end')]
108     >>> print s.tok_act
109     [<action: TEXT, shift, goto 1>, <action: LBRACE, shift, goto 2>]
110     >>> print s.rule_act
111     [<action: authors, go, goto 3>, <action: author, go, goto 4>, <action: text, go, goto 5>]
112     >>> print s.goes_to()
113     [1, 2, 3, 4, 5]
114     """
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 ;).
118         self.id = None
119         self.rules = []     # list of "rule string"s ###(<rule-id>, <target>, [], pos)
120         self.tok_act = []
121         self.rule_act = []
122         lookfor = 'state'
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)
126             split = line.split()
127             if len(split) < 1 :      # blank line
128                 if hadtext == False :
129                     continue # ignore
130                 elif lookfor == 'rules' :
131                     lookfor = 'tok-act'
132                     continue
133                 elif lookfor == 'tok-act' :
134                     lookfor = 'rule-act'
135                     continue
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
140                 lookfor = 'rules'
141                 hadtext = False
142                 continue
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 
147                                      #  expr: X
148                                      #      | expr PLUS expr
149                     rule_text.pop(0)
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))
158             else :
159                 raise Exception, "Unrecognized lookfor '%s' for line '%s'" % (lookfor, line)
160             hadtext = True
161     def __cmp__(self, other) :
162         return cmp(self.id, other.id)
163     def __str__(self) :
164         return '<state %d>' % self.id
165     def __repr__(self) :
166         return self.__str__()
167     def goes_to(self) :
168         "Return a list of integer states that this node goes to (not reduces to)."
169         ret = []
170         for a in self.tok_act :
171             if hasattr(a, 'goto') :
172                 ret.append(a.goto)
173         for a in self.rule_act :
174             if hasattr(a, 'goto') :
175                 ret.append(a.goto)
176         return ret
177
178 class tnode (object) :
179     """
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
189     <tnode root>
190     >>> print root.find("child01")
191     <tnode child01>
192     """
193     def __init__(self, value=None, parent=None) :
194         self.value = value
195         self.parent = parent
196         self.child = []
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) :
207         "depth first search"
208         if self.value == value :
209             return self
210         else :
211             for cnode in self.child :
212                 match = cnode.find(value)
213                 if match != None :
214                     return match
215         return None
216     def __str__(self) :
217         string = '<tnode %s>' % str(self.value)
218         return string
219     def __repr__(self) :
220         return self.__str__()
221
222 class yaccout (object) :
223     """
224     Parse the output of `yacc -v file.y'
225     For example, for sum.y =
226      %token X
227      %token PLUS     
228      %%
229      sum : X PLUS X
230     We get:
231     >>> yacc_text = ''' 
232     ... Grammar
233     ... 
234     ...     0 $accept: sum $end
235     ... 
236     ...     1 sum: X PLUS X
237     ... 
238     ... 
239     ... Terminals, with rules where they appear
240     ... 
241     ... $end (0) 0
242     ... error (256)
243     ... X (258) 1
244     ... PLUS (259) 1
245     ... 
246     ... 
247     ... Nonterminals, with rules where they appear
248     ... 
249     ... $accept (5)
250     ...     on left: 0
251     ... sum (6)
252     ...     on left: 1, on right: 0
253     ... 
254     ... 
255     ... state 0
256     ... 
257     ...     0 $accept: . sum $end
258     ... 
259     ...     X  shift, and go to state 1
260     ...  
261     ...     sum  go to state 2
262     ... 
263     ... 
264     ... state 1
265     ... 
266     ...     1 sum: X . PLUS X
267     ... 
268     ...     PLUS  shift, and go to state 3
269     ... 
270     ... 
271     ... state 2
272     ... 
273     ...     0 $accept: sum . $end
274     ... 
275     ...     $end  shift, and go to state 4
276     ... 
277     ... 
278     ... state 3
279     ... 
280     ...     1 sum: X PLUS . X
281     ... 
282     ...     X  shift, and go to state 5
283     ... 
284     ... 
285     ... state 4
286     ... 
287     ...     0 $accept: sum $end .
288     ... 
289     ...     $default  accept
290     ... 
291     ... 
292     ... state 5
293     ... 
294     ...     1 sum: X PLUS X .
295     ... 
296     ...     $default  reduce using rule 1 (sum)
297     ... '''
298     >>> y = yaccout(yacc_text)
299     >>> print y.states
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
304     [<tnode <state 3>>]
305     >>> print y.root.child[0].parent
306     <tnode <state 0>>
307     """
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)
312         #self.gen_tree()
313         #self.reduce_link(debug=debug)
314     def read_states(self, yacc_text, debug=False):
315         "read in all the states"
316         self.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' :
322                     continue
323                 else :
324                     lookfor = 'states'
325                     state_text = [line]
326                     continue
327             else :
328                 if split[0] == 'state' : # we've hit the next state
329                     self.states.append(state('\n'.join(state_text), debug=debug))
330                     state_text = [line]
331                     continue
332                 else :
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) :
337         ret = []
338         for id in state.goes_to() :
339             for s in self.states :
340                 if s.id == id :
341                     ret.append(s)
342         return ret
343     def gen_tree(self):
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),
350             # find the rule text
351             rule = None
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
357             split = rule.split()
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
362                 tnode = tnode.parent
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'
366             i = 0
367             for a in tstate.tok_act :
368                 if a.key == action.reduce_targ :
369                     action.reduce_targ_i = i
370                 i += 1
371             for a in tstate.rule_act :
372                 if a.key == action.reduce_targ :
373                     action.reduce_targ_i = i
374                 i += 1                
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)
383
384
385 def dot_state (state) :
386     """
387     Print a dot node for a given state.
388     Node type must be 'record'.
389     layout:
390        state %d
391        rules
392        ...
393       a0 | a1 | ...
394     >>> state_text = '''state 0
395     ... 
396     ...     0 $accept: . authors $end
397     ... 
398     ...     TEXT    shift, and go to state 1
399     ...     LBRACE  shift, and go to state 2
400     ... 
401     ...     authors  go to state 3
402     ...     author   go to state 4
403     ...     text     go to state 5
404     ... 
405     ... '''
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 } }"];
409     """
410     label = '{ <s> state %d | ' % state.id
411     for rule in state.rules :
412         label += '(%d) %s\\n' % (rule[0], rule[1])
413     label += ' | {'
414     a = 0
415     for action in state.tok_act[:-1] :
416         label += ' <a%d> %s |' % (a, action.key,)
417         a += 1
418     if len(state.tok_act) > 0 :
419         label += ' <a%d> %s' % (a, state.tok_act[-1].key)
420         a += 1
421     for action in state.rule_act :
422         label += ' | <a%d> %s' % (a, action.key)
423         a += 1
424     label += ' } }'
425
426     string = '  state%d [label = "%s"];\n' % (state.id, label)
427     return string
428
429 def dot_goto(id, a, action) :
430     """
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),
437
438     >>> a = action('    sum  go to state 2')
439     >>> print dot_goto(0,1,a),
440       state0:a1 -> state2:s;
441     """
442     if hasattr(action, 'goto') :
443         string = '  state%d:a%d -> state%d:s;\n' % (id, a, action.goto)
444     else :
445         string = ''
446     return string
447     
448
449 def dot_gotos(state) :
450     """
451     Print dot links for a given state.
452     >>> state_text = '''state 0
453     ... 
454     ...     0 $accept: . authors $end
455     ... 
456     ...     TEXT    shift, and go to state 1
457     ...     LBRACE  reduce using rule 1 (braces)
458     ... 
459     ...     authors  go to state 3
460     ...     author   go to state 4
461     ...     text     go to state 5
462     ... 
463     ... '''
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;
470     """
471     string = ""
472     a = 0
473     for action in state.tok_act :
474         string += dot_goto(state.id, a, action)
475         a += 1
476     for action in state.rule_act :
477         string += dot_goto(state.id, a, action)
478         a += 1
479     return string
480
481 def dot_reduce(id, a, action) :
482     """
483     Print dot reduce links for a reduction action (action a for state id).
484     """
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)
487     else :
488         string = ''
489     return string
490     
491
492 def dot_reduces(state) :
493     """
494     Print dot reduce links for a given state.
495     """
496     string = ""
497     a = 0
498     for action in state.tok_act :
499         string += dot_reduce(state.id, a, action)
500         a += 1
501     for action in state.rule_act :
502         string += dot_reduce(state.id, a, action)
503         a += 1
504     return string
505     
506 def yacc2dot(yaccout) :
507     string = DOT_HEAD
508     string += "\n"
509     for state in yaccout.states :
510         string += dot_state(state)
511     string += "\n"
512     string += DOT_GOTO_EDGE
513     for state in yaccout.states :
514         string += dot_gotos(state)
515     string += "\n"
516     string += DOT_REDUCE_EDGE
517     #for state in yaccout.states :
518     #    string += dot_reduces(state)
519     string += "\n"
520     string += DOT_TAIL
521     return string
522
523 def open_IOfiles(ifilename=None, ofilename=None, debug=False):
524     if ifilename :
525         if debug :  print >> stderr, "open input file '%s'" % ifilename
526         ifile = file(ifilename, 'r')
527     else :
528         ifile = stdin
529     if ofilename :
530         if debug :  print >> stderr, "open output file '%s'" % ofilename
531         ofile = file(ofilename, 'w')
532     else :
533         ofile = stdout    
534     return (ifile, ofile)
535
536 def close_IOfiles(ifilename=None, ifile=stdin, 
537                   ofilename=None, ofile=stdout,
538                   debug=False):
539     if ifilename :
540         if debug :  print >> stderr, "close input file '%s'" % ifilename
541         ifile.close()
542     if ofilename :
543         if debug :  print >> stderr, "close output file '%s'" % ofilename
544         ofile.close()
545
546 def _test():
547     import doctest
548     doctest.testmod()
549
550 if __name__ == "__main__":
551     from optparse import OptionParser
552
553     parser = OptionParser(usage="usage: %prog [options]", version="%prog 0.1")
554
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)
567
568     (options, args) = parser.parse_args()
569     parser.destroy()
570
571     ifile,ofile = open_IOfiles(options.ifilename, options.ofilename,
572                                options.verbose)
573
574     if options.test :
575         _test()
576     else :
577         text = ifile.read()
578         y = yaccout(text, options.verbose)
579         dot = yacc2dot(y)
580         print >> ofile, dot
581
582     close_IOfiles(options.ifilename, ifile,
583                   options.ofilename, ofile, options.verbose)
584