實例教程,用python實現字節碼編譯器和解釋器

2021-01-07 蟲蟲搜奇

我們都知道編譯源碼需要詞法分析、語法分析、語義分析與中間代碼產生、優化和目標代碼生成等五個過程。對於一個語言來說,有兩個最重要功能,編譯器和解釋器。實現由原始碼到字節碼的轉化,然後才能執行。本文中蟲蟲以CPython 3.6位元組碼為實例,實現一個我們自己的字節碼編譯器和解釋器,以此來熟悉基本的編譯器工作原理(),當然如果想深入理論學習,建議大家去學習了《編譯原理》這本教材。

基本定義

首先,我們介紹一些簡單的定義,以幫助區分解釋器的類型:

樹遍歷解釋器:逐節點地處理程序AST(abstract syntax tree,抽象語法樹),基於某些評估規則進行遞歸評估。對於像Add(Lit 1,Lit 2)這樣的程序,它可能會看到添加規則,遞歸地計算參數,將它們一起添加,然後將結果打包為適當的值類型。

字節碼解釋器不直接在AST上工作。他們研究預處理的AST。這簡化了執行模型,可以產生令人印象深刻的速度。像Add(Lit 1,Lit 2)這樣的程序會被編譯成以下字節碼:

PUSH 1

PUSH 2

ADD

然後解釋器將按指令進行解析,這個過程有和CPU工作處理類似。

JIT解釋器和字節碼解釋器一樣,除了編譯為特定於語言實現的字節碼,它們嘗試編譯為本機的機器指令。大多數生產級JIT解釋器會緩衝最多被調用的函數來"預熱處理",然後在後臺將它們編譯為本機代碼。所以 "熱代碼"會得到優化並且具有較小的性能損失。

樹遍歷解釋器

Lisp解釋器具有非常簡單的語義。下面是一個Lisp REPL(read-eval-print-loop)示例,其中所有命令都被解析為AST,然後立即處理輸出。

>>> 1

1

>>> '1'

1

>>> "hello"

hello

>>> (val x 3)

None

>>> x

3

>>> (set x 7)

None

>>> x

7

>>> (if True 3 4)

3

>>> (lambda (x) (+ x 1))

>>> ((lambda (x) (+ x 1)) 5)

6

>>> (define f (x) (+ x 1))

None

>>> f

>>> +

>>> (f 10)

11

>>>

類似的,我們的解釋器,首先編寫一個名為compile的函數,它接受一個由Python列表表示的表達式(類似於['+',5,['+',3,5]])並返回一個列表字節碼指令。然後我們將編寫eval,它接受這些指令並返回一個Python值(在本例中為int 13)。除了更快之外,它應該與樹遍歷解釋器的行為相同。

指令集

在我們的解釋器中,我們還需要一組指令,為了便捷我們從CPython運行時指令集架構中直接帶過來。 CPython是一個基於堆棧的架構,所以我們的解釋器架構也是如此:

LOAD_CONST

將常量推入堆棧。

STORE_NAME

將值存儲到環境中。

LOAD_NAME

從環境中讀取值。

CALL_FUNCTION

調用函數(內置或用戶定義)。

RELATIVE_JUMP_IF_TRUE

如果堆棧頂部的值為true,則跳轉。

RELATIVE_JUMP

跳轉。

MAKE_FUNCTION

從堆棧上的代碼對象創建一個函數對象,並將其推送到堆棧上。

通過上述指令元語,我們來定義整個語言。一般情況下,在指令集中都會定義數學運算用來提高速度,此處我們將其定義為內置函數,因為它快速而簡單。

Opcode枚舉 和Instruction類

我們定義了一個Opcode枚舉:

它的唯一目的是枚舉所有可能的操作碼,稍後我們會添加。Python的枚舉API有點複雜,我們不再多說,你把他想像成一個個整數列表就好了。

接著,我們編寫一個存儲操作碼和可選參數對的Instruction類:

我們為每個操作碼定義一個指令實例,將制定好參數。然後,當字節碼編譯時,我們可以通過使用給定的參數調用它們,創建每個指令的實例:

創建父節點操作碼:

STORE_NAME = Instruction(Opcode.STORE_NAME, "name")

創建實例:

[STORE_NAME("x"),STORE_NAME("y")]

定義好了編譯基礎設施,讓我們開始編譯。

整型

根據我們上面的基礎,我們開始撰寫編譯函數:

現在,它還只是一個原型,沒有啥用處。我們陸續擴展功能。

最簡單的編譯方法是整數。我先來給整數添加操作的指令,然後實現它。

LOAD_CONST,對應Python操作碼,它可以做類似的操作。我們可以調用PUSH_CONST,在我看來更好地暗示它將被推送到VM堆棧。我們指定了CONST,因為該指令只應用於文本值:5,"hello"等。值由表達式本身完全定義。

這是Instruction類的並行(在模塊範圍定義):

LOAD_CONST = Instruction(Opcode.LOAD_CONST, "const")

參數名稱"const"僅用於字節碼文檔。創建和執行該指令時,它將被實際值替換。接下來,讓我們給compile函數添加功能:

compile將遍歷表達式樹並將已編譯的子表達式中的指令列表合併在一起,因此每個分支都必須返回一個列表。當我們有嵌套的情況時,看起來會更好。我們調用測試一下:

既然我們已經有了指令,我們也應該能夠運行它。我們寫一個基本的eval循環:

pc是"程序計數器"的縮寫,相當於術語"指令指針"。我們的想法是逐個遍歷指令,並按順序執行它們。

當它無法處理特定指令時,此循環將拋出,因此它是合理的腳手架,但不多。讓我們添加一個實例來處理eval中的LOAD_CONST。

稍後我們會調用它,eval會返回堆棧頂部的值。這是我們開始。

assert eval(compile(5)) == 5

assert eval(compile(7)) == 7

現在我們在運行它:

命名

現在,我們的例程還不能做任何事情,唯一做的事是將一個數字推入堆棧的程序。下面讓我們添加一些將這些值賦值給變量的操作碼。

下面我們會添加指令STORE_NAME,它從堆棧中取出一個值並將其存儲在當前環境中,還有LOAD_NAME,它從當前環境讀取值並將其推送到堆棧中。

先來說說上面提到的環境概念,其抽象為一個Env類:

執行模型將為每個函數調用框架創建一個新的Env,並為每個閉包框架創建一個新的env,目前沒有完全實現,請暫時忽略。

我們現在關心的是全局環境。我們將創建一個用於存儲值的全局環境。然後我們將通過eval函數對其進行處理,以便我們可以使用它。先擴展commpile函數的功能:

我們新增加了第二個分支來檢查表達式是否是一個列表。由於我們現在幾乎沒有錯誤處理,我還添加了一些斷言以幫助簡化代碼。

在val的情況下,我們想要提取名稱和子表達式:編譯像5這樣的簡單值,還有(+ 1 2)類似的表達式。然後,我們要編譯子表達式並添加STORE_NAME指令。

讓我們測試一下我們能做什麼:

assert compile(['val', 'x', 5]) == [LOAD_CONST(5), STORE_NAME('x')]

現在讓我們回到eval函數:

注意到,我們添加一個env參數,以便我們可以在不同的上下文中計算表達式並獲得不同的結果。我們還為STORE_NAME添加了一個實例。執行會報錯:

為了,正常運行,我們必須修改,其他測試以傳遞一個env參數。

讓我們創建第一個環境並測試STORE_NAME。

現在我們添加編譯器和評估器功能來讀取這些存儲的值。編譯器只需檢查變量訪問,表示為字符串。

並為它添加一個測試:

assert compile('x') == [LOAD_NAME('x')]

現在我們可以生成LOAD_NAME了,讓我們為它添加eval支持。

為了測試它,我們首先手動將名稱存儲到環境中,然後查看LOAD_NAME是否可以將其讀回。

內置函數

我們還可以按需添加函數操作碼,或者添加一個操作碼,允許我們調用本機(Python)代碼並以這種方式擴展我們的解釋器。

在該實例中,我們將添加CALL_FUNCTION操作碼,它將用於內置函數和用戶定義函數。我們稍後會介紹用戶定義的函數。

當表達式的形式為(x0 x1 x2 ...)並且x0不是像val這樣的預設識別名稱之一時,將生成CALL_FUNCTION。編譯器應生成代碼以首先加載函數,然後將參數加載到堆棧中。然後CALL_FUNCTION指令。這與CPython的實現非常相似。

CALL_FUNCTION指令的定義如下:

CALL_FUNCTION = Instruction(Opcode.CALL_FUNCTION, "nargs")

CALL_FUNCTION指令帶有傳遞給函數的參數個數,以便我們可以正確調用它。請注意,我們這樣做而沒有在函數中存儲正確數量的參數,主要是這樣函數就可以可以自持可變數量的參數。

我們擴充complie函數增加調用表達式功能。

我為列表表達式添加了一個默認情況。看到它編譯名稱,然後編譯參數,然後發出一個CALL_FUNCTION。我們添加一些測試實例:

現在讓我們同步擴充eval函數的功能:

請注意,我們正在從堆棧中讀取參數是以相反的順序從堆棧中讀取的。這是堆棧的LIFO特性(先入後出)。

我們需要檢查fn是否可調用。由於我們在堆棧上允許原始Python對象,因此我們必須確保我們實際上要調用Python函數。然後我們將使用參數列表調用該函數並將其結果推送到堆棧上。

以下是現實生活中的測試結果:

如果我們將lambda表達式填充到環境中,我們將獲得這些超級簡單的內置函數。但這不是最優+功能。讓我們製作一個可變的變量。

因為我們將參數傳遞給列表,所以我們可以做各種類似的操作。

只提到過,我們在STORE_NAME中添加一個用於編譯嵌套表達式的測試。

你還可以隨心所以添加自己需要的內置函數,比如添加print功能:

應該能看到以正確順序傳遞的參數。注意,如果使用的是Python 2,則應將print打包在lambda中,在Python 2 語法中print不是函數。

條件語句

如果沒有條件,一個語言就做不了啥事。雖然我們可以將條件作為內置函數。但是我們在這選擇更正常的傳統條件語法:

(if a b c)

會被編譯為

我們還將採用CPython方法生成相對跳轉JUMP。

為此,我們需要新添加兩個操作碼和說明:

RELATIVE_JUMP_IF_TRUE = Instruction(Opcode.RELATIVE_JUMP_IF_TRUE, "off")

RELATIVE_JUMP = Instruction(Opcode.RELATIVE_JUMP, "off")

它們中的每一個都採用偏移量來表示跳轉的距離。

我們的編譯器函數需要新增加:

首先編譯條件。然後,編譯將在條件通過時執行的分支。 if-false分支有點棘手,那裡包含了跳轉到結尾。這是為了跳轉到if-true分支的偏移計算是正確。

同樣我們添加一些測試來檢查我們的工作:

我們特意添加了第二個測試來仔細檢查嵌套表達式是否正常工作。

增加eval函數的功能:

對應的測試案例:

自定義函數

用戶定義的函數對於一個語言是否真正可以用很關鍵。我們一個簡單的Function對象。

params將是一個命名元組,body是一個指令元組,env是一個環境。

所有函數都是一個閉包。可以引用定義函數的範圍中定義的變量。基本格式如下:

((lambda (x) (+ x 1)) 5)

或者為:

(define inc (x) (+ x 1))

(inc 5)

實際上,我們將使用語法轉換來根據val和lambda重寫定義。

要實現這些功能,我們需要新增加操作碼:MAKE_FUNCTION。該指令會將在堆棧中的一些對象轉換為Function對象。

MAKE_FUNCTION = Instruction(Opcode.MAKE_FUNCTION, "nargs")

這需要一個整數,即函數所期望的參數數量。現在我們只允許位置,非可選參數。如果我們想要有額外的調用約定,我們必須稍後添加它們。

來給complie增加功能:

對於lambda來說,它非常簡單。push參數,push body代碼,然後就可以用。

define有點小技巧。他是一個宏並在編譯之前重寫AST。如果我們想要更專業,需要新建一個宏系統,以便標準庫可以定義define和if ...這超出本文範圍。

測試代碼:

為了讓功能真正可用,我們給eval函數增加處理MAKE_FUNCTION操作碼。

與調用函數一樣,我們按照push順序的相反方式讀取所有內容。首先是body,然後是參數,檢查他的長度,然後push新的Function對象。

但是函數在沒有可調用的情況下並不是特別有用。為了讓其調用實現功能:

第一個策略是簡單的策略,即在CALL_FUNCTION中添加另一個處理Function對象的案例。這大多數程式語言中都是這樣的:

另一種方法更像是Pythonic。它將Function轉換為可調用對象,將自定義設置代碼放入Function本身。如果選擇這樣做,請單獨保留CALL_FUNCTION並以這種方式修改Function:

然後eval應該調用函數,就像它是一個普通的Python函數一樣。

測試用例:

最後,我們寫一個遞歸函數實例:

表達式列表

編寫一個可以獲取表達式列表,編譯表達式並加函數compile_program就更好。所以我們還要添加一個begin關鍵字。

編譯函數增加對應功能:

對應測試用例:

本文中,我們使用Python實例編寫了一個字節碼編譯和解釋器,用以說明相應的編譯原理,和開頭提到的一樣,基本原理適用於任何語言,你可以使用你最那首的語言Perl,golang,rust,js甚至是R來實現自己的例子。

相關焦點

  • 11 個優秀的 Python 編譯器和解釋器
    然後編譯內容被轉換為字節碼。通過機器和作業系統進一步擴展到 Python 虛擬機。本文重點介紹了適用於 Python 程式設計師的 11 種優秀的 Python 編譯器和解釋器。很好的 Python 編譯器和解釋器1.Brython
  • Python 動態編譯器PyPy比其他動態編譯器的優點所在
    Python 動態編譯器PyPy比其他動態編譯器的優點所在 PyPy是Python 語言的動態編譯器,在實際的應用中它要比C實現的Python的實際操作步驟更為簡捷,以下是文章的相關內容的描述。
  • 一篇文章全面吃透Java虛擬機,線程安全的實現方法
    內存空間小,線程私有.它可以看做是當前線程所執行的字節碼的行號指示器.也就是說,線程主要是執行任務,而執行到哪裡,需要使用程序計數器來記錄.字節碼解釋器工作是就是通過改變這個計數器的值來選取下一條需要執行指令的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴計數器完成.
  • 都有Python 了,還要什麼編譯器!
    編譯的目的是將源碼轉化為機器可識別的可執行程序,在早期,每次編譯都需要重新構建所有東西,後來人們意識到可以讓編譯器自動完成一些工作,從而提升編譯效率。但「編譯器不過是用於代碼生成的軟機器,你可以使用你想要的任何語言來生成代碼」,真的是必要的嗎?
  • 一、python編輯器使用基礎之hello world
    實驗目的:1.學習最簡單的python代碼編輯方式2.直觀了解python語言的高效實驗環境:已安裝python3.5並添加環境變量函數詳解:print()函數的原型詳解以及實例應用1、利用python創建腳本文件在【開始】菜單中找到python3.5,點擊打開python的編輯器:在打開窗口點擊
  • Python和C語言的語法有什麼不同?
    Python和C語言的語法有什麼不同? python與C的區別如下: 1、語言類型:Python是一種基於解釋器的語言,會逐行讀取代碼,將Python編譯為字節碼,由大型C程序解釋;C是一種編譯語言,完整的原始碼將直接編譯為機器代碼,由CPU直接執行。
  • 將Notepad++配置為Python編譯器(方法二)
    將Notepad++配置成為腳本編譯器安裝Python, 安裝最新版本的Notepad++安裝NppExec插件;a.打開Notepad++(編輯任何一個txt文檔即可),b.<PYTHON_HOME> : 表示安裝在你本機的python的路徑,一定要帶python.exe,例如我的路徑:C:\ Python27\python.exe2. 如果給python添加了環境變量的,可以直接使用python即可.使用<PYTHON_HOM>的方便在於,如果你安裝了好幾個版本的python,那麼可以通過不同的路徑來編譯不同版本的腳本, b.
  • python與c語言的語法有哪些不一樣的
    在眾多程式語言之中,想必很多人都聽說過Python和C語言,在進行編程學習之前,大家都會問:python和c語言的區別有哪些?我該如何選擇?接下來我們來看看吧。python與C的區別如下:1、語言類型:Python是一種基於解釋器的語言,會逐行讀取代碼,將Python編譯為字節碼,由大型C程序解釋;C是一種編譯語言,完整的原始碼將直接編譯為機器代碼,由CPU直接執行。
  • Python基礎教程(一) - 快速入門
    從今天開始學習python,會將學習到的相關知識整理到這裡。今後的所有內容都基於Ubuntu系統中進行的,和其他語言一樣,讓我們先來"Hello World!"吧。Hello world!#!/usr/bin/python為Linux系統下Python解釋器的路徑,通常python解釋器的路徑安裝在/usr/local/bin或/usr/bin目錄下。程序輸入和raw_input()內建函數從用戶得到數據輸入的最好方式使用raw_input()函數,它讀取標準輸入,並將讀取到的數據賦值給指定的變量。
  • 出國必備,用python實現美元和人民幣的實時匯率兌換
    各個國家的流通貨幣是不同的,而當我們要出國時,就需要先算好貨幣之間的兌換,而羽憶教程下面為你介紹用python實現美元和人民幣之間的實時匯率兌換。python美元和人民幣匯率兌換python美元和人民幣匯率兌換匯率兌換是一個十分簡單的python程序,只需要知道其兌換的比例就可以輕鬆得出結果
  • Python的爬蟲基礎知識及安裝
    ,因為Python本身的意思也是蟒蛇的意思誕生和發展1991年,第一個Python編譯器(同時也是解釋器)誕生。它是用C語言實現的,並能夠調用C庫(.so文件)。從一出生,Python已經具有了︰類,函數,異常處理,包含表和詞典在內的核心數據類型,以及模塊為基礎的拓展系統。
  • 如何入門Python之Python基礎教程詳解
    這裡整理了一些個人經驗和Python入門教程供大家參考。如果你是零基礎入門 Python 的話,建議初學者至少達到兩個目標: 會用,理解。會用通過 Python 入門教程,學習 Python 的語法,熟悉 Python 標準庫的使用。目前 Python 官方已經發布了中文版的官方教程,降低了學習 Python 的門檻。
  • Python基礎進階之海量表情包多線程爬蟲功能的實現
    這篇文章主要介紹了Python基礎進階之海量表情包多線程爬蟲,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑑價值,需要的朋友可以參考下一、前言在我們日常聊天的過程中會使用大量的表情包
  • 0基礎入門Python學習步驟如何安排?
    所以既然你決定了要學習python,那麼就需要先下一個決心,至少決定要做為自己的主力語言。 python是全能語言,社區龐大,有太多的庫和框架。你只需要找到合適的工具來實現想法,省去了造輪子的精力。 coder可以寫儘可能少的代碼來實現同等的功能。「人生苦短,我用python」是至理名言。
  • 使用Visual Studio 和 python 設置自己的數據科學工作區
    關於這個問題,荷蘭數據分析師 Christiaan Dollen 近日發表了一篇博文,在文中他分享了用 Visual Studio(VS)和 python 設置自己的數據科學工作區的經驗,這些經驗或許可以幫助你避免這些問題。雷鋒網 AI 開發者將全文編輯如下:如果你剛開始從事數據科學領域,那麼創建一個個人工作空間將幫助你保持項目的有序性。有很多不同的工具可以使用。
  • 理解 Python 的 for 循環
    我們將從一組基本例子和它的語法開始,還將討論與 for 循環關聯的 else 代碼塊的用處。然後我們將介紹迭代對象、迭代器和迭代器協議,還會學習如何創建自己的迭代對象和迭代器。之後,我們將討論如何使用迭代對象和迭代器實現 for 循環,以及利用 while 循環通過迭代器協議實現 for 循環邏輯。
  • 世界上第一個C語言編譯器是怎麼編寫的?它為什麼能夠用C語言編寫?
    這些操作,C語言都是可以實現的。 所以用C語言來做C語言的編譯器是完全可行的。 但是,歷史上的第一個C語言編譯器,肯定不是C語言寫的,因為在沒有編譯器時,無法把C語言轉換成可執行文件。只要有了第一版其它語言的編譯器,就可以用C語言寫編譯器了。
  • 一周學全Python面試基礎(2)
    通過列出30個python面試問題和答案,本文涵蓋在Python面試中經常問到的問題。如果您是該行業的新手,本基礎篇將極大地幫助您。我們衷心希望這篇文章在準備面試時會有所幫助。Python的需求量很大,必須與成千上萬擁有與python技能的申請人競爭,才能在就業市場中找到工作。
  • python字符串前加r、f、u、l 的區別
    (目前支持python3.6版本) 下面看下f-strings的使用方法 基本使用(作用:替換值) 建議所有編碼方式採用utf8 字符串前加 l 表示寬字符,unicode字符( unicode字符集是兩個字節組成的。L告示編譯器使用兩個字節的 unicode 字符集) 如 L"我的字符串" 表示將ANSI字符串轉換成unicode的字符串,就是每個字符佔用兩個字節。