在過去六個月,我一直在致力于開發(fā)一門叫 Pinecone 的編程語言。我還不能說它已經(jīng)成熟了砰碴,但是它在使用中已經(jīng)擁有足夠多(編程語言)的特征,例如:
- 變量
- 函數(shù)
- 用戶定義的結構體
如果你有興趣板丽,可以看看 Pinecone 的引導頁(landing page)或者它的GitHub呈枉。
我不是一個專家。當我開始這個工程的時候埃碱,我對我所做的事情還沒有方向猖辫,但我還是沒有放棄。我在語言創(chuàng)建上的級別為0砚殿,只是讀了一點點在線的資料啃憎,也沒有遵循我給出的那些建議。
不過似炎,我還是制造了一個完整的新語言荧飞。并且它能工作凡人。所以我一定做了正確的事情。
在這篇文章中叹阔,我將深入展示管線?Pinecone (以及其他編程語言)把源碼變成魔法挠轴。
我也會談談我已經(jīng)做出的一些權衡,以及為什么我會做出那些決定耳幢。
這絕對不是制作編程語言的完整教程岸晦,但是如果你對語言開發(fā)感到好奇,那么這是一個好的開始睛藻。
入門
“我都不知道我該從哪里開始”启上,當我告訴其他開發(fā)人員我在寫一門語言時,我通常會得到這樣的回應店印。如果聽后的反應也是這樣冈在,我現(xiàn)在將通過一些已經(jīng)嘗試過的決定和步驟,來告訴你如何開始一門新語言按摘。
編譯型 vs 解釋型
語言主要有兩種類型:編譯型和解釋型:
編譯器會計算出一個程序將執(zhí)行的操作包券,將其轉換為“機器代碼”(計算機可以運行的格式,非踌畔停快)溅固,然后保存以便稍后執(zhí)行。
一個解釋器逐行逐步執(zhí)行源代碼兰珍,弄清楚它在做什么侍郭。
技術上,任何語言都可以被編譯或解釋掠河,但是一種或另一種語言通常對于特定語言更有意義亮元。一般來說,解釋往往更加靈活唠摹,而編譯往往具有更高的性能苹粟。但這只是解決復雜問題前的預熱。
我高度重視性能跃闹,我看到缺乏高性能和簡單性的編程語言,所以我去編譯了 Pinecone毛好。
這是需要今早確定的重要決定望艺,因為很多語言設計決策受到它影響(例如,靜態(tài)類型對于編譯型語言來說是一個很大的好處肌访,但對于解釋型語言而言并不是那么重要)找默。
盡管 Pinecone 是按照編譯型設計,但它也有唯一一個可運行的吼驶,功能完整的解釋器惩激。原因我稍后會解釋店煞。
選擇一門語言
我知道這有點像是一個元數(shù)據(jù),但編程語言本身就是一個程序风钻,因此你需要用一種語言編寫它顷蟀。 我選擇了 C++ ,因為它的性能和龐大的功能集骡技。此外鸣个,我其實很喜歡使用 C ++ 工作。
如果你正在編寫一種解釋性語言布朦,那么在編譯語言(如 C囤萤、C ++ 或 Swift )中編寫將是非常有意義的,因為你的解釋型語言中的性能損失及其對應的解釋器將會更加復雜是趴。
如果你打算編譯涛舍,較慢的語言(如 Python 或 JavaScript )是更為可接受的。編譯時間可能很糟糕唆途,但在我看來富雅,運行時間差別不大。
高級設計
一門編程語言通常被構造為一類管線窘哈。也就是說吹榴,它通常擁有幾個階段。每個階段的數(shù)據(jù)都會以明確的方式被格式化滚婉。還具有將數(shù)據(jù)從這一階段轉換到下一個階段的功能图筹。
第一個階段是一串包含了整個輸入源文件的字符串。最終階段是可以被運行的東西让腹。我們逐步完成?Pinecone 管線的時候远剩,這一切就會變得清晰起來。
Lexing 詞法
大多數(shù)編程語言的第一步是詞法分析或分詞骇窍。 “Lex” 是詞法分析的縮寫瓜晤,這是一個非常棒的詞,是將一大堆文本分解成多個符號腹纳。 “tokenizer” 這個詞更有意義痢掠,但是,“詞法分析”說起來很有趣嘲恍,因此我經(jīng)常使用它足画。
標記
標記或記號是語言的一個單元。標記可能是一個變量或函數(shù)名(也叫標識符)佃牛,也可能是一個操作符或數(shù)字淹辞。
詞法分析器的任務
詞法分析器將包含源碼的文件作為輸入字符串,輸出包含標記符號的列表俘侠。
流水線(就是編譯過程)后面的階段將不再參考這些字符串源代碼象缀,所以詞法分析器必須產生所有后面各階段需要的信息蔬将。之所以會有這樣相對嚴格的格式設計,是因為這個階段詞法分析器可以做一些工作央星,比如移除注釋或檢測標識符或數(shù)字等霞怀。如果你將這些邏輯規(guī)則放在詞法分析器里,那么在構造語言的其它部分時就不必再考慮這些規(guī)則了等曼,而且你可以方便地在同一個地方集中修改這些語法規(guī)則里烦。
Flex
我開始開發(fā)這個語言,第一件事情就是寫了一個簡單的詞法禁谦。不久之后胁黑,我開始學習可以讓詞法更簡單正確的工具。
這個小工具就是 Flex 州泊,一個生成詞法的程序丧蘸。你傳入一個具有特定格式來描述語言語法的文件。它會生成一個 C 語言語法的程序代碼遥皂。
我的決定
我選擇暫時保留最初寫的詞法分類器力喷。因為到最后我沒有看到 Flex 的明顯優(yōu)勢,至少不能達到添加依賴和完成復雜構建演训。
我的詞法分類器只有幾百行代碼弟孟,幾乎沒有什么問題。迭代我的詞法分類器也給了更多的靈活性样悟。例如在不編輯多個文件的情況下向語言添加操作符拂募。
語法分析
管線流程的第二階段就是語法分析器。語法分析器把標識符列表解析為一個帶結點的樹窟她。用于存儲這種數(shù)據(jù)的樹稱為抽象語法樹陈症,即 AST 。?最后在 Pinecone 的抽象語法樹中不會包含任何標識符類型信息震糖,它就是一個簡單的結構化的標識符录肯。
解析器的作用
解析器將結構添加到詞法分析器產生有序列表中的令牌。 為了阻止歧義吊说,解析器必須考慮括號和操作順序论咏。 簡單的解析運算符并不怎么困難,但隨著更多的語言結構的添加颁井,解析變得非常復雜厅贪。
Bison
再次,有一個決定涉及第三方庫蚤蔓。 主要的解析庫是 Bison。 Bison 的作品很像 Flex糊余。 你使用存儲語法信息的自定義格式編寫文件秀又,然后 Bison 使用該文件生成將執(zhí)行解析的 C 程序单寂。 但我沒有選擇使用?Bison。
為什么自定義更好
在詞法分析器中吐辙,使用我自己的代碼這是相當明顯的決定宣决。詞法分析器是一個這樣一個小程序,我自己不寫昏苏,感覺就像不會寫我自己的“l(fā)eft-pad”一樣愚蠢尊沸。
解析器是另一回事。我的Pinecone解析器目前是750線長贤惯,我寫了三個洼专,因為前兩個都是垃圾。
我做出這樣的決定原因有很多孵构,雖然不算順利屁商,但大部分都是正確的。主要內容如下:
最小化工作流中的上下文切換:C ++和Pinecone之間的上下文切換是不夠的颈墅,而不會拋出Bison的語法
保持構建簡單:每次語法改變Bison必須在構建之前運行蜡镶。這可以是自動化的,但是在構建系統(tǒng)之間切換時會變得很痛苦恤筛。
我喜歡構建很酷玩意:我沒有做Pinecone官还,因為我認為這很容易,所以為什么我自己決定一個中心角色毒坛?自定義解析器可能不是微不足道的望伦,但它是完全可行的。
一開始我并不完全確定這是否可行粘驰,但是我對Walter Bright(C ++的早期版本的開發(fā)人員屡谐,D語言的創(chuàng)造者)不得不說的是:
有一點更有爭議的是,我不會因為詞法分析器或解析器生成器和其他所謂的”編譯器的編譯器“浪費時間蝌数,這些太浪費時間愕掏。編寫詞法分析器和解析器是編寫編譯器的一小部分工作。使用一個生成器將花費與編寫一個手工一樣多的時間顶伞,它將把您與生成器(在將編譯器移植到一個新平臺上非常重要)相結合饵撑。生成器也有時候會發(fā)出糟糕的錯誤信息和不幸的聲音。
行為樹(Action Tree)
我們現(xiàn)在已經(jīng)離開了有共同術語或者通用術語的領域唆貌,至少這些術語我不認識滑潘。從我的理解,我所謂的‘行動樹' 是最類似于 LLVM 的 IR(中間表示)锨咙。
我花了相當長的一段時間弄清楚语卤,行為樹和抽象語法樹之間有一個細微但非常重要的區(qū)別,我們應該區(qū)別對待(這促成了解析器的改寫)。
行為樹 vs AST
簡單來說粹舵,行為樹是帶有上下文的 AST钮孵。上下文是一個函數(shù)返回的類型的信息,或者兩個地方使用的變量實際上是相同的變量眼滤。 因為它需要弄清楚并記住所有這些上下文巴席,生成行為樹的代碼需要大量的命名空間查找表和其他的東西。
運行行為樹
一旦我們有了行為樹诅需,運行代碼就很容易了漾唉。 每個行為節(jié)點都有一個函數(shù)“execute”,它接受一些輸入堰塌,不管行為應該如何(包括可能調用子行為)赵刑,返回行為的輸出。 這是行為中的解釋器蔫仙。
編譯的選擇
等等料睛,Pinocone 不是應該先編譯嗎?是的摇邦,但是編譯起來要比解釋復雜的多,有幾種解決方案:
新開發(fā)一個編譯器
聽起來是個好辦法恤煞,我喜歡創(chuàng)造東西,早就想好好研究下編譯領域了施籍。
但是居扒,寫一個編譯器并不是將語言的每個元素翻譯成機器代碼這么簡單,因為有很多不同的架構和操作系統(tǒng)丑慎,個人想要編寫一個跨平臺的編譯器不切實際喜喂。
即使是 Swift 團隊的 Rust 和 Clang 也不想從頭開始編寫,他們的辦法是...
LLVM
LLVM 是一個編譯工具集竿裂,基本上就是一個庫玉吁,可以把你的編程語言編譯成可執(zhí)行文件,看似是完美的選擇腻异,所以我馬上使用了它进副,但不幸的是當時并未意識到水有多深。
LLVM 即使沒有匯編語言那么難悔常,也是一個異常龐大的庫影斑,幾乎沒法使用。即使他們有很好的幫助文檔机打,但是我覺得在完全使用 LLVM 實現(xiàn) Pinecone 之前矫户,我還要多積累些經(jīng)驗。
轉譯
我想快速編譯?Pinecone残邀,所以我轉向了一種可行的方法:轉譯皆辽。
我寫了一個 Pinecone 到 C ++ 轉譯器柑蛇,并添加了使用 GCC 自動編譯輸出源碼的功能。 這個目前適用于幾乎所有 Pinecone 程序(但也有例外)驱闷。 它不是一個特別便攜或可擴展的解決方案唯蝶,但是個可用的臨時解決方案。
未來
假設我繼續(xù)開發(fā) Pinecone遗嗽,它遲早將得到 LLVM 的編譯支持。 懷疑無論我做了多少工作鼓蜒,轉譯器永遠不會完全穩(wěn)定工作痹换,LLVM 的好處則很多。 問題是什么時候我才能有時間在 LLVM 中做一些示例項目都弹,并掌握它娇豫。
在此之前,解釋器對于微不足道的程序是非常好的畅厢,并且 C ++ 轉譯適用于大多數(shù)需要更多性能的時候冯痢。
結論
我希望我所編寫的編程語言對你來說簡單明了。如果你想自己做一個框杜,我強烈推薦它浦楣。還有很多實現(xiàn)細節(jié)需要弄清楚,這里的大綱應該對你有所幫助咪辱。
這是我給出的入門建議(記住振劳,我真的不知道我做的什么,所以僅舉個例子):
- 如有疑問油狂,請選擇解釋型的历恐。解釋型語言通常更易于設計、構建和學習专筷。如果你確定你想要做的是編譯型語言弱贼,我不會阻止你嘗試編寫一個,但持觀望態(tài)度磷蛹。
- 當談到詞法分析器和解析器吮旅,選擇任何你想要的。這里有很多自己編寫和反方的有效論據(jù)弦聂。最后鸟辅,如果你給出了你的設計,并以合理的方式實現(xiàn)了一切莺葫,這并不重要匪凉。
- 從本文結束部分中的管道中學到一些技巧。我在設計管道時有很多嘗試和錯誤捺檬。我試圖消除AST再层,將AST變成action樹,以及其他糟糕的想法。這個管道可以工作了聂受,所以不需要改動它蒿秦,除非你有一個很好的主意。
- 如果你沒有時間或動機來實施復雜的通用語言蛋济,請嘗試像Brainfuck一樣實現(xiàn)一個深奧的語言棍鳖。這些解釋器可以短至幾百行。
很抱歉我在Pinecone的實現(xiàn)過程中做了一些糟糕的決定碗旅,但是我已經(jīng)重寫了大部分受這種錯誤影響的代碼渡处。
現(xiàn)在,Pinecone已經(jīng)足夠好了祟辟,特別是它的功能医瘫,可以接受改進。編寫Pinecone對我而言是一項非常受益和愉快的經(jīng)歷旧困,它才剛剛開始醇份。
編譯自:I wrote a programming language. Here’s how you can, too.