#说明
##写在前面
本笔记是将 Lex&YACC HOWTO[1] 的部分内容用PLY重新实现。
本文不是正则表达式的教程,相关内容请寻找其他教程。
PLY文档[2]的翻译(以及原文)也是非常重要的参考资料。
本文代码可在此处下载。
##PLY能做什么
PLY能帮助你更方便地完成编译器前端的工作-词法分析和语法分析,后端使用LLVM即可创造一个新的编程语言。
词法分析部分使用正则表达式,LEX内部利用有穷自动机完成词法识别。
语法分析可以使用LALR(1)分析和SLR(1)分析。
类似Java的AntLR,PLY也能帮助你开发领域特定语言(Domain Specific Language)。
#安装PLY
pip install ply
#lex简易示例
##认识lex
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| import ply.lex as lex tokens = ['START','STOP'] def t_START(t): r'start' print "start command received" return t def t_STOP(t): r'stop' print "stop command received" return t def t_newline(t): r'\n+' t.lexer.lineno += t.value.count("\n") def t_error(t): print "Illegal character '%s'" % t.value[0] t.lexer.skip(1) lexer = lex.lex() s = ''' stop and start ''' lexer.input(s) while True: tok = lexer.token() if not tok: break
|
##一个更复杂的示例
假设下面是一个我们想解析的文件:
1 2 3 4 5 6 7 8 9
| logging { category lame−servers { null; }; category cname { null; }; }; zone "." { type hint; file "/etc/bind/db.root"; };
|
这个文件中有以下几类符号(tokens)
WORDs ,如zone和type
FILENAMEs ,如/etc/bind/db.root
QUOTEs ,如包括文件名的符号
OBRACEs ,左花括号{
EBRACEs ,右花括号}
SEMICOLONs ,;
对应的lex文件如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| import ply.lex as lex tokens = ["WORD","FILENAME","QUOTE","OBRACE","EBRACE","SEMICOLON"] def t_WORD(t): r'[a-zA-Z][a-zA-Z0-9-]*' print "WORD ", return t def t_FILENAME(t): r'[a-zA-Z0-9/.-]+' print "FILENAME ", return t def t_QUOTE(t): r'"' print "QUOTE ", return t def t_OBRACE(t): r'{' print "OBRACE ", return t def t_EBRACE(t): r'}' print "EBRACE ", return t def t_SEMICOLON(t): r';' print "SEMICOLON ", return t t_ignore = " \t" def t_newline(t): r'\n+' t.lexer.lineno += t.value.count("\n") print '' def t_error(t): print "Illegal character '%s'" % t.value[0] t.lexer.skip(1) lexer = lex.lex() file_object = open('test.conf') s = file_object.read() print s lexer.input(s) while True: tok = lexer.token() if not tok: break
|
#yacc示例
##一个简单的温度调节控制器
我们想用一门简单的语言去控制一个温度调节器,例如:
1 2 3 4 5 6
| heat on Heater on! heat off Heater off! target temperature 22 New temperature set!
|
我们需要辨别的符号有:heat,on/off(STATE),target,temperature,NUMBER。对应的lex文件如下(Example 3):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| import ply.lex as lex tokens = ['NUMBER','TOKHEAT','STATE','TOKTARGET','TOKTEMPRATURE'] def t_NUMBER(t): r'[0-9]+' return t; def t_TOKHEAT(t): r'heat' return t def t_STATE(t): r'on|off' return t def t_TOKTARGET(t): r'target' return t def t_TOKTEMPRATURE(t): r'temprature' return t t_ignore = " \t" def t_newline(t): r'\n+' t.lexer.lineno += t.value.count("\n") def t_error(t): print "Illegal character '%s'" % t.value[0] t.lexer.skip(1) lexer = lex.lex()
|
接下来是在yacc文件中用产生式说明文法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| import ply.yacc as yacc from exam3lex import tokens def p_commands(p): '''commands : empty | commands command ''' def p_command(p): '''command : heatswitch | targetset''' def p_heatswitch(p): 'heatswitch : TOKHEAT STATE' print "Heat turned on or off" def p_targetset(p): 'targetset : TOKTARGET TOKTEMPRATURE NUMBER' print "temprature set" def p_empty(p): 'empty :' pass def p_error(p): print "Syntax error in input!" parser = yacc.yacc() while True: try: s = raw_input('calc > ') except EOFError: break if not s: continue result = parser.parse(s) print result
|
##拓展温度调节器使其可处理参数
利用参数p[0],p[1]等获取属性值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| import ply.lex as lex import ply.yacc as yacc tokens = ['NUMBER','TOKHEAT','STATE','TOKTARGET','TOKTEMPRATURE'] def t_NUMBER(t): r'[0-9]+' return t; def t_TOKHEAT(t): r'heat' return t def t_STATE(t): r'on|off' return t def t_TOKTARGET(t): r'target' return t def t_TOKTEMPRATURE(t): r'temprature' return t t_ignore = " \t" def t_newline(t): r'\n+' t.lexer.lineno += t.value.count("\n") def t_error(t): print "Illegal character '%s'" % t.value[0] t.lexer.skip(1) def p_commands(p): '''commands : empty | commands command ''' def p_command(p): '''command : heat_switch | target_set''' def p_heatswitch(p): 'heat_switch : TOKHEAT STATE' print "Heat turned " + p[2] def p_targetset(p): 'target_set : TOKTARGET TOKTEMPRATURE NUMBER' print "temprature set " + p[3] def p_empty(p): 'empty :' pass def p_error(p): print "Syntax error in input!" lexer = lex.lex() parser = yacc.yacc() while True: try: s = raw_input('input > ') except EOFError: break if not s: continue result = parser.parse(s)
|
##解析配置文件
让我们继续讨论前面提到的配置文件:
1 2 3 4
| zone "." { type hint; file "/etc/bind/db.root"; }
|
example 5:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
| import ply.lex as lex import ply.yacc as yacc reserved = { 'zone' : 'ZONETOK', 'file' : 'FILETOK', 'else' : 'ELSE', } tokens = ['WORD','FILENAME','QUOTE','OBRACE','EBRACE','SEMICOLON'] + list(reserved.values()) t_FILENAME = r'[a-zA-Z0-9/.-]+' t_QUOTE = r'"' t_OBRACE = r'{' t_EBRACE = r'}' t_SEMICOLON = r';' def t_WORD(t): r'[a-zA-Z][a-zA-Z0-9]+' t.type = reserved.get(t.value,'WORD') return t t_ignore = " \t" def t_newline(t): r'\n+' t.lexer.lineno += t.value.count("\n") def t_error(t): print "Illegal character '%s'" % t.value[0] t.lexer.skip(1) def p_commands(p): '''commands : empty | commands command ''' if len(p) == 3: p[0] = p[2] def p_command(p): 'command : zone_set' p[0] = p[1] def p_zoneset(p): 'zone_set : ZONETOK quotename zonecontent' print "complete zone for",p[2],"found" p[0] = p[3] def p_zonecontent(p): 'zonecontent : OBRACE zonestatements EBRACE SEMICOLON' p[0] = p[2] def p_quotename(p): 'quotename : QUOTE FILENAME QUOTE' p[0] = p[2] def p_zonestatements(p): '''zonestatements : empty | zonestatements zonestatement SEMICOLON ''' if len(p) == 4: p[0] = p[2] def p_zonestatement(p): '''zonestatement : statements | FILETOK quotename ''' if p[1]=='file': p[0] = p[2] print "a zonefile name",p[2],"was encountered" def p_block(p): 'block : OBRACE zonestatements EBRACE SEMICOLON' def p_statements(p): '''statements : empty | statements statement ''' def p_statement(p): '''statement : WORD | block | quotename''' def p_error(p): print "Syntax error in input!" def p_empty(p): 'empty :' pass lexer = lex.lex() parser = yacc.yacc() file_object = open('test2.conf') s = file_object.read() print s result = parser.parse(s) print result
|
#深度阅读
GUN YACC (Bison)带有一个很常不错的info文件(.info),它是非常好的YACC语法文档,除了里面仅提到了一次Lex,其它的都还好。可以使用Emacs阅读info文件,或者非常不错的工具pinfo。
Flex有一个不错的用户手册,如果你已经理解Flex是做什么的,它还是非常有用的。
读完了这个Lex和YACC介绍,你可能想找到更多的信息。虽然以下的书我一本都没看过,不过听说不错:
Bision-The Yacc-Compatible Parser Generator
Lex&Yacc
Compliers: Principles,Techiniques,and Tools
Tohmas Niemann 写了一篇文档,讨论如何使用Lex和YACC写一个编译器和计算器。
usenet新闻组com.compilers也是非常有用的,不过请记住,那些人并非专门服务支持,在你发贴之前,阅读他们的感兴趣的页面,特别是FAQ
Lex-A Lexical Analyzer Generator[4],M.E.Lesk and E.Schmidt,最原始的论文。
Yacc: Yet Another Compiler[5]
#参考资料
以下推荐部分参考资料。
比以上内容略复杂的PLY示例项目[3]中的calc.py感觉很适合作为进一步学习的资料。
[1] Lex&YACC HOWTO 中文翻译
[2] PLY文档 中文翻译
[3] PLY示例项目
[4] Lex-A Lexical Analyzer Generator,M.E.Lesk and E.Schmidt
[5] Yacc: Yet Another Compiler
[6] janryWang/exp-parser 漫谈如何自制表达式解析器