在過去六個月,我一直在致力于開發一門叫 Pinecone 的編程語言。我還不能說它已經成熟了,但是它在使用中已經擁有足夠多(編程語言)的特征,例如:
- 變量
- 函數
- 用戶定義的結構體
如果你有興趣,可以看看 Pinecone 的引導頁(landing page)或者它的GitHub。
我不是一個專家。當我開始這個工程的時候,我對我所做的事情還沒有方向,但我還是沒有放棄。我在語言創建上的級別為0,只是讀了一點點在線的資料,也沒有遵循我給出的那些建議。
不過,我還是制造了一個完整的新語言。并且它能工作。所以我一定做了正確的事情。
在這篇文章中,我將深入展示管線?Pinecone (以及其他編程語言)把源碼變成魔法。
我也會談談我已經做出的一些權衡,以及為什么我會做出那些決定。
這絕對不是制作編程語言的完整教程,但是如果你對語言開發感到好奇,那么這是一個好的開始。
入門
“我都不知道我該從哪里開始”,當我告訴其他開發人員我在寫一門語言時,我通常會得到這樣的回應。如果聽后的反應也是這樣,我現在將通過一些已經嘗試過的決定和步驟,來告訴你如何開始一門新語言。
編譯型 vs 解釋型
語言主要有兩種類型:編譯型和解釋型:
編譯器會計算出一個程序將執行的操作,將其轉換為“機器代碼”(計算機可以運行的格式,非常快),然后保存以便稍后執行。
一個解釋器逐行逐步執行源代碼,弄清楚它在做什么。
技術上,任何語言都可以被編譯或解釋,但是一種或另一種語言通常對于特定語言更有意義。一般來說,解釋往往更加靈活,而編譯往往具有更高的性能。但這只是解決復雜問題前的預熱。
我高度重視性能,我看到缺乏高性能和簡單性的編程語言,所以我去編譯了 Pinecone。
這是需要今早確定的重要決定,因為很多語言設計決策受到它影響(例如,靜態類型對于編譯型語言來說是一個很大的好處,但對于解釋型語言而言并不是那么重要)。
盡管 Pinecone 是按照編譯型設計,但它也有唯一一個可運行的,功能完整的解釋器。原因我稍后會解釋。
選擇一門語言
我知道這有點像是一個元數據,但編程語言本身就是一個程序,因此你需要用一種語言編寫它。 我選擇了 C++ ,因為它的性能和龐大的功能集。此外,我其實很喜歡使用 C ++ 工作。
如果你正在編寫一種解釋性語言,那么在編譯語言(如 C、C ++ 或 Swift )中編寫將是非常有意義的,因為你的解釋型語言中的性能損失及其對應的解釋器將會更加復雜。
如果你打算編譯,較慢的語言(如 Python 或 JavaScript )是更為可接受的。編譯時間可能很糟糕,但在我看來,運行時間差別不大。
高級設計
一門編程語言通常被構造為一類管線。也就是說,它通常擁有幾個階段。每個階段的數據都會以明確的方式被格式化。還具有將數據從這一階段轉換到下一個階段的功能。
第一個階段是一串包含了整個輸入源文件的字符串。最終階段是可以被運行的東西。我們逐步完成?Pinecone 管線的時候,這一切就會變得清晰起來。
Lexing 詞法
大多數編程語言的第一步是詞法分析或分詞。 “Lex” 是詞法分析的縮寫,這是一個非常棒的詞,是將一大堆文本分解成多個符號。 “tokenizer” 這個詞更有意義,但是,“詞法分析”說起來很有趣,因此我經常使用它。
標記
標記或記號是語言的一個單元。標記可能是一個變量或函數名(也叫標識符),也可能是一個操作符或數字。
詞法分析器的任務
詞法分析器將包含源碼的文件作為輸入字符串,輸出包含標記符號的列表。
流水線(就是編譯過程)后面的階段將不再參考這些字符串源代碼,所以詞法分析器必須產生所有后面各階段需要的信息。之所以會有這樣相對嚴格的格式設計,是因為這個階段詞法分析器可以做一些工作,比如移除注釋或檢測標識符或數字等。如果你將這些邏輯規則放在詞法分析器里,那么在構造語言的其它部分時就不必再考慮這些規則了,而且你可以方便地在同一個地方集中修改這些語法規則。
Flex
我開始開發這個語言,第一件事情就是寫了一個簡單的詞法。不久之后,我開始學習可以讓詞法更簡單正確的工具。
這個小工具就是 Flex ,一個生成詞法的程序。你傳入一個具有特定格式來描述語言語法的文件。它會生成一個 C 語言語法的程序代碼。
我的決定
我選擇暫時保留最初寫的詞法分類器。因為到最后我沒有看到 Flex 的明顯優勢,至少不能達到添加依賴和完成復雜構建。
我的詞法分類器只有幾百行代碼,幾乎沒有什么問題。迭代我的詞法分類器也給了更多的靈活性。例如在不編輯多個文件的情況下向語言添加操作符。
語法分析
管線流程的第二階段就是語法分析器。語法分析器把標識符列表解析為一個帶結點的樹。用于存儲這種數據的樹稱為抽象語法樹,即 AST 。?最后在 Pinecone 的抽象語法樹中不會包含任何標識符類型信息,它就是一個簡單的結構化的標識符。
解析器的作用
解析器將結構添加到詞法分析器產生有序列表中的令牌。 為了阻止歧義,解析器必須考慮括號和操作順序。 簡單的解析運算符并不怎么困難,但隨著更多的語言結構的添加,解析變得非常復雜。
Bison
再次,有一個決定涉及第三方庫。 主要的解析庫是 Bison。 Bison 的作品很像 Flex。 你使用存儲語法信息的自定義格式編寫文件,然后 Bison 使用該文件生成將執行解析的 C 程序。 但我沒有選擇使用?Bison。
為什么自定義更好
在詞法分析器中,使用我自己的代碼這是相當明顯的決定。詞法分析器是一個這樣一個小程序,我自己不寫,感覺就像不會寫我自己的“left-pad”一樣愚蠢。
解析器是另一回事。我的Pinecone解析器目前是750線長,我寫了三個,因為前兩個都是垃圾。
我做出這樣的決定原因有很多,雖然不算順利,但大部分都是正確的。主要內容如下:
最小化工作流中的上下文切換:C ++和Pinecone之間的上下文切換是不夠的,而不會拋出Bison的語法
保持構建簡單:每次語法改變Bison必須在構建之前運行。這可以是自動化的,但是在構建系統之間切換時會變得很痛苦。
我喜歡構建很酷玩意:我沒有做Pinecone,因為我認為這很容易,所以為什么我自己決定一個中心角色?自定義解析器可能不是微不足道的,但它是完全可行的。
一開始我并不完全確定這是否可行,但是我對Walter Bright(C ++的早期版本的開發人員,D語言的創造者)不得不說的是:
有一點更有爭議的是,我不會因為詞法分析器或解析器生成器和其他所謂的”編譯器的編譯器“浪費時間,這些太浪費時間。編寫詞法分析器和解析器是編寫編譯器的一小部分工作。使用一個生成器將花費與編寫一個手工一樣多的時間,它將把您與生成器(在將編譯器移植到一個新平臺上非常重要)相結合。生成器也有時候會發出糟糕的錯誤信息和不幸的聲音。
行為樹(Action Tree)
我們現在已經離開了有共同術語或者通用術語的領域,至少這些術語我不認識。從我的理解,我所謂的‘行動樹' 是最類似于 LLVM 的 IR(中間表示)。
我花了相當長的一段時間弄清楚,行為樹和抽象語法樹之間有一個細微但非常重要的區別,我們應該區別對待(這促成了解析器的改寫)。
行為樹 vs AST
簡單來說,行為樹是帶有上下文的 AST。上下文是一個函數返回的類型的信息,或者兩個地方使用的變量實際上是相同的變量。 因為它需要弄清楚并記住所有這些上下文,生成行為樹的代碼需要大量的命名空間查找表和其他的東西。
運行行為樹
一旦我們有了行為樹,運行代碼就很容易了。 每個行為節點都有一個函數“execute”,它接受一些輸入,不管行為應該如何(包括可能調用子行為),返回行為的輸出。 這是行為中的解釋器。
編譯的選擇
等等,Pinocone 不是應該先編譯嗎?是的,但是編譯起來要比解釋復雜的多,有幾種解決方案:
新開發一個編譯器
聽起來是個好辦法,我喜歡創造東西,早就想好好研究下編譯領域了。
但是,寫一個編譯器并不是將語言的每個元素翻譯成機器代碼這么簡單,因為有很多不同的架構和操作系統,個人想要編寫一個跨平臺的編譯器不切實際。
即使是 Swift 團隊的 Rust 和 Clang 也不想從頭開始編寫,他們的辦法是...
LLVM
LLVM 是一個編譯工具集,基本上就是一個庫,可以把你的編程語言編譯成可執行文件,看似是完美的選擇,所以我馬上使用了它,但不幸的是當時并未意識到水有多深。
LLVM 即使沒有匯編語言那么難,也是一個異常龐大的庫,幾乎沒法使用。即使他們有很好的幫助文檔,但是我覺得在完全使用 LLVM 實現 Pinecone 之前,我還要多積累些經驗。
轉譯
我想快速編譯?Pinecone,所以我轉向了一種可行的方法:轉譯。
我寫了一個 Pinecone 到 C ++ 轉譯器,并添加了使用 GCC 自動編譯輸出源碼的功能。 這個目前適用于幾乎所有 Pinecone 程序(但也有例外)。 它不是一個特別便攜或可擴展的解決方案,但是個可用的臨時解決方案。
未來
假設我繼續開發 Pinecone,它遲早將得到 LLVM 的編譯支持。 懷疑無論我做了多少工作,轉譯器永遠不會完全穩定工作,LLVM 的好處則很多。 問題是什么時候我才能有時間在 LLVM 中做一些示例項目,并掌握它。
在此之前,解釋器對于微不足道的程序是非常好的,并且 C ++ 轉譯適用于大多數需要更多性能的時候。
結論
我希望我所編寫的編程語言對你來說簡單明了。如果你想自己做一個,我強烈推薦它。還有很多實現細節需要弄清楚,這里的大綱應該對你有所幫助。
這是我給出的入門建議(記住,我真的不知道我做的什么,所以僅舉個例子):
- 如有疑問,請選擇解釋型的。解釋型語言通常更易于設計、構建和學習。如果你確定你想要做的是編譯型語言,我不會阻止你嘗試編寫一個,但持觀望態度。
- 當談到詞法分析器和解析器,選擇任何你想要的。這里有很多自己編寫和反方的有效論據。最后,如果你給出了你的設計,并以合理的方式實現了一切,這并不重要。
- 從本文結束部分中的管道中學到一些技巧。我在設計管道時有很多嘗試和錯誤。我試圖消除AST,將AST變成action樹,以及其他糟糕的想法。這個管道可以工作了,所以不需要改動它,除非你有一個很好的主意。
- 如果你沒有時間或動機來實施復雜的通用語言,請嘗試像Brainfuck一樣實現一個深奧的語言。這些解釋器可以短至幾百行。
很抱歉我在Pinecone的實現過程中做了一些糟糕的決定,但是我已經重寫了大部分受這種錯誤影響的代碼。
現在,Pinecone已經足夠好了,特別是它的功能,可以接受改進。編寫Pinecone對我而言是一項非常受益和愉快的經歷,它才剛剛開始。
編譯自:I wrote a programming language. Here’s how you can, too.