2015讀書進度-《自制程式語言》[漆楚衡]

漆楚衡發表於2015-04-10

筆記:《自制程式語言》

這本書在我這已經沉睡了快一年了。一直以自己能力不夠為藉口。不過這個學期開了編譯原理課程,想著順帶著就把這本書看看。

現在編譯原理也上了一個月了,各方面也都瞭解了一些,發現這本書其實跟編譯原理的教科書的側重點完全不一樣,相互之間算是互補。

教科書主要教了一些演算法和概念,而這本書的主要路線就是實戰,兩門語言,涵蓋了目前的主流語言的諸多特性,都是通過實現來講。

基於lex/yacc的語言開發在本書中所表現出來的路線是

  1. 詞法分析,此處lex將一個正規表示式對映為一個語句塊,可以直接在語句塊裡對token實現解釋。
  2. 語法分析,此處直接利用上一步驟的token串,根據語法定義(同lex,此處有產生式到語句塊的對映)生成相應的語法分析樹。
  3. 值得注意的是上面兩步中的token和語法法分析樹中的非終結符似乎都是有型別的。這一塊型別對映沒有深究。
  4. 接下來
    • crowbar的語法樹生成後就可以開始解釋執行了。
    • Diskam則有語法樹修正,編譯為虛擬機器位元組碼,執行等過程。

這是利用到了工具(lex/yacc)的部分,實際上,這只是整個編譯工作中的一小部分 ,lex/yacc的作用其實十分有限。

現在稱所設計的語言為目標語言,而實現目標語言的語言為底層語言

設計語言(的執行系統)的主要工作是設計底層語言對目標語言中各種結構(值,表示式,控制結構)的表示,以及如何將這些結構轉化為底層語言的計算過程。

lex/yacc所完成的只不過是自動化的結構搭建工作。


學習 Diksam

從crowbar到Diksam,難度跨度有點大,而作者對內容的講述卻越來越簡練。

不過也不能怪作者,這本書進行到Diksam,在很大程度上要靠讀者自己去讀原始碼,而我現在耐心不足,貪心有餘。

昨天耐下心來,將前半本書又看了一遍,熟悉作者在Diksam部分幾乎不再提的那些基礎部分。

Diksam0.1

這裡一切還都不難理解(吹牛了,看了半天才看懂)。

比較有難度的有

  • 派生型別:這是處理陣列和函式的型別的機制,乍看一眼難倒我了。
  • 函式呼叫機制:這一部分感覺作者說了幾次都沒有說透。

這一部分的主要流程

  1. 分析原始碼,生成語法樹
  2. 修正語法樹,在應該插入型別轉換的地方插入型別轉換,表示式的型別資訊在此時新增
  3. 生成位元組碼,此處產生的是一個DVM_Executable
  4. 將DVM_Executable繫結到已給DVM_VirtualMachine
  5. 執行

值得注意的是此處有三個在後來十分重要的結構(以下列出的只是主要成員):

  • DVM_Compiler:分類儲存分析結果
    • 變數宣告的集合
    • 函式定義的集合
    • 頂層語句序列
  • DVM_Executable:儲存Compiler的編譯結果(位元組碼),一個檔案對應一個Executable
    • 常量池
    • 全域性變數(只是形式)
    • 函式集合(包括位元組碼)
    • 頂層語句的位元組碼
  • DVM_VirtualMachine:Executable儲存的是“程式”(位元組碼等)的不變部分,要執行一個“程式”,還要給它提供一套可變部分的系統
    • 全域性變數的實際儲存空間
    • 執行時棧
    • 函式登記表

這裡最繞的部分就是函式集合(DVM_Function *function)和函式登記表(Function *function)了。

在生成位元組碼的時候,push_function的引數是DVM_Function表的引數,DVM_Function存放了檔案中出現的所有函式(本地宣告的和原生函式)。

掛載到VirtualMachine後,會用Function的對應索引替換DVM_Function索引,在Function中實現對原生函式的登記。

所以Function的內容有兩種

  1. 指回DVM_Function
  2. 指向原生函式

這幾個結構的成員的功能一定要記好, 它們三個的成員之間的相互聯絡(特別是與函式有關的部分)也一定要記好,否則後面的連結機制很難看懂。

Diksam0.2

這裡沒遇見特別難以理解的部分,

Diksam0.3 require

我覺得這一部分可以分成兩部分,因為包引入機制是我認為這本書裡最難理解的部分,這裡沒有細講,讓對整體認識把握不足的讀者(我)完全摸不著頭腦。

這一部分的困難在於

  1. 相對於0.1版,各結構的功能有變化
  2. 各結構的內容有變化
  3. 所有這些變化都是相關聯的

VM(VirtualMachine)和Exe(Executable)的關係

0.1版中VM和Exe是一一對應的,VM管理了Exe所需的“記憶體”(棧,堆,靜態區)

現在VM和多個Exe對應了,多個Exe公用一個棧,一個堆。

不過Exe們要各自私有一個靜態區。所以通過一個ExecutableEntry結構將靜態區附加到Exe上, 而VM的管理單位從Exe變成了ExecutableEntry。

函式,函式登記表,函式關聯

對於Exe中的DVM_Function,它有兩個作用

  1. 記錄本地函式的內容(位元組碼等),有一個is_implemented的欄位,為真時表示當前項具有這一功能。
  2. 記錄本檔案對所有函式的使用,原因是當要生成位元組碼的時候(此時還未載入到VM,對外界一無所知),我們依然要給每一個被呼叫的函式分配編號。

對於VM中的Function

  1. 全域性的函式登記:所有VM中被使用的Exe的函式都會登記在此(原生函式也登記在此)。
  2. 動態載入:這個功能要求登記表中的具體連結可以為空,所有一個is_implemented欄位。
  3. 因為動態載入是按函式名稱來的,所以Function要儲存函式的名稱。

核心問題是多個檔案的函式如何關聯彼此,這一部分我感覺極其雜亂,還未能完全理解。

大體思路是從一個檔案開始,會遞迴編譯檔案和它的require(.dkh),組成一組Exe,將這一組Exe逐個加入VM,就可以執行了,

執行時會發生dkh要求載入dkm,發生動態載入,動態載入就是跟據需要編譯對應dkm,然後重新整理Function。

應用篇

異常處理

實現異常的核心(與其他控制結構,比如if-else的不同)就是對棧的非常規操作,所以如果是自己完全實現了目標語言的棧,則比較方便的寫出異常。

而crowbar中的直譯器依賴於宿主語言C的遞迴執行,所以之前在實現基本的crowbar時break等流程跳轉需要一些輔助的控制(返回執行狀態標誌)。

到了要實現異常了,作者祭出了殺手鐗:setjump/long_jmp,這兩個函式雖然是標準的,不過我之前也從來沒見過有誰用它們。這次也算是開眼了。


總結

首先吐槽一下這本書的翻譯,感覺用詞跟我之前習慣的用詞不太一樣,不知道是不是日文原文就是這樣。

這本書在crowbar部分還是很耐心的講解各個部分的,到了diksam部分感覺作者是假定讀者以原始碼閱讀為主,當你不懂得時候,來看書,提點一下。

幾點收穫:

  1. 先有宿主語言,在宿主語言中實現目標語言的控制結構和基本型別。

  2. 表示式與語句的不同

    • 邏輯目標:表示式是與運算相關的,而語句是與流程控制相關的
    • 函式呼叫是表示式,不是顯式的流程控制對應
    • 層次關係:表示式可以構成語句,而語句不能構成表示式
  3. 型別是一個語言的高層邏輯,(這個不太好表達),型別不是執行的特徵,而是面向編譯器的輔助檢查手段。

  4. 異常是一種控制結構(跟if-else同級),特點是具有對棧的讀取能力。

  5. 虛擬機器提供的是面向目標語言的最小基本操作集

    • 對各種基本型別的操作
    • 棧模型
    • 幾個“記憶體空間”,棧,堆,靜態區(全域性變數)之間的相互傳遞,
    • 流程控制:順序,goto
  6. 最痛苦的就是Diksam中包引入的部分。說實話我現在也沒有特別透徹的理解,只是把幾個影響閱讀的地方搞懂了。

    • 個人認為這裡的幾個核心結構(DVM_Function, Function, DVM_Exectuable)都承擔了多重功能,之間的關係和通訊手段感覺比較雜亂。
    • 不過收穫也蠻大的,之前都沒有思考過多個檔案的整合到底要怎麼來,瞭解了作者的一個VM對應多個Exe(共用一個棧,堆)之後感覺這樣挺好。
  7. 類的處理其實比較簡單,屬性就是一組變數,而函式不過是普通函式附加一個this引數。

  8. 類相關的概念中比較複雜的是(介面)多繼承。不過這也只是需要一個比較強大的上/下轉型時虛表替換而已。

教訓:要提升自己的程式碼閱讀能力

相關文章