1
2
3
4
5
6
7
8
9
10 """
11 A graphical tool for exploring the shift/reduce parser.
12
13 The shift/reduce parser maintains a stack, which records the structure
14 of the portion of the text that has been parsed. The stack is
15 initially empty. Its contents are shown on the left side of the main
16 canvas.
17
18 On the right side of the main canvas is the remaining text. This is
19 the portion of the text which has not yet been considered by the
20 parser.
21
22 The parser builds up a tree structure for the text using two
23 operations:
24
25 - "shift" moves the first token from the remaining text to the top
26 of the stack. In the demo, the top of the stack is its right-hand
27 side.
28 - "reduce" uses a grammar production to combine the rightmost stack
29 elements into a single tree token.
30
31 You can control the parser's operation by using the "shift" and
32 "reduce" buttons; or you can use the "step" button to let the parser
33 automatically decide which operation to apply. The parser uses the
34 following rules to decide which operation to apply:
35
36 - Only shift if no reductions are available.
37 - If multiple reductions are available, then apply the reduction
38 whose CFG production is listed earliest in the grammar.
39
40 The "reduce" button applies the reduction whose CFG production is
41 listed earliest in the grammar. There are two ways to manually choose
42 which reduction to apply:
43
44 - Click on a CFG production from the list of available reductions,
45 on the left side of the main window. The reduction based on that
46 production will be applied to the top of the stack.
47 - Click on one of the stack elements. A popup window will appear,
48 containing all available reductions. Select one, and it will be
49 applied to the top of the stack.
50
51 Note that reductions can only be applied to the top of the stack.
52
53 Keyboard Shortcuts::
54 [Space]\t Perform the next shift or reduce operation
55 [s]\t Perform a shift operation
56 [r]\t Perform a reduction operation
57 [Ctrl-z]\t Undo most recent operation
58 [Delete]\t Reset the parser
59 [g]\t Show/hide available production list
60 [Ctrl-a]\t Toggle animations
61 [h]\t Help
62 [Ctrl-p]\t Print
63 [q]\t Quit
64 """
65
66 """
67 Possible future improvements:
68 - button/window to change and/or select text. Just pop up a window
69 with an entry, and let them modify the text; and then retokenize
70 it? Maybe give a warning if it contains tokens whose types are
71 not in the grammar.
72 - button/window to change and/or select grammar. Select from
73 several alternative grammars? Or actually change the grammar? If
74 the later, then I'd want to define nltk.draw.cfg, which would be
75 responsible for that.
76 """
77
78 import string
79 from Tkinter import *
80 import tkFont
81
82 from nltk import parse, tokenize
83
84 from nltk.draw.tree import *
85 from nltk.draw import *
86 from nltk.draw.cfg import CFGEditor
87
88
90 """
91 A graphical tool for exploring the shift/reduce parser. The tool
92 displays the parser's stack and the remaining text, and allows the
93 user to control the parser's operation. In particular, the user
94 can shift tokens onto the stack, and can perform reductions on the
95 top elements of the stack. A "step" button simply steps through
96 the parsing process, performing the operations that
97 C{parse.ShiftReduceParser} would use.
98 """
99 - def __init__(self, grammar, sent, trace=0):
136
137
138
139
140
142
143 self._sysfont = tkFont.Font(font=Button()["font"])
144 root.option_add("*Font", self._sysfont)
145
146
147 self._size = IntVar(root)
148 self._size.set(self._sysfont.cget('size'))
149
150 self._boldfont = tkFont.Font(family='helvetica', weight='bold',
151 size=self._size.get())
152 self._font = tkFont.Font(family='helvetica',
153 size=self._size.get())
154
156
157 self._prodframe = listframe = Frame(parent)
158 self._prodframe.pack(fill='both', side='left', padx=2)
159 self._prodlist_label = Label(self._prodframe,
160 font=self._boldfont,
161 text='Available Reductions')
162 self._prodlist_label.pack()
163 self._prodlist = Listbox(self._prodframe, selectmode='single',
164 relief='groove', background='white',
165 foreground='#909090',
166 font=self._font,
167 selectforeground='#004040',
168 selectbackground='#c0f0c0')
169
170 self._prodlist.pack(side='right', fill='both', expand=1)
171
172 self._productions = list(self._parser.grammar().productions())
173 for production in self._productions:
174 self._prodlist.insert('end', (' %s' % production))
175 self._prodlist.config(height=min(len(self._productions), 25))
176
177
178 if 1:
179 listscroll = Scrollbar(self._prodframe,
180 orient='vertical')
181 self._prodlist.config(yscrollcommand = listscroll.set)
182 listscroll.config(command=self._prodlist.yview)
183 listscroll.pack(side='left', fill='y')
184
185
186 self._prodlist.bind('<<ListboxSelect>>', self._prodlist_select)
187
188
189 self._hover = -1
190 self._prodlist.bind('<Motion>', self._highlight_hover)
191 self._prodlist.bind('<Leave>', self._clear_hover)
192
194
195 self._top.bind('<Control-q>', self.destroy)
196 self._top.bind('<Control-x>', self.destroy)
197 self._top.bind('<Alt-q>', self.destroy)
198 self._top.bind('<Alt-x>', self.destroy)
199
200
201 self._top.bind('<space>', self.step)
202 self._top.bind('<s>', self.shift)
203 self._top.bind('<Alt-s>', self.shift)
204 self._top.bind('<Control-s>', self.shift)
205 self._top.bind('<r>', self.reduce)
206 self._top.bind('<Alt-r>', self.reduce)
207 self._top.bind('<Control-r>', self.reduce)
208 self._top.bind('<Delete>', self.reset)
209 self._top.bind('<u>', self.undo)
210 self._top.bind('<Alt-u>', self.undo)
211 self._top.bind('<Control-u>', self.undo)
212 self._top.bind('<Control-z>', self.undo)
213 self._top.bind('<BackSpace>', self.undo)
214
215
216 self._top.bind('<Control-p>', self.postscript)
217 self._top.bind('<Control-h>', self.help)
218 self._top.bind('<F1>', self.help)
219 self._top.bind('<Control-g>', self.edit_grammar)
220 self._top.bind('<Control-t>', self.edit_sentence)
221
222
223 self._top.bind('-', lambda e,a=self._animate:a.set(20))
224 self._top.bind('=', lambda e,a=self._animate:a.set(10))
225 self._top.bind('+', lambda e,a=self._animate:a.set(4))
226
243
245 menubar = Menu(parent)
246
247 filemenu = Menu(menubar, tearoff=0)
248 filemenu.add_command(label='Reset Parser', underline=0,
249 command=self.reset, accelerator='Del')
250 filemenu.add_command(label='Print to Postscript', underline=0,
251 command=self.postscript, accelerator='Ctrl-p')
252 filemenu.add_command(label='Exit', underline=1,
253 command=self.destroy, accelerator='Ctrl-x')
254 menubar.add_cascade(label='File', underline=0, menu=filemenu)
255
256 editmenu = Menu(menubar, tearoff=0)
257 editmenu.add_command(label='Edit Grammar', underline=5,
258 command=self.edit_grammar,
259 accelerator='Ctrl-g')
260 editmenu.add_command(label='Edit Text', underline=5,
261 command=self.edit_sentence,
262 accelerator='Ctrl-t')
263 menubar.add_cascade(label='Edit', underline=0, menu=editmenu)
264
265 rulemenu = Menu(menubar, tearoff=0)
266 rulemenu.add_command(label='Step', underline=1,
267 command=self.step, accelerator='Space')
268 rulemenu.add_separator()
269 rulemenu.add_command(label='Shift', underline=0,
270 command=self.shift, accelerator='Ctrl-s')
271 rulemenu.add_command(label='Reduce', underline=0,
272 command=self.reduce, accelerator='Ctrl-r')
273 rulemenu.add_separator()
274 rulemenu.add_command(label='Undo', underline=0,
275 command=self.undo, accelerator='Ctrl-u')
276 menubar.add_cascade(label='Apply', underline=0, menu=rulemenu)
277
278 viewmenu = Menu(menubar, tearoff=0)
279 viewmenu.add_checkbutton(label="Show Grammar", underline=0,
280 variable=self._show_grammar,
281 command=self._toggle_grammar)
282 viewmenu.add_separator()
283 viewmenu.add_radiobutton(label='Tiny', variable=self._size,
284 underline=0, value=10, command=self.resize)
285 viewmenu.add_radiobutton(label='Small', variable=self._size,
286 underline=0, value=12, command=self.resize)
287 viewmenu.add_radiobutton(label='Medium', variable=self._size,
288 underline=0, value=14, command=self.resize)
289 viewmenu.add_radiobutton(label='Large', variable=self._size,
290 underline=0, value=18, command=self.resize)
291 viewmenu.add_radiobutton(label='Huge', variable=self._size,
292 underline=0, value=24, command=self.resize)
293 menubar.add_cascade(label='View', underline=0, menu=viewmenu)
294
295 animatemenu = Menu(menubar, tearoff=0)
296 animatemenu.add_radiobutton(label="No Animation", underline=0,
297 variable=self._animate, value=0)
298 animatemenu.add_radiobutton(label="Slow Animation", underline=0,
299 variable=self._animate, value=20,
300 accelerator='-')
301 animatemenu.add_radiobutton(label="Normal Animation", underline=0,
302 variable=self._animate, value=10,
303 accelerator='=')
304 animatemenu.add_radiobutton(label="Fast Animation", underline=0,
305 variable=self._animate, value=4,
306 accelerator='+')
307 menubar.add_cascade(label="Animate", underline=1, menu=animatemenu)
308
309
310 helpmenu = Menu(menubar, tearoff=0)
311 helpmenu.add_command(label='About', underline=0,
312 command=self.about)
313 helpmenu.add_command(label='Instructions', underline=0,
314 command=self.help, accelerator='F1')
315 menubar.add_cascade(label='Help', underline=0, menu=helpmenu)
316
317 parent.config(menu=menubar)
318
320 self._feedbackframe = feedbackframe = Frame(parent)
321 feedbackframe.pack(fill='x', side='bottom', padx=3, pady=3)
322 self._lastoper_label = Label(feedbackframe, text='Last Operation:',
323 font=self._font)
324 self._lastoper_label.pack(side='left')
325 lastoperframe = Frame(feedbackframe, relief='sunken', border=1)
326 lastoperframe.pack(fill='x', side='right', expand=1, padx=5)
327 self._lastoper1 = Label(lastoperframe, foreground='#007070',
328 background='#f0f0f0', font=self._font)
329 self._lastoper2 = Label(lastoperframe, anchor='w', width=30,
330 foreground='#004040', background='#f0f0f0',
331 font=self._font)
332 self._lastoper1.pack(side='left')
333 self._lastoper2.pack(side='left', fill='x', expand=1)
334
336 self._cframe = CanvasFrame(parent, background='white',
337 width=525, closeenough=10,
338 border=2, relief='sunken')
339 self._cframe.pack(expand=1, fill='both', side='top', pady=2)
340 canvas = self._canvas = self._cframe.canvas()
341
342 self._stackwidgets = []
343 self._rtextwidgets = []
344 self._titlebar = canvas.create_rectangle(0,0,0,0, fill='#c0f0f0',
345 outline='black')
346 self._exprline = canvas.create_line(0,0,0,0, dash='.')
347 self._stacktop = canvas.create_line(0,0,0,0, fill='#408080')
348 size = self._size.get()+4
349 self._stacklabel = TextWidget(canvas, 'Stack', color='#004040',
350 font=self._boldfont)
351 self._rtextlabel = TextWidget(canvas, 'Remaining Text',
352 color='#004040', font=self._boldfont)
353 self._cframe.add_widget(self._stacklabel)
354 self._cframe.add_widget(self._rtextlabel)
355
356
357
358
359
361 scrollregion = self._canvas['scrollregion'].split()
362 (cx1, cy1, cx2, cy2) = [int(c) for c in scrollregion]
363
364
365 for stackwidget in self._stackwidgets:
366 self._cframe.destroy_widget(stackwidget)
367 self._stackwidgets = []
368 for rtextwidget in self._rtextwidgets:
369 self._cframe.destroy_widget(rtextwidget)
370 self._rtextwidgets = []
371
372
373 (x1, y1, x2, y2) = self._stacklabel.bbox()
374 y = y2-y1+10
375 self._canvas.coords(self._titlebar, -5000, 0, 5000, y-4)
376 self._canvas.coords(self._exprline, 0, y*2-10, 5000, y*2-10)
377
378
379 (x1, y1, x2, y2) = self._stacklabel.bbox()
380 self._stacklabel.move(5-x1, 3-y1)
381 (x1, y1, x2, y2) = self._rtextlabel.bbox()
382 self._rtextlabel.move(cx2-x2-5, 3-y1)
383
384
385 stackx = 5
386 for tok in self._parser.stack():
387 if isinstance(tok, parse.Tree):
388 attribs = {'tree_color': '#4080a0', 'tree_width': 2,
389 'node_font': self._boldfont,
390 'node_color': '#006060',
391 'leaf_color': '#006060', 'leaf_font':self._font}
392 widget = tree_to_treesegment(self._canvas, tok,
393 **attribs)
394 widget.node()['color'] = '#000000'
395 else:
396 widget = TextWidget(self._canvas, tok,
397 color='#000000', font=self._font)
398 widget.bind_click(self._popup_reduce)
399 self._stackwidgets.append(widget)
400 self._cframe.add_widget(widget, stackx, y)
401 stackx = widget.bbox()[2] + 10
402
403
404 rtextwidth = 0
405 for tok in self._parser.remaining_text():
406 widget = TextWidget(self._canvas, tok,
407 color='#000000', font=self._font)
408 self._rtextwidgets.append(widget)
409 self._cframe.add_widget(widget, rtextwidth, y)
410 rtextwidth = widget.bbox()[2] + 4
411
412
413 if len(self._rtextwidgets) > 0:
414 stackx += self._rtextwidgets[0].width()
415
416
417
418
419 stackx = max(stackx, self._stacklabel.width()+25)
420 rlabelwidth = self._rtextlabel.width()+10
421 if stackx >= cx2-max(rtextwidth, rlabelwidth):
422 cx2 = stackx + max(rtextwidth, rlabelwidth)
423 for rtextwidget in self._rtextwidgets:
424 rtextwidget.move(4+cx2-rtextwidth, 0)
425 self._rtextlabel.move(cx2-self._rtextlabel.bbox()[2]-5, 0)
426
427 midx = (stackx + cx2-max(rtextwidth, rlabelwidth))/2
428 self._canvas.coords(self._stacktop, midx, 0, midx, 5000)
429 (x1, y1, x2, y2) = self._stacklabel.bbox()
430
431
432 if len(self._rtextwidgets) > 0:
433 def drag_shift(widget, midx=midx, self=self):
434 if widget.bbox()[0] < midx: self.shift()
435 else: self._redraw()
436 self._rtextwidgets[0].bind_drag(drag_shift)
437 self._rtextwidgets[0].bind_click(self.shift)
438
439
440 self._highlight_productions()
441
443
444 midx = widget.bbox()[2]+50
445 self._canvas.coords(self._stacktop, midx, 0, midx, 5000)
446
453
454
455
456
457
459 if self._top is None: return
460 self._top.destroy()
461 self._top = None
462
464 self._parser.initialize(self._sent)
465 self._lastoper1['text'] = 'Reset Demo'
466 self._lastoper2['text'] = ''
467 self._redraw()
468
469 - def step(self, *e):
470 if self.reduce(): return 1
471 elif self.shift(): return 1
472 else:
473 if len(self._parser.parses()) > 0:
474 self._lastoper1['text'] = 'Finished:'
475 self._lastoper2['text'] = 'Success'
476 else:
477 self._lastoper1['text'] = 'Finished:'
478 self._lastoper2['text'] = 'Failure'
479
481 if self._animating_lock: return
482 if self._parser.shift():
483 tok = self._parser.stack()[-1]
484 self._lastoper1['text'] = 'Shift:'
485 self._lastoper2['text'] = '%r' % tok
486 if self._animate.get():
487 self._animate_shift()
488 else:
489 self._redraw()
490 return 1
491 return 0
492
494 if self._animating_lock: return
495 production = self._parser.reduce()
496 if production:
497 self._lastoper1['text'] = 'Reduce:'
498 self._lastoper2['text'] = '%s' % production
499 if self._animate.get():
500 self._animate_reduce()
501 else:
502 self._redraw()
503 return production
504
505 - def undo(self, *e):
506 if self._animating_lock: return
507 if self._parser.undo():
508 self._redraw()
509
510 - def postscript(self, *e):
511 self._cframe.print_to_file()
512
513 - def mainloop(self, *args, **kwargs):
514 """
515 Enter the Tkinter mainloop. This function must be called if
516 this demo is created from a non-interactive program (e.g.
517 from a secript); otherwise, the demo will close as soon as
518 the script completes.
519 """
520 if in_idle(): return
521 self._top.mainloop(*args, **kwargs)
522
523
524
525
526
542
543 - def help(self, *e):
544
545 try:
546 ShowText(self._top, 'Help: Chart Parser Demo',
547 (__doc__).strip(), width=75, font='fixed')
548 except:
549 ShowText(self._top, 'Help: Chart Parser Demo',
550 (__doc__).strip(), width=75)
551
553 ABOUT = ("NLTK Shift-Reduce Parser Demo\n"+
554 "Written by Edward Loper")
555 TITLE = 'About: Shift-Reduce Parser Demo'
556 try:
557 from tkMessageBox import Message
558 Message(message=ABOUT, title=TITLE).show()
559 except:
560 ShowText(self._top, TITLE, ABOUT)
561
564
571
577
579 self._sent = sent.split()
580 self.reset()
581
582
583
584
585
587 if self._show_grammar.get():
588 self._prodframe.pack(fill='both', side='left', padx=2,
589 after=self._feedbackframe)
590 self._lastoper1['text'] = 'Show Grammar'
591 else:
592 self._prodframe.pack_forget()
593 self._lastoper1['text'] = 'Hide Grammar'
594 self._lastoper2['text'] = ''
595
614
616
617 productions = self._parser.reducible_productions()
618 if len(productions) == 0: return
619
620 self._reduce_menu.delete(0, 'end')
621 for production in productions:
622 self._reduce_menu.add_command(label=str(production),
623 command=self.reduce)
624 self._reduce_menu.post(self._canvas.winfo_pointerx(),
625 self._canvas.winfo_pointery())
626
627
628
629
630
632
633 widget = self._rtextwidgets[0]
634
635
636 right = widget.bbox()[0]
637 if len(self._stackwidgets) == 0: left = 5
638 else: left = self._stackwidgets[-1].bbox()[2]+10
639
640
641 dt = self._animate.get()
642 dx = (left-right)*1.0/dt
643 self._animate_shift_frame(dt, widget, dx)
644
646 if frame > 0:
647 self._animating_lock = 1
648 widget.move(dx, 0)
649 self._top.after(10, self._animate_shift_frame,
650 frame-1, widget, dx)
651 else:
652
653
654
655 del self._rtextwidgets[0]
656 self._stackwidgets.append(widget)
657 self._animating_lock = 0
658
659
660 self._draw_stack_top(widget)
661 self._highlight_productions()
662
664
665 numwidgets = len(self._parser.stack()[-1])
666 widgets = self._stackwidgets[-numwidgets:]
667
668
669 if isinstance(widgets[0], TreeSegmentWidget):
670 ydist = 15 + widgets[0].node().height()
671 else:
672 ydist = 15 + widgets[0].height()
673
674
675 dt = self._animate.get()
676 dy = ydist*2.0/dt
677 self._animate_reduce_frame(dt/2, widgets, dy)
678
680 if frame > 0:
681 self._animating_lock = 1
682 for widget in widgets: widget.move(0, dy)
683 self._top.after(10, self._animate_reduce_frame,
684 frame-1, widgets, dy)
685 else:
686 del self._stackwidgets[-len(widgets):]
687 for widget in widgets:
688 self._cframe.remove_widget(widget)
689 tok = self._parser.stack()[-1]
690 if not isinstance(tok, parse.Tree): raise ValueError()
691 label = TextWidget(self._canvas, str(tok.node), color='#006060',
692 font=self._boldfont)
693 widget = TreeSegmentWidget(self._canvas, label, widgets,
694 width=2)
695 (x1, y1, x2, y2) = self._stacklabel.bbox()
696 y = y2-y1+10
697 if not self._stackwidgets: x = 5
698 else: x = self._stackwidgets[-1].bbox()[2] + 10
699 self._cframe.add_widget(widget, x, y)
700 self._stackwidgets.append(widget)
701
702
703 self._draw_stack_top(widget)
704 self._highlight_productions()
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732 self._animating_lock = 0
733
734
735
736
737
759
761
762 if self._hover == -1: return
763 self._hover = -1
764 for stackwidget in self._stackwidgets:
765 if isinstance(stackwidget, TreeSegmentWidget):
766 stackwidget.node()['color'] = 'black'
767 else:
768 stackwidget['color'] = 'black'
769
770
772 """
773 Create a shift reduce parser demo, using a simple grammar and
774 text.
775 """
776
777 from nltk import cfg
778 nonterminals = 'S VP NP PP P N Name V Det'
779 (S, VP, NP, PP, P, N, Name, V, Det) = [cfg.Nonterminal(s)
780 for s in nonterminals.split()]
781
782 productions = (
783
784 cfg.Production(S, [NP, VP]),
785 cfg.Production(NP, [Det, N]),
786 cfg.Production(NP, [NP, PP]),
787 cfg.Production(VP, [VP, PP]),
788 cfg.Production(VP, [V, NP, PP]),
789 cfg.Production(VP, [V, NP]),
790 cfg.Production(PP, [P, NP]),
791
792
793 cfg.Production(NP, ['I']), cfg.Production(Det, ['the']),
794 cfg.Production(Det, ['a']), cfg.Production(N, ['man']),
795 cfg.Production(V, ['saw']), cfg.Production(P, ['in']),
796 cfg.Production(P, ['with']), cfg.Production(N, ['park']),
797 cfg.Production(N, ['dog']), cfg.Production(N, ['statue']),
798 cfg.Production(Det, ['my']),
799 )
800
801 grammar = cfg.Grammar(S, productions)
802
803
804 sent = 'my dog saw a man in the park with a statue'.split()
805
806 ShiftReduceDemo(grammar, sent).mainloop()
807
808 if __name__ == '__main__': demo()
809