#!/usr/bin/python # # Generate a PStrick version of a yacc grammar file # WWW: http://se.wtb.tue.nl/~hat/showgrammar/ # # $Id: showgrammar.py,v 1.2 2000/09/01 10:35:19 hat Exp $ # import sys,getopt from spark import GenericScanner, GenericParser # Global list with tokens # Should be non-empty when used globterminallist=[] # # -------------------------------------------------- # SCANNER TOKENS # class Token: def __init__(self, type, line, attr=None): self.type = type self.attr = attr self.line = line def __cmp__(self, o): return cmp(self.type, o) def __repr__(self): if self.attr == None: return self.type+"@"+str(self.line) else: return self.type+"@"+str(self.line)+"("+self.attr+")" # # -------------------------------------------------- # SCANNER # class GrammarScanner(GenericScanner): def __init__(self): GenericScanner.__init__(self) self.lineno=1 def tokenize(self,input): self.rv=[] self.lineno=1 GenericScanner.tokenize(self,input) return self.rv # # Token definitions # def t_whitespace(self,s): r'[ \t]+' pass def t_nl(self,s): r'\n' self.lineno = self.lineno+1 def t_colon(self,s): r':' self.rv.append(Token('colon',self.lineno)) def t_semicol(self,s): r';' self.rv.append(Token('semicol',self.lineno)) def t_alt(self,s): r'\|' self.rv.append(Token('alt',self.lineno)) def t_iden(self,s): r'[a-zA-Z][a-zA-Z0-9]*' t=Token('iden', self.lineno, attr=s) self.rv.append(t) def t_start(self,s): r'%start' t = Token('start', self.lineno) self.rv.append(t) def t_token(self,s): r'%token' t = Token('token', self.lineno) self.rv.append(t) def t_percperc(self,s): r'%%' self.rv.append(Token('percperc', self.lineno)) def scan(input): data = input.read() scanner = GrammarScanner() return scanner.tokenize(data) # # -------------------------------------------------- # PARSER # class GrammarParser(GenericParser): def __init__(self,start='gramfile'): GenericParser.__init__(self,start) # # Parse rules # def p_gramfile(self,args): 'gramfile ::= seca percperc secb' return (args[0][0],args[0][1],args[2]) def p_seca_1(self,args): ' seca ::= ' return ([],[]) def p_seca_2(self,args): ' seca ::= seca start iden ' r = args[0] r[0].append(args[2].attr) return r def p_seca_3(self,args): 'seca ::= seca token idenlist' ret = args[0] for t in args[2]: ret[1].append(t) return ret def p_idenlist_1(self,args): 'idenlist ::= iden' return [args[0].attr] def p_idenlist_2(self,args): 'idenlist ::= idenlist iden' return args[0]+[args[1].attr] def p_secb_1(self,args): 'secb ::= ' return [] def p_secb_2(self,args): 'secb ::= secb rule' return args[0]+args[1] def p_rule(self,args): 'rule ::= iden colon rulealts semicol' ret = [] for r in args[2]: ret.append( (args[0].attr,r) ) return ret def p_rulealts_1(self,args): 'rulealts ::= seqrule' return [ args[0] ] def p_rulealts_2(self,args): 'rulealts ::= rulealts alt seqrule' return args[0] + [ args[2] ] def p_seqrule_1(self,args): 'seqrule ::=' return [] def p_seqrule_2(self,args): 'seqrule ::= idenlist' return args[0] # parse returns a triple # ( [ start tokens ] [ tokens ] [ (lhs-iden, [ rhs-sequence ] ) ] ) def parse(tokens): parser = GrammarParser() return parser.parse(tokens) # # -------------------------------------------------- # GENERATOR # Works in 2 steps for each set of production rules # 1: Generate an abstract representation # 2: Convert to actual LaTeX code # # # First some classes of core graphical elements # class Graphic: def __init__(self,tp): self.type = tp def _coord(self,x,y): return '('+str(x)+','+str(y)+')' def _label(self,x,y): return '{p'+str(x)+'_'+str(y)+'}' class Line(Graphic): def __init__(self,fx,fy,tx,ty, arr='-'): Graphic.__init__(self,'line') self.x1=fx self.y1=fy self.x2=tx self.y2=ty assert(fx>=-1000 and fx<1000) assert(fy>=-1000 and fy<1000) assert(tx>=-1000 and tx<1000) assert(ty>=-1000 and ty<1000) self.arrow=arr def __repr__(self): return 'Line<'+self._coord(self.x1,self.y1)+self._coord(self.x2,self.y2)+",'"+self.arrow+"'>" def area(self): if self.x11000: raise "Node x coord out of range" if ny<-1000 or ny>1000: raise "Node y coord out of range" def __repr__(self): return 'Node<'+self._coord(self.x,self.y)+",'"+self.text+"'>" def area(self): return (self.x,self.y,self.x,self.y) def mirrorX(self,maxx): self.x=maxx-self.x def mirrorY(self,maxy): self.y=maxy-self.y def shift(self,dx,dy): self.x=self.x+dx self.y=self.y+dy def _gencode(self,output,nodelabels): global globterminallist if nodelabels.has_key( (self.x, self.y) ): raise "Node already defined" label = self._label(self.x,self.y) nodelabels[(self.x, self.y)]=label if len(self.text)==0: output.append( '\\pnode'+self._coord(self.x,self.y)+label ) else: assert(len(globterminallist)>0) if self.text in globterminallist: #tekst = '{\\rnode'+label+'{\\'+self.text+'}}' tekst = '{\\rnode'+label+'{\\psframebox{\\'+self.text+'}}}' else: tekst = '{\\rnode'+label+'{\\psframebox[framearc=1]{\\'+self.text+'}}}' output.append( '\\rput'+self._coord(self.x,self.y)+tekst ) class Drawing(Graphic): def __init__(self): Graphic.__init__(self,'drawing') self.contents = [] def __repr__(self): s='Drawing[' b=0 for c in self.contents: if b: s=s+',' else: b=1 s=s+c.__repr__() return s+']' def area(self): if len(self.contents)==0: return (0,0,0,0) # Not entirely correct, but will do (sx,sy,bx,by)=self.contents[0].area() for c in self.contents: (a,b,v,w)=c.area() if abx: bx=v if w>by: by=w return (sx,sy,bx,by) def mirrorX(self,maxx): for c in self.contents: c.mirrorX(maxx) def mirrorY(self,maxy): for c in self.contents: c.mirrorY(maxy) def shift(self,dx,dy): for c in self.contents: c.shift(dx,dy) def add(self,g): if g.type == 'line' or g.type == 'node': self.contents.append(g) elif g.type == 'drawing': for c in g.contents: self.contents.append(c) else: raise "Don't know how to add type '"+g.type+"'" # Append the pspicture to the output def gencode(self,output): nodelabels={} (sx,sy,bx,by)=self.area() output.append('\\begin{pspicture}'+self._coord(sx,sy)+self._coord(bx,by)) for c in self.contents: c._gencode(output,nodelabels) output.append('\\end{pspicture}') return output # # -------------------------------------------------- # BLOCK GENERATOR # # A block is a square, with a vertical line on the left and right, and # horizontal under each-other the alternatives. The generator constructs the # block upside down, so before outputting the block you are advised to mirror # it first in Y. # class BlockGenerator: def __init__(self): pass # prods is a list of alternatives sequnces, all assumed to be non-empty # maxlen = max #identifiers on 1 line # positionemptyrule: 0=do not generate # 1=generate below (after mirrorring in Y at top) # 2=generate above (after mirrorring in Y at bottom) # leftside: 1=connect at bottom, 2=connect at top, 3=connect at both # rightside: 1=connect at bottom, 2=connect at top, 3=connect at both def generate(self, prods, maxlen, positionemptyrule, leftside=1, rightside=2): dr = Drawing() unfinishedstart=[] unfinishedend=[] ypos=0 wrapped=0 if len(prods)==0 and positionemptyrule==0: raise "Empty block" if positionemptyrule == 1: (ypos,wr)=self._generateEmpty(dr,unfinishedstart,unfinishedend,ypos) assert(wr==0) for p in prods: if len(p)==0: raise "Empty sequence" (ypos,wr)=self._generateAlt(p,dr,unfinishedstart,unfinishedend,maxlen,ypos) if wr: wrapped=1 if positionemptyrule == 2: (ypos,wr)=self._generateEmpty(dr,unfinishedstart,unfinishedend,ypos) assert(wr==0) # Left side of the block miny=10000 if leftside & 1: miny=0 maxy=0 if leftside & 2: maxy=ypos-10 for q in unfinishedstart: assert(q[0]==0) if q[1]maxy: maxy=q[1] if miny!=maxy: dr.add( Line(0,miny, 0,maxy) ) # Right side of the block maxx=10 if wrapped: maxx=maxlen*10 miny=10000 if rightside & 1: miny=0 maxy=0 if rightside & 2: maxy=ypos-10 for q in unfinishedend: if q[0]>maxx: maxx=q[0] if q[1]maxy: maxy=q[1] for q in unfinishedend: dr.add( Line(q[0],q[1], maxx+10,q[1]) ) if miny!=maxy: dr.add( Line(maxx+10,miny, maxx+10, maxy) ) return dr def _generateEmpty(self,dr,unfinishedstart,unfinishedend,ypos): unfinishedstart.append( (0,ypos) ) dr.add( Line(0,ypos, 10,ypos, '->') ) unfinishedend.append( (10,ypos) ) return (ypos+10,0) def _generateAlt(self,prod,dr,unfinishedstart,unfinishedend,maxlen,ypos): startx=0 unfinishedstart.append( (startx,ypos) ) wrapped=0 while 1: if len(prod)>maxlen: line = prod[:maxlen] prod=prod[maxlen:] else: line = prod prod=[] xpos=10 for id in line: dr.add( Node(xpos,ypos,id) ) xpos=xpos+10 for n in range(10,xpos-10,10): dr.add( Line(n,ypos,n+10,ypos) ) dr.add( Line(startx,ypos, 10,ypos, '->') ) if len(prod)>0: assert(maxlen*10 == xpos-10) dr.add( Line(maxlen*10,ypos, maxlen*10+8,ypos) ) dr.add( Line(maxlen*10+8,ypos, maxlen*10+8,ypos+5) ) dr.add( Line(maxlen*10+8,ypos+5, 2, ypos+5) ) dr.add( Line(2,ypos+5, 2,ypos+10) ) ypos=ypos+10 startx=2 wrapped=1 else: unfinishedend.append( (xpos-10,ypos) ) ypos=ypos+10 break; return (ypos,wrapped) class CodeGenerator: def __init__(self,mxlen,xunlen,yunlen,wanterrorrules,gennewcmds,newcmdsfname,genheaders,indexcount): self.errorrulescopied=wanterrorrules self.maxseqlen = mxlen self.xunitlen=xunlen self.yunitlen=yunlen self.generatenewcommands=gennewcmds self.newcommandsfname=newcmdsfname self.genlatexheaders=genheaders self.index=indexcount self.blockgen = BlockGenerator() def _LaTeXHeader(self,output): if self.genlatexheaders: output.append('\\documentclass{article}') output.append('\\usepackage{a4wide}') output.append('\\usepackage{pstricks}') output.append('\\usepackage{pst-node}') output.append('') output.append('\\psset{xunit='+self.xunitlen+',yunit='+self.yunitlen+'}') output.append('') output.append('\\begin{document}') output.append('') return output def _LaTeXFooter(self,output): if self.genlatexheaders: output.append('') output.append('\\end{document}') return output def _newCommands(self,tokens,output): for t in tokens: output.append('\\newcommand{\\'+t+'}{'+t+'}') output.append('') return output def _findRules(self,lhs,grammar): prods=[] for g in grammar: if g[0]==lhs: if self.errorrulescopied or 'error' not in g[1]: prods=prods+[ g[1] ] return prods def generate(self, startlist, terminallist, grammar): global globterminallist todolist = startlist[:] donelist = [] output=[] if self.errorrulescopied: terminallist=terminallist+['error'] globterminallist=terminallist # Fill the global terminallist while len(todolist)>0: lhs = todolist[0] del todolist[0] # Deal with rules for 'lhs' prods = self._findRules(lhs,grammar) output=self.generateSection(lhs,prods,output) # Update todo/done administration donelist.append(lhs) for p in prods: for i in p: if i not in todolist and i not in donelist and i not in terminallist and i!='error': todolist.append(i) if self.index>0: index=self.generateIndex(donelist,[]) else: index=['% Index not generated'] ncmds = [] ncmds.append('% List of tokens') ncmds.append('%') ncmds.append('% Terminal symbols') ncmds = self._newCommands(terminallist,ncmds) ncmds.append('%') ncmds.append('% Non-terminal symbols') ncmds = self._newCommands(donelist,ncmds) if self.generatenewcommands and len(self.newcommandsfname)>0: fp = open(self.newcommandsfname,'w') for nc in ncmds: fp.write(nc+'\n') fp.close() header=[] header = self._LaTeXHeader(header) if len(self.newcommandsfname)>0: header.append('\\input{'+self.newcommandsfname+'}') else: for nc in ncmds: header=header+[nc] header.append('') footer=[] footer=self._LaTeXFooter(footer) return header+output+index+footer def _splitEmptyNonempty(self,prods): empty=0 ne=[] for p in prods: if len(p)>0: ne=ne+[p] else: empty=1 return (empty,ne) # Generate a drawing from left to right with a choice between several # alternatives def generateDefault(self,prods,output): (hasempty,prods)=self._splitEmptyNonempty(prods) if hasempty: drawing = self.blockgen.generate(prods,self.maxseqlen,1,1,2) else: drawing = self.blockgen.generate(prods,self.maxseqlen,0,1,2) # Add ingoing and outgoing lines area=drawing.area() drawing.add( Line(area[0],area[1], area[0]-5,area[1]) ) drawing.add( Line(area[2],area[3], area[2]+5,area[3]) ) drawing.mirrorY(area[3]) drawing.shift(5,0) return drawing.gencode(output) # Empty rule, followed by repetitive selection of one of the sufs def generateLoopback(self,sufs,output): drawing = self.blockgen.generate(sufs,self.maxseqlen,0,1,1) area=drawing.area() drawing.mirrorX(area[2]) # Add the empty rule above drawing.shift(0,10) drawing.add( Line(0,0, 10,0,'->') ) drawing.add( Line(10,0,area[2],0) ) drawing.add( Line(0,10,0,0, '->') ) drawing.add( Line(area[2],0, area[2],10) ) # Add the start and end lines drawing.shift(5,0) drawing.add( Line(0,0, 5,0) ) drawing.add( Line(area[2]+5,0, area[2]+5+5,0) ) # Turn it upside down area=drawing.area() drawing.mirrorY(area[3]) return drawing.gencode(output) # Non-empty prefix, followed by 0 or more sequences of one of the suffixes # and the prefix again. Suffix rule may be empty. def generateLooparound(self,pre,back,output): predr=self.blockgen.generate(pre,self.maxseqlen,0,2,2) prearea=predr.area() predr.mirrorY(prearea[3]) (hasempty,back)=self._splitEmptyNonempty(back) if hasempty: sufdr=self.blockgen.generate(back,self.maxseqlen,1,1,1) else: sufdr=self.blockgen.generate(back,self.maxseqlen,0,1,1) sufarea = sufdr.area() sufdr.mirrorX(sufarea[2]) sufdr.mirrorY(sufarea[3]) # Find the biggest edge maxx = prearea[2] if maxx') ) sufdr.add( Line(-5,sufarea[3]+10, 0,sufarea[3]+10) ) # Entry line # exit line sufdr.add( Line(prearea[2],sufarea[3]+10,maxx+5,sufarea[3]+10) ) sufdr.add( Line(maxx,sufarea[3], maxx,sufarea[3]+10) ) # Line down to suffix block if sufarea[2]') ) predrawing.add( Line(prearea[0]-5,prearea[3], prearea[0],prearea[3]) ) sufdrawing = self.blockgen.generate(suf,self.maxseqlen,0,1,1) sufarea = sufdrawing.area() sufdrawing.mirrorY(sufarea[3]) if prearea[2]>sufarea[2]: sufdrawing.shift(prearea[2]-sufarea[2],0) sufarea = sufdrawing.area() elif sufarea[2]>prearea[2]: predrawing.shift(sufarea[2]-prearea[2],0) else: pass # prearea[2]==sufarea[2] predrawing.shift(0,sufarea[3]+10+10) sufdrawing.add(predrawing) sufdrawing.add( Line(sufarea[2],sufarea[3], sufarea[2],sufarea[3]+10,'->') ) sufdrawing.add( Line(sufarea[2],sufarea[3]+10,sufarea[2]-10,sufarea[3]+10,'->') ) if sufarea[2]-10>sufarea[0]: sufdrawing.add( Line(sufarea[0],sufarea[3]+10, sufarea[2]-10, sufarea[3]+10) ) sufdrawing.add( Line(sufarea[0],sufarea[3]+10, sufarea[0],sufarea[3]) ) sufdrawing.add( Line(sufarea[2],sufarea[3]+10, sufarea[2]+5, sufarea[3]+10) ) # Align the entire drawing sufarea = sufdrawing.area() if sufarea[0]!=0 or sufarea[1]!=0: sufdrawing.shift(-sufarea[0],-sufarea[1]) return sufdrawing.gencode(output) # Try to split the set of rules in prefixes and suffixes # Prefix is X ::= # Suffix is X ::= X # Return ( , , ) # bool is 1 if split was successful, 0 otherwise # suffix rules do not have lhs any more def _splitPrefixSuffix(self,lhs,prods): res=([],[],1) for p in prods: if len(p)==0: res=(res[0]+[p],res[1],1) elif lhs not in p: res=(res[0]+[p],res[1],1) elif len(p)>1 and p[0]==lhs and lhs not in p[1:]: res=(res[0],res[1]+[p[1:]],1) else: return ([],[],0) return res def generateIndex(self,ntlist,output): if self.index>0 and len(ntlist)>0: ntlist.sort() output.append('\\section*{Index}') colspec='' for i in range(0,self.index): colspec=colspec+'l' output.append('\\begin{tabular}{'+colspec+'}') ylen = len(ntlist)/self.index if len(ntlist)%self.index>0: ylen=ylen+1 lines=[] for y in range(0,ylen): lines.append('') # Add the columns while len(ntlist)>0: # Prefix an & if needed if len(lines[0])>0: for y in range(0,ylen): lines[y]=lines[y]+'\t&' for y in range(0,ylen): if len(ntlist)>0: lhs=ntlist[0] ntlist=ntlist[1:] lines[y]=lines[y]+' \\'+lhs+'{}, \\pageref{sec:'+lhs+'}' for y in range(0,ylen-1): lines[y]=lines[y]+'\t\\\\' for y in range(0,ylen): output.append(lines[y]) output.append('\\end{tabular}') return output def generateSection(self,lhs,prods,output): if self.index==0: output.append('\\section*{\\'+lhs+'}') else: output.append('\\section*{\\'+lhs+'}\\label{sec:'+lhs+'}') (pre,suf,ok)=self._splitPrefixSuffix(lhs,prods) # It is not a set rules with the form # X::= A B C or the form X::= X A B C # Generate a diagram in default output format if not ok: output = self.generateDefault(prods,output) output.append('') return output # rules could be split # Seperate empty/non empty prefix rules (preempty,nonemptypreprods)=self._splitEmptyNonempty(pre) # No sufiixes -> use the default if len(suf)==0: output = self.generateDefault(prods,output) output.append('') return output # Empty prefix if len(pre)==1 and preempty: output = self.generateLoopback(suf,output) output.append('') return output if len(pre)==1 and not preempty: # one non-empty prefix prerule=pre[0] back=[] b=1 for s in suf: if len(s)>=len(prerule) and s[-len(prerule):]==prerule: back=back+[s[:-len(prerule)]] else: b=0 break if b: # One prefix, and is also used at the end of all suffixes # -> Loop around # Note that back is a shortened version of the sufs # it may contain empty rules (e.g. X ::= A | X A) output=self.generateLooparound(pre,back, output) output.append('') return output # Default prefix/suffix diagram output = self.generatePrefixSuffix(pre,suf,output) output.append('') return output # CODE BELOW NOT REACHED !! # Real ugly default (mainly for debugging purposes) for p in prods: output.append(str(p)) output.append('') return output def generate(p,maxlen,xunitlen,yunitlen,wanterrorrules,generatenewcommands,newcommandsfname,genheaders,indexcount): cg=CodeGenerator(maxlen,xunitlen,yunitlen,wanterrorrules,generatenewcommands,newcommandsfname,genheaders,indexcount) (sl,tl,g)=p output=cg.generate(sl,tl,g) return output # # -------------------------------------------------- # Main program # if __name__ == '__main__': # Default program option settings copyerror=0 # Default do not copy error grammar rules gennewcommands=0 # Generate a terminal file genheaders=0 maxlen=7 newcommandsfname='' xunitlen='1.7mm' yunitlen='1mm' indexcount=0 (opts,args) = getopt.getopt(sys.argv[1:],'eghm:t:x:y:i:') for op in opts: if op[0]=='-e': copyerror=1 elif op[0]=='-g': gennewcommands=1 elif op[0]=='-h': genheaders=1 elif op[0]=='-m': maxlen=int(op[1]) elif op[0]=='-t': newcommandsfname=op[1] elif op[0]=='-x': xunitlen=op[1] elif op[0]=='-y': yunitlen=op[1] elif op[0]=='-i': indexcount=int(op[1]) else: assert(0) # We should never get here if len(newcommandsfname)==0: gennewcommands=0 # Read the input if len(args)==0: s=scan(sys.stdin) elif len(args)==1 or len(args)==2: input=open(args[0],'r') s=scan(input) input.close() else: stderr.write('showgrammar: Too many arguments\n') sys.exit(1) p=parse(s) output=generate(p,maxlen,xunitlen,yunitlen,copyerror,gennewcommands, newcommandsfname,genheaders,indexcount) # Write the output if len(args)==0 or len(args)==1: for l in output: print l elif len(args)==2: outfile=open(args[1],'w') for l in output: outfile.write(l+'\n') outfile.close()