如何使用Python開發自己的編譯器

1. 前言

總所周知,編譯器是一個將一種語言(源語言)翻譯成另一種語言(目標語言)的程序,如果我們只想使用它,我們只需要將它看作一個黑盒子即可不必關心它的實現,如圖1所示。


圖1 編譯器

但是如果你想發明一種新的語言,你就需要了解它的內部構造了,因為要發明一門新語言,其實你需要做的就是編寫一個新的編譯器。實際上,編譯器將源程序翻譯成目標程序的過程可以分為詞法分析、語法分析、語義分析以及目標代碼生成等多個階段,如圖2所示。通常,我們稱詞法分析、語法分析、語義分析以及中間代碼生成這幾個階段為前端,而代碼優化以及目標代碼生成為后端。根據使用場景的不同,其中有些階段不是必須的,例如一個編譯器可以沒有中間代碼以及代碼優化。但是即便一個只包含詞法分析、語法分析語義分析的簡單編譯器,如果需要從零開始也是比較困難的,需要非常熟悉編譯原理。


圖2 編譯器編譯過程

由于編譯原理太過復雜,為了能讓開發一款編譯器變得更高效,出現了很多編譯器框架,例如著名的LLVM。在之前的文章LLVM,一堆積木的故事中介紹過,LLVM提供了所有編譯器所需的組件,我們只需要增加或者替換一些特定組件,就能實現一個新的編譯器。例如,只需要提供一個新的前端,你就能實現一個運行在目前LLVM所支持的硬件的上的全新語言。

那么要怎么快速的實現自己的前端呢?這就是我們今天的主角——PLY——所做的事情。

2. 鋪墊

2.1. 詞法分析器

詞法分析的作用是將組成源程序的字符流識別成一個一個的記號(Token),并去除多余的空格以及注釋等,方便語法分析器進行后續的語法分析,其工作原理如圖3所示。


圖3 詞法分析工作原理

例如在C語言中,詞法分析器會將int value = 100;這個表達式轉變為下列的一個個記號:

int (keyword), value (identifier), = (operator), 100 (constant) and ; (symbol)

2.2. 語法分析器

語法分析器——也叫解析器——的作用就是將從詞法分析器獲得的記號流與給定的一條條規則進行比對,從而檢測源程序中是否存在錯誤,這些規則稱為產生式(Production)。如果源程序沒有錯誤,詞法分析器會輸出一個解析樹,也成為抽象語法樹(AST)。語法分析的工作原理如圖4所示。


圖4 語法分析器工作原理

2.3. BNF

既然詞法分析器是通過產生式來判斷源代碼是否有錯誤,那么我們就得先知道產生式是什么東西。我們知道,每一種程序設計語言都有其描述語法規則的結構,而這些描述語法的結構就可以用上下文無關文法——也就是BNF范式——來描述。

BNF是由John Backus以及Peter Baur提出的,它可以用于描述上下文無關語言,例如可以用于描述以一個語言中的加減乘除操作,其形態如圖5所示。組成BNF范式的每一條規則就是一個產生式。

圖3 BNF示例

如上圖BNF范式所示,其描述了某種語言中的加法和乘法操作,例如在算數表達式24 * 43中,經過詞法分析會得到24*43這三個記號,其中2443都是id。我們首先通過第三條產生式將2443都替換成了E,得到了E * E,之后,我們發現產生式2正好可以匹配,說明算數表達式24 * 43是沒有語法錯誤的。反之,由于這個BNF范式中沒有定義減法的產生式,因此對于算數表達式88 - 43,最終找不到與它想匹配的產生式,因此就會出現語法錯誤。

2.4.Lex & Yacc

Lex 與 Yacc是用于構建編譯器前端的兩個工具,他們分別由Eric Schmidt 與Stephen Johnson于上世紀70年代創造。Lex用于詞法分析,而Yacc(Yet Another Compiler Compiler)用于語法分析,后續的許多解析器都是他們的變種。
而今天介紹的PLY,就是Lex以及Yacc的純Python實現

2.5. PLY簡介

PLY,全稱為Python Lex-Yacc,是Lex以及Yacc的純Python實現,用于構建編譯器的前端,Lex負責詞法分析,Yacc負責語法分析。他們擁有與傳統的Lex\Yacc一樣的功能。PLY這個庫的結構很簡單,就包含兩個重要文件lex.py 以及yacc.py。使用的時候只需要在你的工程下新建一個目錄并命名為ply然后將這兩個文件拷貝進去,然后通過import ply.lex以及import ply.yacc這兩個語句導入就可以使用了。

3. PLY舉例

我們使用PLY的時候需要遵守一定的規則,根據需要定義一些我們需要的變量以及函數。PLY運行的時候會通過自省的方式獲取到我們定義的變量以及參數用于進行詞法分析以及語法分析。

3.1. Lex

首先我們來看看如果我們要使用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()
 

在上面的例子中,首先我們需要定義一個名叫tokens的列表,這個列表中包含了所有可能被Lex所處理的記號的名字,想要使用lex.py這個列表是必須要有的,因為Lex的以及就是將輸入的源代碼轉換成一個一個記號,因此你需要定義一個記號的列表告訴Lex你的源代碼都可能出現些什么記號,yacc.py也會用到這個列表。

之后,我們需要為每一個記號的名字定義一個正則表達式,這些正則表達式的規則必須是與Python正則表達式庫re相兼容的,因為lex.py使用正則表達式來識別記號,并且每個正則表達式的名字都是以t_開頭,后面緊跟著其對應的記號的名字。例如我們給加號+定義的這則表達式為t_PLUS = r'\+'。如果我們還希望識別到某些特定的記號的時候進行一些自定義的操作,我們可以使用函數代替,例如上面例子中當識別到數字的時候我們希望將其轉換為對應的數值類型,我們便將t_NUMBER = r'\d+變成了下面的樣子:

 def t_NUMBER(t):
     r'\d+'
     t.value = int(t.value)    
     return t

其中函數的參數t是類LexToken的實例,LexToken有四個常用屬性,分別是(type, value, lineno, lexpos)。函數的名字與普通的記號的遵循一樣的規則,都是以t_開頭。函數的第一行是識別該記號的正則表達式,接下來是對識別到的記號的操作,最后需要將這個LexToken實例返回,如果該函數沒有返回值,則這個被處理的記號就會被直接丟棄。

緊接著,我們定義了一個特殊的函數t_nemline()用于記錄行數以及一個t_error()用于處理錯誤。

最后我們執行lexer = lex.lex()去生成一個詞法分析器。

3.2. Yacc

yacc.py是PLY中Yacc的實現,與lex.py類似, 我們也通過一個例子來說明在使用yacc.py之前我們需要做的事情。使用yacc.py之前,你應該已經有了一個BNF范式來描述你的語言。
例如對于算數運算操作,我們定義了如圖4所示BNF范式。


圖4 加減乘除BNF范式

有了這個BNF范式之后,想要使用PLY的yacc模塊來進行語法分析,所需要做的就是為每個產生式編寫一個處理函數。下面的例子就是根據圖4的BNF范式寫出的對應的產生式的處理函數,每個函數可以只對應一個語法規則,也可以對應同一類型的多個語法規則。例如可以把下面例子中分開的加減乘除四個產生式的處理函數寫成一個:

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模塊定義記號列表類,這些為產生式編寫的函數也要準守一定的規則:

  1. 函數有兩部分組成:1)docstring,對應的是該函數所處理的產生式;2)函數體,代表的是這個產生式的語義;
  2. 每個函數有且只有一個參數p(當然參數的名字是任意的,如果樂意你可以叫它做狗蛋),這個p是一個數組,每個元素代表的是對應的語法中的符號的值,例如圖5所示的函數處理的是expression : expression PLUS term這條語法規則,那么從p[0]p[3]所對應的值分別如圖中所示;
    image.png
  3. 函數必須以p_開頭,后面的名字也不重要,例如你可以吧p_expression_plus改成p_dogegg
  4. 函數出現的順序是有意義的,必須按照BNF范式定義的順序來定義處理產生式的函數,還拿圖4的BNF范式舉例,定義處理assign : NAME EQUALS expr的產生式的函數必須在處理其他產生的函數之前。

4. 例子

下面,我們就將上面的片段整合一下,開發一個新的語言的編譯器,我們就叫它Hello World編譯器。它可以編譯任何加減乘除的數學表達式,然后執行這個表達式,執行的結果就是數學表達式計算得到結果是多少,就輸出多少個Hello World字符串,我們這門語言可能是最容易打出Hello World的語言了。

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關3注4個5人6微7信8公9眾0號: Tensorboy
源碼 | 原理 | 語言

5. References

[1] http://www.dabeaz.com/ply/ply.html#ply_nn24
[2] Compilers: Principles, Techniques and Tools: Chapter#2

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。