淺析Python解釋器的設計(二)

從現代編譯器的角度看,解釋器和編譯器的邊界已經相當的模糊。我們後面的討論說到的編譯器就是Python的解釋器,沒有特別說明的指的是CPython的實現。

內存管理(Memory Management )

在討論編譯器的具體實現之前,我們不得不了解一下在這裡面內存是如何井然有序地被分配的。為了讓內存分配簡單一些,一般我們都會建立一個Arena(不知道用中文怎麼準確的表達)來管理內存。有了Arena我們就可以把內存集中在一起更容易地進行分配和銷毀。在這裡面沒有了真正的內存的釋放,也就是說內存的釋放不會顯式地對應系統的free(3)調用。也正是由於編譯器每部每次內存的分配對於Arena都只是一次註冊,釋放編譯器用到的所有內存也變得非常的簡單:僅僅需要一次free(3)系統調用就可以達成。

一般來說,除非你是在研究或者開發編譯器最核心的部分,才需要關注內存分配的問題。Python解釋器里所有內存分配相關的代碼都在Include/pyarena.hn和 Python/pyarena.cn可以找到。

BTW,這裡有一個我很久以前實現的一個定長內存池(Fixed Sized Pool)的源碼,代碼量很少,希望可以有助於大家了解內存池的概念:github.com/auxten/gkoAl

PyArena_New()函數會幫我們創建一個新的Arena,並返回一個PyArenan結構體,這個結構體將會保存所有分配給此Arena的內存的指針。在編譯器完成工作釋放內存退出之前,都是由Arena負責對所有用到的內存進行「記賬」。釋放內存對應的API是PyArena_Free()。基本上,每個編譯器只需要在最後的「戰略大撤退」時調用它一次即可。

綜上所述,內存池的設計初衷就是讓我們在進行編譯器的各項工作的時候不用去關心內存分配的各種細節。因此,我們研究編譯器的絕大多數時候也可以先跳過這部分的內容。n

唯一的一個例外就是在管理PyObject的時候。由於Python使用引用計數的方式進行內存垃圾回收,所以在Arena上增加了一些機制對它進行額外的支持,以便垃圾回收的時候能正確的處理每一個PyObject。雖然這種例外是很少見的,但是如果你在編譯器的代碼里分配一個PyObject,你必須通過調用PyArena_AddPyObject()來通知Arena增加相應的引用計數。

從語法樹到抽象語法樹(AST)

通過調用Python源碼中的PyAST_FromNode()函數,語法樹(參見Python/ast.c)被轉換成抽象語法樹(AST)。

mod_tynPyAST_FromNode(const node *n, PyCompilerFlags *flags, const char *filename_str,n PyArena *arena)n{n mod_ty mod;n PyObject *filename;n filename = PyUnicode_DecodeFSDefault(filename_str);n if (filename == NULL)n return NULL;n mod = PyAST_FromNodeObject(n, flags, filename, arena);n Py_DECREF(filename);n return mod;nn}n

PyAST_FromNode()函數對語法樹進行樹形遍歷,在遍歷的過程中動態創建出各種類型的AST節點。簡單來說,就是遍歷每一個節點,為每一個語法節點調用相對應的AST節點的構造函數,以及相關的支持函數,按照節點的組織方式對他們進行「連接」。

為了繼續下面的內容,我們要簡單介紹一下yacc,簡單來說,yacc就是編譯器的編譯器:

yacc(Yet Another Compiler Compiler),是Unix/Linux上一個用來生成編譯器的編譯器(編譯器代碼生成器)。yacc生成的編譯器主要是用C語言寫成的語法解析器(Parser),需要與詞法解析器Lex一起使用,再把兩部分產生出來的C程序一併編譯。yacc本來只在Unix系統上才有,但現時已普遍移植往Windows及其他平台。

yacc的輸入是巴科斯範式(BNF)表達的語法規則以及語法規約的處理代碼,Yacc輸出的是基於表驅動的編譯器,包含輸入的語法規約的處理代碼部分。

yacc是開發編譯器的一個有用的工具,採用LALR(1)語法分析方法。

yacc最初由AT&T的Steven C. Johnson為Unix操作系統開發,後來一些兼容的程序如Berkeley Yacc,GNU bison,MKS yacc和Abraxasnyacc陸續出現。它們都在原先基礎上做了少許改進或者增加,但是基本概念是相同的。

由於所產生的解析器需要詞法分析器配合,因此Yacc經常和詞法分析器的產生器——一般就是Lex——聯合使用。IEEE POSIX P1003.2標準定義了Lex和Yacc的功能和需求。

——摘自Wikipedia

需要格外注意的是,在語法規範和解析樹中的節點之間沒有自動化或符號連接。這點上Python解釋器和yacc不同。

例如,就像我們處理「if」這個語法結構時,必須關注「:」這個token出現的位置,來決定「if」後面的條件語句的結束位置一樣。跟蹤我們正在使用解析樹中哪個節點是十分必要的。

調用從解析樹生成AST節點的函數都被命名為ast_for_xx,其中xx是函數處理的語法規則(但是,alias_for_import_name這個函數是個例外)。這些ast_for_xx函數會依次調用由ASDL語法定義的構造函數以及包含在Python/Python-ast.c(由Parser/asdl_c.py生成)的構造函數以創建AST的節點。這樣下來,所有的AST節點就全部存儲在asdl_seq結構體中了。

創建和使用asdl_seq *類型的函數和宏都在 Python/asdl.c 和 Include/asdl.h中:

  • asdl_seq *_Py_asdl_seq_new(Py_ssize_t size, PyArena *arena);
    • 在arena上分配size個長度的asdl_seq
  • asdl_seq_GET(S, I)
    • 獲取S(類型是asdl_seq*)的I位置的元素
  • asdl_seq_SET(S, I, V)
    • 把S的指定位置I上的元素置為V
  • asdl_seq_LEN(S)
    • 返回S的長度

如果你正在處理Python的代碼語句,代碼語句的行號是一個繞不開的問題。目前來說,行號作為最後一個參數傳遞給每個stmt_ty函數的。這點在以後的Python解釋器實現中可能會有改變。

to be Continued……

技術交流群:348904670,本文僅授權51Reboot相關賬號發布。


推薦閱讀:

[八卦] LLILC項目貌似掛了…
寫在垠神前面: CHR3 語言
不同編譯器是如何處理預編譯頭文件的?
[八卦] Phoenix Compiler Infrastructure或許那個有望開源?
xmake 源碼架構剖析

TAG:Python | 编译器 | 抽象语法树 |