跟vczh看實例學編譯原理——零:序言

在《如何設計一門語言》裏面,我講了一些語言方面的東西,還有痛快的噴了一些XX粉什麼的。不過單純講這個也是很無聊的,因此我開了這個《跟vczh看實例學編譯原理》系列,意在科普一些編譯原理的知識,儘可能讓你們能夠在創造語言以後,本身寫一個原型。在這裏我拿我創造的一門頗有趣的語言 https://github.com/vczh/tinymoe/ 做爲實例。html

 

商業編譯器對功能和質量的要求都是很高的,裏面大量的東西其實都跟編譯原理不要緊。一個典型的編譯原理的原型有什麼特徵呢?node

  1. 性能低
  2. 錯誤信息難看
  3. 沒有檢查全部狀況就生成代碼
  4. 優化作得爛
  5. 幾乎沒有編譯選項

 

等等。Tinymoe就知足了上面的5種狀況,由於個人目標也只是想作一個原型,向你們介紹編譯原理的基礎知識。固然,我對語法的設計仍是儘可能靠近工業質量的,只是實現沒有花太多心思。jquery

 

爲何我要用Tinymoe來做爲實例呢?由於Tinymoe是少有的一種用起來簡單,並且庫能夠有多複雜寫多複雜的語言,就跟C++同樣。C++11額標準庫在一塊兒用簡直是愉快啊,Tinymoe的代碼也是這麼寫的。可是這並不妨礙你能夠在寫C++庫的時候發揮你的想象力。Tinymoe也是同樣的。爲何呢,我來舉個例子。git

 

Hello, world!

Tinymoe的hello world程序是很簡單的:github

 

module hello world 編程

using standard library 數組

 

sentence print (message) ruby

    redirect to "printf" 函數式編程

end 函數

 

phrase main

    print "Hello, world!"

end

 

module指的是模塊的名字,普通的程序也是一個模塊。using指的是你要引用的模塊——standard library就是Tinymoe的STL了——固然這個程序並無用到任何standard library的東西。說到這裏你們可能意識到了,Tinymoe的名字能夠是不定長的token組成的!沒錯,後面你們會慢慢意識到這種作法有多麼的強大。

 

後面是print函數和main函數。Tinymoe是嚴格區分語句和表達式的,只有sentence和block開頭的函數才能做爲語句,並且同時只有phrase開頭的函數才能做爲表達式。因此下面的程序是不合法的:

 

phrase main

    (print "Hello, world!") + 1

end

 

緣由就是,print是sentence,不能做爲表達式使用,所以他不能被+1。

 

Tinymoe的函數參數都被寫在括號裏面,一個參數須要一個括號。到了這裏你們可能會以爲很奇怪,不過很快就會有解答了。爲何要這麼作,下一個例子就會告訴咱們。

 

print函數用的redirect to是Tinymoe聲明FFI(Foreign Function Interface)的方法,也就是說,當你運行了print,他就會去host裏面找一個叫作printf的函數來運行。不過你們不要誤會,Tinymoe並無被設計成能夠直接調用C函數,因此這個名字實際上是隨便寫的,只要host提供了一個叫作printf的函數完成printf該作的事情就好了。main函數就不用解釋了,很直白。

1加到100等於5050

這個例子能夠在Tinymoe的主頁(https://github.com/vczh/tinymoe/)上面看到:

 

module hello world

using standard library

 

sentence print (message)

redirect to "printf"

end

 

phrase sum from (start) to (end)

set the result to 0

repeat with the current number from start to end

add the current number to the result

end

end

 

phrase main

print "1+ ... +100 = " & sum from 1 to 100

end

 

爲何名字能夠是多個token?爲何每個參數都要一個括號?看加粗的部分就知道了!正是由於Tinymoe想讓每一行代碼均可以被念出來,因此才這麼設計的。固然,你們確定都知道怎麼算start + (start+1) + … + (end-1) + end了,因此應該很容易就能夠看懂這個函數裏面的代碼具體是什麼意思。

 

在這裏能夠稍微多作一下解釋。the result是一個預約義的變量,表明函數的返回值。只要你往the result裏面寫東西,只要函數一結束,他就變成函數的返回值了。Tinymoe的括號沒有什麼特殊意思,就是改變優先級,因此那一句循環則能夠經過添加括號的方法寫成這樣:

 

repeat with (the current number) from (start) to (end)

 

你們可能會想,repeat with是否是關鍵字?固然不是!repeat with是standard library裏面定義的一個block函數。你們知道block函數的意思了吧,就是這個函數能夠帶一個block。block有一些特性可讓你寫出相似try-catch那樣的幾個block連在一塊兒的大block,特別適合寫庫。

 

到了這裏你們心中可能會有疑問,循環爲何能夠作成庫呢?還有更加使人震驚的是,break和continue也不是關鍵字,是sentence!由於repeat with是有代碼的:

 

category

    start REPEAT

    closable

block (sentence deal with (item)) repeat with (argument item) from (lower bound) to (upper bound)

    set the current number to lower bound

    repeat while the current number <= upper bound

        deal with the current number

        add 1 to the current number

    end

end

 

前面的category是用來定義一些block的順序和包圍結構什麼的。repeat with是屬於REPEAT的,而break和continue聲明瞭本身只能直接或者間接方在REPEAT裏面,所以若是你在一個沒有循環的地方調用break或者continue,編譯器就會報錯了。這是一個花邊功能,用來防止手誤的。

 

你們可能會注意到一個新東西:(argument item)。argument的意思指的是,後面的item是block裏面的代碼的一個參數,對於repeat with函數自己他不是一個參數。這就經過一個很天然的方法給block添加參數了。若是你用ruby的話就得寫成這個悲催的樣子:

 

repeat_with(1, 10) do |item|

    xxxx

end

 

而用C++寫起來就更悲催了:

 

repeat_with(1, 10, [](int item)

{

    xxxx

});

 

block的第一個參數sentence deal with (item)就是一個引用了block中間的代碼的委託。因此你會看到代碼裏面會調用它。

 

好了,那repeat while老是關鍵字了吧——不是!後面你們還會知道,就連

 

if xxx

    yyy

else if zzz

    www

else if aaa

    bbb

else

    ccc

end

 

也只是你調用了if、else if和else的一系列函數而後讓他們串起來而已。

 

那Tinymoe到底提供了什麼基礎設施呢?其實只有select-case和遞歸。用這兩個東西,加上內置的數組,就圖靈完備了。圖靈完備就是這麼容易啊。

 

多重分派(Multiple Dispatch)

講到這裏,我不得不說,Tinymoe也能夠寫類,也能夠繼承,不過他跟傳統的語言不同的,類是沒有構造函數、析構函數和其餘成員函數的。Tinymoe全部的函數都是全局函數,可是你可使用多重分派來"挑選"類型。這就須要第三個例子了(也能夠在主頁上找到):

 

module geometry

using standard library

 

phrase square root of (number)

    redirect to "Sqrt"

end

 

sentence print (message)

    redirect to "Print"

end

 

type rectangle

    width

    height

end

 

type triangle

    a

    b

    c

end

 

type circle

    radius

end

 

phrase area of (shape)

    raise "This is not a shape."

end

 

phrase area of (shape : rectangle)

    set the result to field width of shape * field height of shape

end

 

phrase area of (shape : triangle)

    set a to field a of shape

    set b to field b of shape

    set c to field c of shape

    set p to (a + b + c) / 2

    set the result to square root of (p * (p - a) * (p - b) * (p - c))

end

 

phrase area of (shape : circle)

    set r to field radius of shape

    set the result to r * r * 3.14

end

 

phrase (a) and (b) are the same shape

    set the result to false

end

 

phrase (a : rectangle) and (b : rectangle) are the same shape

    set the result to true

end

 

phrase (a : triangle) and (b : triangle) are the same shape

    set the result to true

end

 

phrase (a : circle) and (b : circle) are the same shape

    set the result to true

end

 

phrase main

    set shape one to new triangle of (2, 3, 4)

    set shape two to new rectangle of (1, 2)

    if shape one and shape two are the same shape

        print "This world is mad!"

    else

        print "Triangle and rectangle are not the same shape!"

    end

end

 

這個例子稍微長了一點點,不過你們能夠很清楚的看到我是如何定義一個類型、建立他們和訪問成員變量的。area of函數能夠計算一個平面幾何圖形的面積,並且會根據你傳給他的不一樣的幾何圖形而使用不一樣的公式。當全部的類型判斷都失敗的時候,就會掉進那個沒有任何類型聲明的函數,從而引起一場。嗯,其實try/catch/finally/raise都是函數來的——Tinymoe對控制流的控制就是如此強大,啊哈哈哈哈。就連return均可以本身作,因此Tinymoe也不提供預約義的return。

 

那phrase (a) and (b) are the same shape怎麼辦呢?沒問題,Tinymoe能夠同時指定多個參數的類型。並且Tinymoe的實現具備跟C++虛函數同樣的性質——不管你有多少個參數標記了類型,我均可以O(n)跳轉到一個你須要的函數。這裏的n指的是標記了類型的參數的個數,而不是函數實例的個數,因此跟C++的狀況是同樣的——由於this只能有一個,因此就是O(1)。至於Tinymoe究竟是怎麼實現的,只須要看《如何設計一門語言》第五篇(http://www.cppblog.com/vczh/archive/2013/05/25/200580.html)就有答案了。

Continuation Passing Style

爲何Tinymoe的控制流均可以本身作呢?由於Tinymoe的函數都是寫成了CPS這種風格的。其實CPS你們都很熟悉,當你用jquery作動畫,用node.js作IO的時候,那些嵌套的一個一個的lambda表達式,就有點CPS的味道。不過在這裏咱們並無看到嵌套的lambda,這是由於Tinymoe提供的語法,讓Tinymoe的編譯器能夠把同一個層次的代碼,轉成嵌套的lambda那樣的代碼。這個過程就叫CPS變換。Tinymoe雖然用了不少函數式編程的手段,可是他並非一門函數是語言,只是一門普通的過程式語言。可是這跟C語言不同,由於它連C#的yield return均可以寫成函數!這個例子就更長了,你們能夠到Tinymoe的主頁上看。我這裏只貼一小段代碼:

 

module enumerable

using standard library

 

symbol yielding return

symbol yielding break

 

type enumerable collection

    body

end

 

type collection enumerator

    current yielding result

    body

    continuation

end

 

略(這裏實現了跟enumerable相關的函數,包括yield return)

 

block (sentence deal with (item)) repeat with (argument item) in (items : enumerable collection)

    set enumerator to new enumerator from items

    repeat

        move enumerator to the next

        deal with current value of enumerator

    end

end

 

sentence print (message)

    redirect to "Print"

end

 

phrase main

    create enumerable to numbers

        repeat with i from 1 to 10

            print "Enumerating " & i

            yield return i

        end

    end

 

    repeat with number in numbers

        if number >= 5

            break

        end

        print "Printing " & number

    end

end

 

什麼叫模擬C#的yield return呢?就是連惰性計算也一塊兒模擬!在main函數的第一部分,我建立了一個enumerable(iterator),包含1到10十個數字,並且每產生一個數字還會打印出一句話。可是接下來我在循環裏面只取前5個,打印前4個,所以執行結果就是

當!

 

CPS風格的函數的威力在於,每個函數均可以控制他如何執行函數結束以後寫在後面的代碼。也就是說,你能夠根據你的須要,乾脆選擇保護現場,而後之後再回復。是否是聽起來很像lua的coroutine呢?在Tinymoe,coroutine也能夠本身作!

 

雖然函數最後被轉換成了CPS風格的ast,並且測試用的生成C#代碼的確也是原封不動的輸出了出來,因此運行這個程序耗費了大量的函數調用。但這並不意味着Tinymoe的虛擬機也要這麼作。你們要記住,一個語言也好,類庫也好,給你的接口的概念,跟實現的概念,有可能徹底不一樣。yield return寫出來的確要花費點心思,因此《序言》我也不講這麼多了,後續的文章會詳細介紹這方面的知識,固然了,還會告訴你怎麼實現的。

 

尾聲

這裏我挑選了四個例子來展現Tinymoe最重要的一些概念。一門語言,要應用用起來簡單,庫寫起來能夠發揮想象力,纔是有前途的。yield return例子裏面的main函數同樣,用的時候多清爽,清爽到讓你徹底忘記yield return實現的時候裏面的各類麻煩的細節。

 

因此爲何我要挑選Tinymoe做爲實例來科普編譯原理呢?有兩個緣由。第一個緣由是,想要實現Tinymoe,須要大量的知識。因此既然這個系列想讓你們可以看完實現一個Tinymoe的低質量原型,固然會講不少知識的。第二個緣由是,我想經過這個例子向你們將一個道理,就是庫和應用 、編譯器和語法、實現和接口,徹底能夠作到隔離複雜,只留給最終用戶簡單的部分。你看到的複雜的接口,並不意味着他的實現是臃腫的。你看到的簡單的接口,也不意味着他的實現就很簡潔

 

Tinymoe目前已經能夠輸出C#代碼來執行了。後面我還會給Tinymoe加上靜態分析和類型推導。對於這類語言作靜態分析和類型推導又不少麻煩,我如今尚未徹底搞明白。譬如說這種能夠本身控制continuation的函數要怎麼編譯成狀態機才能避免掉大量的函數調用,就不是一個容易的問題。因此在系列一邊作的時候,我還會一邊研究這個事情。若是到時候系列把編譯部分寫完的同時,這些問題我也搞明白的話,那我就會讓這個系列擴展到包含靜態分析和類型推導,繼續往下講。

相關文章
相關標籤/搜索