歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

使用 Go 構建一個解釋型語言

我目前正參與我們的一個大項目,Alloy。Alloy 是一種編譯型的編程語言。我目前在計算機及編程領域最喜歡的一個愛好就是語言。事實上,我認為每個程序員都應該對編程語言是如何工作的有個基本的了解,這就是我寫這個系列的原因。

這是系列文章中的第一篇文章。該系列將描述我已經寫過的代碼,來向你展示如何制作自己的編程語言。這裡注意一下,本文假設你對編譯器/解釋器的理論/實踐有已有很少或沒有過往經驗。還有要注意的是,這一系列的文章不是介紹編程或Go編程的。

什麼是解釋器(interpreter)?

解釋器會直接執行或表現寫在某特定腳本語言中的指令。這可以是一種已存在的腳本語言,像 Python或者 Ruby。它也可以是一種你自己創造的腳本語言,這將是我們在這裡要做的。這一系列將從Go的基礎開始指導你實現自己的腳本語言/解釋器“玩具”

為什麼是“玩具”腳本語言/解釋器?

解釋器可以是極其復雜的。現代解釋器(比如Ruby或Python)十分龐大,包括成百上千行,甚至多達百萬的代碼量。這對一個新手來說不太容易理解。玩具語言是個更為簡化的版本,它們常常跳過或者省去一些短語(在這裡我們將不考慮優化)。制造一種玩具語言是一個理解它們如何工作的有效方法,當開始使用它們時,它們將實實在在地幫助你理解,即使你不是在一個已經存在的解釋器(如Rust)上工作。

編程語言

你可以用任意一種你喜歡的語言構建一個解釋器。在這個案例中,我將使用Go。這之前我還沒寫過許多Go,所以對我來說這也是一個學習的經歷!然而如果你不習慣用Go寫,你可以用如下任一種語言制作你的解釋器,可以是 C,Java,或者甚至是 JavaScript。

小結

由於在當今世界有如此多的解釋器和編譯器,因此有許多工具可以來幫助你制作它們。你需要決定是否考慮偷偷使用一個外部工具,或者你想要自己寫所有的代碼。我更喜歡後者,因為我覺得如果我使用某個外部工具來代勞,我就學不會它如何工作。不過這完全取決於你自己。在解釋器環境中,你是否使用這些工具會在編譯器/解釋器社區引起非常強烈的爭論。一些人會告訴你如果你不用ANTLR,BISON或者其它一些工具那麼你會出錯。另一些人會說完成它的唯一方法就是親手寫你自己的詞匯分析器(lexer)和語法分析器(parser)。最後,這是你的選擇,但在這一系列文章中,我會至少會涵蓋如何構建詞匯分析器(Lexer)和語法分析器(Parser)。

理論

在深入之前,我們需要講解一下理論。

什麼是詞匯分析器和語法分析器

如果你看到這一段落,並困惑於我所指的詞匯分析器和語法分析器,那麼不用擔心。典型做法是把這個分析過程分成不同的階段。有些階段是可選的,換句話叫做優化階段。但是大部分現代解析器幾乎處理所有階段。讓我們深入去看看這些階段吧。

詞法分析

第一階段是語法分析,基本上就是一個分詞器。詞法分析、解析器或者語法分析把字符或者輸入流分割成標記。這些標記以列表或者容器等數據結構存成標記流。解析器通過歸類這些詞(輸入流中的符號字符串),給於特定的標記某種含義。例如,*,=,+等詞可以歸為操作符,tost 和bacon可以歸為字符串常亮,而’a‘和’b‘則是字符。

解析

解析器是一個翻譯組件,它用來接收數據的輸入,一個詞法分析器產生令牌列表,並產生一個表達式,通常是一種抽象的語法樹,或其他結構。解釋器遵循的規則被叫做語法,它是你定義的一種語言的方式,這些語法諸如Extended Backus- Naur Form (EBNF)和 BNF (Backus-Naur Form),它們被用來描述一種語言的語法。下面是一個被寫成EBNF語法的例子:

letter = "A" | "a" | ... "Z" | "z" | "_";
digit = { "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" };
identifier = letter { letter | digit };

這可能對你沒有任何意義。你可能認識這些在編程語言中的符號,例如管道|,花括號{}。所有的符號都有特殊的含義:

{ }   - denotes repetition  
|     - denotes an option, similar to OR
[...] - optional terminal/nonterminal
;     - termination  
=     - definition  
...   - sequence
"..." - terminal string

我們在後面將著眼於更多的一些符號. 上面的示例定義了一個“生產規則”. 一個生產規則可能包含兩個詞匯元素: 非終端和終端. 終端是不能使用語法規則不能被改變的文字. 非終端則是可以被替換的符號, 可以把它看做是一個占位符或者一個變量. 它們有時會被稱為“語法變量”. 在上面的示例中, 標識符,字母和數字都是非終端符號. 而 "Z", "0", "1", 都是終端符號的例子,它們是常量字符,也就是說它們不能被改變.

現在來看,上面的語法中所有的符號都意味著什麼呢? 一個字母的定義是:

letter = "A" | "a" | ... "Z" | "z" | "_";

為了能夠理解,要像讀英語一樣閱讀它,例如,上面的語法被讀作 “A” 或者 “a” 到 “Z” 或者 “z” 或者 “_”. 因此一個字母可以是任何從 a-Z 或者一個下劃線的東西.

我們如此定義一個數字:

digit = { "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" };

這意味著一個數字可以是 “0” 或者 “1” 或者 “2” … 你理解到點子上了. 不過, 要注意這裡的大括號. 如果你還記得我們在上面提供的列表, 括弧代表了重復, 指定重復 0 到 n 次, 這裡的n可以是任何一個數字. 這就意味著一個數字可以是 0 - 9 總共重復 n 多次, 因此 123 是對的, 5123 也是對的.

最後是標識符:

identifier = letter { letter | digit };

目前我們理解了字母(letter)和數字(digit)的意思, 我們現在就能夠理解這個小的生產規則. 基本上,一個標識符必須以一個字符開頭,其後可以是0個或者更多個不同字母或者數字的重復. 例如,a_, a_a, a_1, a__, 等等都是正確的標識符.

這兩個階段的詞法和語法解析通常指的是作為前端的編譯器和解釋器。現在,讓我們開始寫一些代碼,我將使用GO來編寫。所有的源代碼將公布在我的 Github頁面上。如果你接著使用GO來編寫,首先為你的項目建立一個新的目錄並且設置好你的main go文件。剛好,我編寫了一個簡單的hello world文件來進行測試。GO擁有一個神奇的工作空間系統,因此一開始,你就需要創建你的工作空間,我一直使用Linux來作為我的工作空間,因此我使用GO設置$HOME/go 的環境變量
。為方便起見,GO推薦我們增加這個設置到達我們的路徑:

mkdir $HOME/go 
export PATH=$PATH:$GOPATH/bin

我的項目的基本路徑是在 github.com/felixangell。

你可以找到你想要的,或者你的 github 用戶名:

mkdir -p $GOPATH/src/github.com/yourusername

現在開始設置我們的解釋器程序,我們在個人目錄下創建一個文件夾,名字可以是你給這個解釋器起的任何名字,我叫它 vident。我們進入這個目錄。

mkdir $GOPATH/src/github.com/felixangell/vident
cd $GOPATH/src/github.com/felixangell/vident

然後我們創建一個簡單的文件作為測試用,可以直接拷貝這一部分:

package main
 
import "fmt"
 
func main() {
  fmt.Printf("hello, world\n");
}

把他保存到我們剛剛建立的文件夾 vident 中,名字為 main.go。現在我們編譯並運行它:

go install
vident

因為我們正在用工程目錄結構系統,我們需要添加 bin 目錄到我們的目錄,然後簡單的運行上面的代碼。當你運行時,你應該可以看到輸出了“hello, world”。

那麼接下來我們要定義我們的語言。Vident 是一門簡單的語言,我們從一些小的特性入手,然後我們再轉移到復雜的示例。下面是 Vident 的一個代碼實例:

let x = 5 + 5
print: x, "hello", x

我需要把->改為:,否則熟悉 Tumblr 格式的人對它很多抱怨,抱歉!我們語言的 EBNF 語法:

letter = "A" | "a" | ... "Z" | "z" | "_";
digit = { "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" };
identifier = letter { letter | digit }; 
number_literal = digit | [ "." digit ];
string_literal = """ letter { letter } """;
char_literal = "'" letter "'";
literal = number_literal | string_literal | char_literal;
binaryOp = "+" | "-" | "/" | "*";
binary_expr = expression binaryOp expression;
expression = binary_expr | function_call | identifier | literal;
let_stat = "let" identifier [ "=" expression ];
arguments = { expression "," };
function_call = identifier [ ":" arguments ];
statement = let_stat | function_call;

目前我們已經為這門語言引入了一些東西,最明顯的是方括號。方括號表示一個可選值,例如:

let_stat = "let" identifier [ "=" expression ];

這代表 let x 和 let x = 5 + 5 都是有效的,第一個是一個定義,比如定義變量,第二是顯示的變量聲明,即定義變量並聲明值。

現在看上面的語法可能會有點復雜,但如果你一點點的靠近去理解它,它就會比你想象的更加簡單. 注意,我們不會一下就全部實現它,而是按階段分部分去著重於語法的每一個部分並進行實現!

不管怎麼樣,如上就是第一部分! 敬請關注接下來的章節,我們將會編寫詞法分析器,而我們也會討論更多有關解釋器後端的內容。

Linux系統入門學習-在Linux中安裝Go語言  http://www.linuxidc.com/Linux/2015-02/113159.htm

Ubuntu 安裝Go語言包 http://www.linuxidc.com/Linux/2013-05/85171.htm

《Go語言編程》高清完整版電子書 http://www.linuxidc.com/Linux/2013-05/84709.htm

Go語言並行之美 -- 超越 “Hello World” http://www.linuxidc.com/Linux/2013-05/83697.htm

我為什麼喜歡Go語言 http://www.linuxidc.com/Linux/2013-05/84060.htm

Go語言內存分配器的實現 http://www.linuxidc.com/Linux/2014-01/94766.htm

英文原文:Part 1: Let’s build an interpreted language in Go!

Copyright © Linux教程網 All Rights Reserved