1. 前言
總所周知,編譯器是一個(gè)將一種語(yǔ)言(源語(yǔ)言)翻譯成另一種語(yǔ)言(目標(biāo)語(yǔ)言)的程序,如果我們只想使用它,我們只需要將它看作一個(gè)黑盒子即可不必關(guān)心它的實(shí)現(xiàn),如圖1所示。

但是如果你想發(fā)明一種新的語(yǔ)言,你就需要了解它的內(nèi)部構(gòu)造了,因?yàn)橐l(fā)明一門(mén)新語(yǔ)言,其實(shí)你需要做的就是編寫(xiě)一個(gè)新的編譯器。實(shí)際上,編譯器將源程序翻譯成目標(biāo)程序的過(guò)程可以分為詞法分析、語(yǔ)法分析、語(yǔ)義分析以及目標(biāo)代碼生成等多個(gè)階段,如圖2所示。通常,我們稱(chēng)詞法分析、語(yǔ)法分析、語(yǔ)義分析以及中間代碼生成這幾個(gè)階段為前端,而代碼優(yōu)化以及目標(biāo)代碼生成為后端。根據(jù)使用場(chǎng)景的不同,其中有些階段不是必須的,例如一個(gè)編譯器可以沒(méi)有中間代碼以及代碼優(yōu)化。但是即便一個(gè)只包含詞法分析、語(yǔ)法分析語(yǔ)義分析的簡(jiǎn)單編譯器,如果需要從零開(kāi)始也是比較困難的,需要非常熟悉編譯原理。

由于編譯原理太過(guò)復(fù)雜,為了能讓開(kāi)發(fā)一款編譯器變得更高效,出現(xiàn)了很多編譯器框架,例如著名的LLVM。在之前的文章LLVM,一堆積木的故事中介紹過(guò),LLVM提供了所有編譯器所需的組件,我們只需要增加或者替換一些特定組件,就能實(shí)現(xiàn)一個(gè)新的編譯器。例如,只需要提供一個(gè)新的前端,你就能實(shí)現(xiàn)一個(gè)運(yùn)行在目前LLVM所支持的硬件的上的全新語(yǔ)言。
那么要怎么快速的實(shí)現(xiàn)自己的前端呢?這就是我們今天的主角——PLY——所做的事情。
2. 鋪墊
2.1. 詞法分析器
詞法分析的作用是將組成源程序的字符流識(shí)別成一個(gè)一個(gè)的記號(hào)(Token),并去除多余的空格以及注釋等,方便語(yǔ)法分析器進(jìn)行后續(xù)的語(yǔ)法分析,其工作原理如圖3所示。

例如在C語(yǔ)言中,詞法分析器會(huì)將int value = 100;這個(gè)表達(dá)式轉(zhuǎn)變?yōu)橄铝械囊粋€(gè)個(gè)記號(hào):
int (keyword), value (identifier), = (operator), 100 (constant) and ; (symbol)
2.2. 語(yǔ)法分析器
語(yǔ)法分析器——也叫解析器——的作用就是將從詞法分析器獲得的記號(hào)流與給定的一條條規(guī)則進(jìn)行比對(duì),從而檢測(cè)源程序中是否存在錯(cuò)誤,這些規(guī)則稱(chēng)為產(chǎn)生式(Production)。如果源程序沒(méi)有錯(cuò)誤,詞法分析器會(huì)輸出一個(gè)解析樹(shù),也成為抽象語(yǔ)法樹(shù)(AST)。語(yǔ)法分析的工作原理如圖4所示。

2.3. BNF
既然詞法分析器是通過(guò)產(chǎn)生式來(lái)判斷源代碼是否有錯(cuò)誤,那么我們就得先知道產(chǎn)生式是什么東西。我們知道,每一種程序設(shè)計(jì)語(yǔ)言都有其描述語(yǔ)法規(guī)則的結(jié)構(gòu),而這些描述語(yǔ)法的結(jié)構(gòu)就可以用上下文無(wú)關(guān)文法——也就是BNF范式——來(lái)描述。
BNF是由John Backus以及Peter Baur提出的,它可以用于描述上下文無(wú)關(guān)語(yǔ)言,例如可以用于描述以一個(gè)語(yǔ)言中的加減乘除操作,其形態(tài)如圖5所示。組成BNF范式的每一條規(guī)則就是一個(gè)產(chǎn)生式。

如上圖BNF范式所示,其描述了某種語(yǔ)言中的加法和乘法操作,例如在算數(shù)表達(dá)式24 * 43中,經(jīng)過(guò)詞法分析會(huì)得到24、*、43這三個(gè)記號(hào),其中24、43都是id。我們首先通過(guò)第三條產(chǎn)生式將24、43都替換成了E,得到了E * E,之后,我們發(fā)現(xiàn)產(chǎn)生式2正好可以匹配,說(shuō)明算數(shù)表達(dá)式24 * 43是沒(méi)有語(yǔ)法錯(cuò)誤的。反之,由于這個(gè)BNF范式中沒(méi)有定義減法的產(chǎn)生式,因此對(duì)于算數(shù)表達(dá)式88 - 43,最終找不到與它想匹配的產(chǎn)生式,因此就會(huì)出現(xiàn)語(yǔ)法錯(cuò)誤。
2.4.Lex & Yacc
Lex 與 Yacc是用于構(gòu)建編譯器前端的兩個(gè)工具,他們分別由Eric Schmidt 與Stephen Johnson于上世紀(jì)70年代創(chuàng)造。Lex用于詞法分析,而Yacc(Yet Another Compiler Compiler)用于語(yǔ)法分析,后續(xù)的許多解析器都是他們的變種。
而今天介紹的PLY,就是Lex以及Yacc的純Python實(shí)現(xiàn)
2.5. PLY簡(jiǎn)介
PLY,全稱(chēng)為Python Lex-Yacc,是Lex以及Yacc的純Python實(shí)現(xiàn),用于構(gòu)建編譯器的前端,Lex負(fù)責(zé)詞法分析,Yacc負(fù)責(zé)語(yǔ)法分析。他們擁有與傳統(tǒng)的Lex\Yacc一樣的功能。PLY這個(gè)庫(kù)的結(jié)構(gòu)很簡(jiǎn)單,就包含兩個(gè)重要文件lex.py 以及yacc.py。使用的時(shí)候只需要在你的工程下新建一個(gè)目錄并命名為ply然后將這兩個(gè)文件拷貝進(jìn)去,然后通過(guò)import ply.lex以及import ply.yacc這兩個(gè)語(yǔ)句導(dǎo)入就可以使用了。
3. PLY舉例
我們使用PLY的時(shí)候需要遵守一定的規(guī)則,根據(jù)需要定義一些我們需要的變量以及函數(shù)。PLY運(yùn)行的時(shí)候會(huì)通過(guò)自省的方式獲取到我們定義的變量以及參數(shù)用于進(jìn)行詞法分析以及語(yǔ)法分析。
3.1. Lex
首先我們來(lái)看看如果我們要使用Lex我們需要做些什么,我們將以下面的代碼為例子作為講解。
import ply.lex as lex
# List of token names. This is always required
tokens = (
'NUMBER',
'PLUS',
'MINUS',
'TIMES',
'DIVIDE',
'LPAREN',
'RPAREN',
)
# Regular expression rules for simple tokens
t_PLUS = r'\+'
t_MINUS = r'-'
t_TIMES = r'\*'
t_DIVIDE = r'/'
t_LPAREN = r'\('
t_RPAREN = r'\)'
# A regular expression rule with some action code
def t_NUMBER(t):
r'\d+'
t.value = int(t.value)
return t
# Define a rule so we can track line numbers
def t_newline(t):
r'\n+'
t.lexer.lineno += len(t.value)
# A string containing ignored characters (spaces and tabs)
t_ignore = ' \t'
# Error handling rule
def t_error(t):
print("Illegal character '%s'" % t.value[0])
t.lexer.skip(1)
# Build the lexer
lexer = lex.lex()
在上面的例子中,首先我們需要定義一個(gè)名叫tokens的列表,這個(gè)列表中包含了所有可能被Lex所處理的記號(hào)的名字,想要使用lex.py這個(gè)列表是必須要有的,因?yàn)長(zhǎng)ex的以及就是將輸入的源代碼轉(zhuǎn)換成一個(gè)一個(gè)記號(hào),因此你需要定義一個(gè)記號(hào)的列表告訴Lex你的源代碼都可能出現(xiàn)些什么記號(hào),yacc.py也會(huì)用到這個(gè)列表。
之后,我們需要為每一個(gè)記號(hào)的名字定義一個(gè)正則表達(dá)式,這些正則表達(dá)式的規(guī)則必須是與Python正則表達(dá)式庫(kù)re相兼容的,因?yàn)閘ex.py使用正則表達(dá)式來(lái)識(shí)別記號(hào),并且每個(gè)正則表達(dá)式的名字都是以t_開(kāi)頭,后面緊跟著其對(duì)應(yīng)的記號(hào)的名字。例如我們給加號(hào)+定義的這則表達(dá)式為t_PLUS = r'\+'。如果我們還希望識(shí)別到某些特定的記號(hào)的時(shí)候進(jìn)行一些自定義的操作,我們可以使用函數(shù)代替,例如上面例子中當(dāng)識(shí)別到數(shù)字的時(shí)候我們希望將其轉(zhuǎn)換為對(duì)應(yīng)的數(shù)值類(lèi)型,我們便將t_NUMBER = r'\d+變成了下面的樣子:
def t_NUMBER(t):
r'\d+'
t.value = int(t.value)
return t
其中函數(shù)的參數(shù)t是類(lèi)LexToken的實(shí)例,LexToken有四個(gè)常用屬性,分別是(type, value, lineno, lexpos)。函數(shù)的名字與普通的記號(hào)的遵循一樣的規(guī)則,都是以t_開(kāi)頭。函數(shù)的第一行是識(shí)別該記號(hào)的正則表達(dá)式,接下來(lái)是對(duì)識(shí)別到的記號(hào)的操作,最后需要將這個(gè)LexToken實(shí)例返回,如果該函數(shù)沒(méi)有返回值,則這個(gè)被處理的記號(hào)就會(huì)被直接丟棄。
緊接著,我們定義了一個(gè)特殊的函數(shù)t_nemline()用于記錄行數(shù)以及一個(gè)t_error()用于處理錯(cuò)誤。
最后我們執(zhí)行lexer = lex.lex()去生成一個(gè)詞法分析器。
3.2. Yacc
yacc.py是PLY中Yacc的實(shí)現(xiàn),與lex.py類(lèi)似, 我們也通過(guò)一個(gè)例子來(lái)說(shuō)明在使用yacc.py之前我們需要做的事情。使用yacc.py之前,你應(yīng)該已經(jīng)有了一個(gè)BNF范式來(lái)描述你的語(yǔ)言。
例如對(duì)于算數(shù)運(yùn)算操作,我們定義了如圖4所示BNF范式。

有了這個(gè)BNF范式之后,想要使用PLY的yacc模塊來(lái)進(jìn)行語(yǔ)法分析,所需要做的就是為每個(gè)產(chǎn)生式編寫(xiě)一個(gè)處理函數(shù)。下面的例子就是根據(jù)圖4的BNF范式寫(xiě)出的對(duì)應(yīng)的產(chǎn)生式的處理函數(shù),每個(gè)函數(shù)可以只對(duì)應(yīng)一個(gè)語(yǔ)法規(guī)則,也可以對(duì)應(yīng)同一類(lèi)型的多個(gè)語(yǔ)法規(guī)則。例如可以把下面例子中分開(kāi)的加減乘除四個(gè)產(chǎn)生式的處理函數(shù)寫(xiě)成一個(gè):
def p_expression_binop(p):
'''expression : expression '+' expression
| expression '-' expression
| expression '*' expression
| expression '/' expression
'''
if p[2] == '+' :
p[0] = p[1] + p[3]
elif p[2] == '-':
p[0] = p[1] - p[3]
elif p[2] == '*':
p[0] = p[1] * p[3]
elif p[2] == '/':
p[0] = p[1] / p[3]
import ply.yacc as yacc
# Get the token map from the lexer. This is required.
from calclex import tokens
def p_expression_plus(p):
'expression : expression PLUS term'
p[0] = p[1] + p[3]
def p_expression_minus(p):
'expression : expression MINUS term'
p[0] = p[1] - p[3]
def p_expression_term(p):
'expression : term'
p[0] = p[1]
def p_term_times(p):
'term : term TIMES factor'
p[0] = p[1] * p[3]
def p_term_div(p):
'term : term DIVIDE factor'
p[0] = p[1] / p[3]
def p_term_factor(p):
'term : factor'
p[0] = p[1]
def p_factor_num(p):
'factor : NUMBER'
p[0] = p[1]
def p_factor_expr(p):
'factor : LPAREN expression RPAREN'
p[0] = p[2]
# Error rule for syntax errors
def p_error(p):
print("Syntax error in input!")
# Build the parser
parser = yacc.yacc()
while True:
try:
s = raw_input('calc > ')
except EOFError:
break
if not s: continue
result = parser.parse(s)
print(result)
與給Lex模塊定義記號(hào)列表類(lèi),這些為產(chǎn)生式編寫(xiě)的函數(shù)也要準(zhǔn)守一定的規(guī)則:
- 函數(shù)有兩部分組成:1)docstring,對(duì)應(yīng)的是該函數(shù)所處理的產(chǎn)生式;2)函數(shù)體,代表的是這個(gè)產(chǎn)生式的語(yǔ)義;
- 每個(gè)函數(shù)有且只有一個(gè)參數(shù)
p(當(dāng)然參數(shù)的名字是任意的,如果樂(lè)意你可以叫它做狗蛋),這個(gè)p是一個(gè)數(shù)組,每個(gè)元素代表的是對(duì)應(yīng)的語(yǔ)法中的符號(hào)的值,例如圖5所示的函數(shù)處理的是expression : expression PLUS term這條語(yǔ)法規(guī)則,那么從p[0]到p[3]所對(duì)應(yīng)的值分別如圖中所示;
image.png - 函數(shù)必須以
p_開(kāi)頭,后面的名字也不重要,例如你可以吧p_expression_plus改成p_dogegg; - 函數(shù)出現(xiàn)的順序是有意義的,必須按照BNF范式定義的順序來(lái)定義處理產(chǎn)生式的函數(shù),還拿圖4的BNF范式舉例,定義處理
assign : NAME EQUALS expr的產(chǎn)生式的函數(shù)必須在處理其他產(chǎn)生的函數(shù)之前。
4. 例子
下面,我們就將上面的片段整合一下,開(kāi)發(fā)一個(gè)新的語(yǔ)言的編譯器,我們就叫它Hello World編譯器。它可以編譯任何加減乘除的數(shù)學(xué)表達(dá)式,然后執(zhí)行這個(gè)表達(dá)式,執(zhí)行的結(jié)果就是數(shù)學(xué)表達(dá)式計(jì)算得到結(jié)果是多少,就輸出多少個(gè)Hello World字符串,我們這門(mén)語(yǔ)言可能是最容易打出Hello World的語(yǔ)言了。
from utils import *
sys.path.insert(0, "../..")
tokens = (
'NAME', 'NUMBER'
)
literals = ['=', '+', '-', '*', '/', '(', ')', ',']
# Tokens
t_NAME = r'[a-zA-Z_][a-zA-Z0-9_.:]*'
def t_NUMBER(t):
r'\#?\d+'
if t.value.startswith('#'):
t.value = int(t.value[1:])
else:
t.value = int(t.value)
return t
# Get the comments and discard it, therefore there is not return statement
# Note: only inline comment are permit
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)
# Build the lexer
import ply.lex as lex
lexer = lex.lex()
# Parsing rules
precedence = (
('left', '+', '-'),
('left', '*', '/'),
('right', 'UMINUS'),
)
# dictionary of names
names = {}
alias = {}
def p_statement_assign(p):
'''statement : NAME "=" expression
'''
if p[2] == '=':
names[p[1]] = p[3]
def p_statement_expr(pppp):
'''statement : expression'''
for i in range(pppp[1] ):
print('Hello world!')
def p_expression_binop(p):
'''expression : expression '+' expression
| expression '-' expression
| expression '*' expression
| expression '/' expression
'''
if p[2] == '+' :
p[0] = p[1] + p[3]
elif p[2] == '-':
p[0] = p[1] - p[3]
elif p[2] == '*':
p[0] = p[1] * p[3]
elif p[2] == '/':
p[0] = p[1] / p[3]
def p_expression_uminus(p):
"expression : '-' expression %prec UMINUS"
p[0] = -p[2]
def p_expression_group(p):
'''expression : '(' expression ')'
| '{' expression '}' '''
p[0] = p[2]
def p_expression_list(p):
'''
expression : expression
| expression ',' expression
'''
def p_expression_number(p):
"expression : NUMBER"
p[0] = p[1]
def p_expression_name(p):
"expression : NAME"
try:
if p[1] in alias:
p[0] = names[alias[p[1]]]
else:
p[0] = names[p[1]]
except LookupError:
print("Undefined name '%s'" % p[1])
p[0] = 0
def p_error(p):
if p:
print("Syntax error at '%s'" % p.value)
else:
print("Syntax error at EOF")
import ply.yacc as yacc
parser = yacc.yacc()
while True:
try:
s = input('hello world calc > ')
except EOFError:
break
if not s:
continue
yacc.parse(s)
歡1迎2關(guān)3注4個(gè)5人6微7信8公9眾0號(hào): Tensorboy
源碼 | 原理 | 語(yǔ)言
5. References
[1] http://www.dabeaz.com/ply/ply.html#ply_nn24
[2] Compilers: Principles, Techniques and Tools: Chapter#2
