Picol 是一個 Tcl 風格的直譯器,最初版本(v1)僅用約 500 行程式碼撰寫(第二版稍多),我於 2007 年 3 月 15 日首次發布。最近我重新檢視了原始程式碼,發現它比我記憶中更好的 C 語言程式設計範例,因此我將其放到 GitHub 上歸檔,並附上原始文章的重點。

更新:2026 年 2 月,該直譯器進行了幾項重大更新,我稱之為 Picol 2。您可以在此儲存庫的提交記錄中找到原始版本,然而,第二版幾乎與第一版一樣小巧,但更具教學意義、除錯更少且更實用,因此此 README 文件現在參考第二版的細節。您可以在本文件的後面找到 v1 和 v2 之間的變更集。

我編寫此程式碼時,心中有一些規則:

解析器與 Tcl 的非常相似,Picol 也支援字串插值,例如您可以這樣寫:

請注意,Picol 具有互動式命令列介面!因此,只需在不帶參數的情況下啟動它即可開始玩(編譯程式碼請使用 gcc -O2 -Wall -o picol picol.c)。

若要執行儲存在檔案中的程式,請使用:picol filename.tcl。

解析器可能可以重寫以減少佔用的空間,目前它佔用了近 250 行程式碼:這太多了,留給其他部分空間不足。另一方面,它是一個不錯的手寫解析器範例。

支援功能的原始列表:

這是 Picol 可以執行的程式範例:

第二版在程式碼大小或複雜度沒有顯著增加的情況下,增加了以下功能:

此版本不再嚴格遵守 500 行程式碼的限制。目前 loc 報告為 669 行。

解析(Parsing)。您在原始程式碼中看到的第一个重要部分是手寫的解析器。解析器的主要功能是 picolGetToken,它只會呼叫能夠解析 Tcl 程式不同部分的函數,並在解析結構中返回 token 的類型以及起始/結束指標,以便提取它。

求值(Eval)。此解析函數又被 picolEval 用於執行程式。每個 token 要么用於形成一個新參數(如果之前找到分隔符 token),要么串接到最後一個參數(Picol 中的插值就是這樣執行的)。一旦返回 EOL(行尾)token,picolEval 將透過在直譯器結構中儲存的命令連結列表中查找來呼叫該命令。

替換(Substitutions)。變數和命令的替換由 picolEval 本身執行。解析器能夠返回已去除 $ 和 [] 的變數和命令 token,因此所有需要做的就是查找呼叫框架中的變數並用 token 的值進行替換,或者如果它是命令替換,則遞迴呼叫 picolEval,使用結果而不是原始 token。

C 語言編寫和使用者定義的命令(C-coded and user defined commands)。命令由名稱和指向實現該命令的 C 函數的指標描述。在命令結構中,還有一個指標用於儲存程序參數列表和主體。這使得您能夠使用單一 C 函數實現多個 Picol 命令。使用者定義的程序就像命令一樣,但它們是透過將參數列表和程序的體作為額外的指標傳遞來實現的,因此單一 C 函數能夠實現所有現有的使用者定義程序。

呼叫框架(Call frames)。程序的呼叫很簡單。直譯器結構包含一個呼叫框架結構,該結構或多或少只有一個指向變數連結列表的指標(變數本身是具有兩個欄位:名稱和值的結構)。當呼叫一個程序時,會創建一個新的呼叫框架並放在舊框架的頂部。當程序返回時,頂部呼叫框架被銷毀。

在 C 語言中實現表達式求值器的常用方法是使用兩個堆疊,一個用於運算符,一個用於運算數。這很有效率且易於實現,但這裡的需要是用更少的空間(總共 41 行程式碼)編寫一個足夠好的 Tcl expr。因此,我採用了 Pratt 風格的解析器,這可能是編寫遞迴函數解析器最緊湊的方式。在此解析器中,語法被建模為一組相互遞迴的函數,但在這種特定情況下,它只是一個單一的遞迴函數。

求值器分兩步工作(程式碼註解中為三步,以進一步簡化理解,但邏輯上是兩步):

最後的技巧:沒有空間讓 expr 處理 $var 和 [commands] 的插值,因此對於 if 和 while 等函數,使用了一個技巧:將它們的表達式參數重寫為 expr <expression> 的形式,並傳遞給 picolEval()。這樣,我們就可以重用通用程式碼路徑,並且 if {$a > $b} 將類似於 if [expr $a > $b],這沒關係(但 && 和 || 的短路將無法工作)。

Picol 是一個非常簡單的程式碼庫,它讓願意學習的程式設計師能夠以最小的複雜度接觸到直譯器。然而,由於其簡單性和程式碼大小,該直譯器有許多限制:

有關更引人注目的範例,請查看 Jim TCL 直譯器。我多年前就開始了這個專案,後來由嵌入式領域的開發者社群繼續發展。它展示了如何從 Picol 發展到一個更嚴謹、效能更好的直譯器。