這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
原文在此。
————翻譯分隔線————
編譯器(7)-掃描
第一部分:介紹
第二部分:編譯、轉譯和解釋
第三部分:編譯器設計概覽
第四部分:語言設計概述
第五部分:Calc 1 語言規格說明書
第六部分:標識符
現在終於可以開始在掃描器上工作了。
詞法分析
那麼,從哪裡開始呢?
這是最難的一部分,對我來說,掃描看起來應該挺簡單的,但是很快我就迷失在細節裡。有許多種實現掃描器的方法,我只會向你展示其中的一種。這裡是 Rob Pike 在一次演講中的簡報,是關於另外一種很酷的方法:在 Go 中的詞法掃描。
掃描器的基本原理就是從頂到底、從左至右、直到原始碼的結尾進行檢索。每次,發現所需要的元素,就報告詞法串被找到,標識符會告訴解析器它是什麼,以及找到它的位置。
有限狀態機器
我現在不會真正深入到有限狀態機器(有限自動機)或其他任何相關內容的細節中去。你應當自己去發掘相關內容。Coursera 有一個關於編譯器設計的課程可能會有所協助,同時它也包含了相關的話題。思想非常重要,不過也不是一定有必要瞭解每一個細節(不過我還是鼓勵你去瞭解一下它們)。
基本思路是我們的掃描器會返回有限個數字狀態。這些狀態是由標識符標識的,同時只可以返回已經定義的有限的標識符。可以認為我們的掃描器具有有限的狀態。也就是有限狀態機器。理解自動機對於理解Regex以及掃描時是否應當接受或拒絕一個獨立的字元會有協助。
很快這些就會變得清晰。
哎呀
我想要對我第一次嘗試編寫掃描器的時候犯的錯誤做一些澄清。在沒有對語言做任何可靠的定義之前編寫編譯器的任何一個部分都是非常糟糕的想法。如果語言的核心設計一直在變化中,那麼你將會需要不斷的重寫編譯器。就是這樣,不斷的。我不得不將我的第一個解譯器重寫了數遍,每次我修訂語言的時候,都是各種蛋疼。完全是浪費時間(譯註:我的 Yin 語言 Group 的申請被駁回了,多半是因為申請的時候我寫了:Looking for the specification。本文作者和王大師對語言和編譯器的設計和實現思路完全不同。不過我覺得,應當沒有對錯,只有選擇)。
這個過程最終讓我意識到這是一個非常糟糕的決定。那些一開始看起來是很棒的想法最終會變成一個懷主意,但是完全不做決定最終將會是災難。許多次,我都對著語言的設計問自己,“你到底是見了什麼鬼做了這些?真蠢!”我是百分之百的事後諸葛亮。
掃描器
掃描器相當簡單。我們從一個可以跟蹤例某些事情的簡單對象開始。例如跟蹤當前掃描的字元、從檔案開頭算起的位移量(讀取位移量)、已經掃描的代碼和指向檔案本身的細節的指標等。
掃描的第一步是利用 Init 初始化掃描器。這裡沒什麼特別的,外層會繼續調用下一個方法,我管這個叫做“填裝彈藥”。
next方法比是個較有趣的函數。它首先將當前的字元設為零,以便確定檔案的結尾。如果讀取的位移量(roffset)比檔案的長度小,那麼位移量(offset)就修改為讀取位移量(roffset)。如果發現了一個新行,就在 file 對象中記錄它的位置。主意,雖然我們丟棄了新行,但是仍然記錄了它的位置。最後,更新當前字元並增加讀取位移量。
讀取位移量和 Unicode
要如何處理增加讀取位移量呢?特別是 Unicode 來說。一個字元可能佔用一個或多個位元組,因此每次對位移量加一是不行的。UTF8 包的 DecodeRune 函數返回了字元的位元組數。在這種情況下,讀取位移量就可以確認下一個需要讀取的字元的開始位置。
當前這個掃描器並不是 Unicode 友好的,不過還是可以整合這些函數,這樣可以最終在添加 Unicode 支援的時候少做點工作。還會用到 unicode 包中的 IsDigit 和 IsSpace 函數。
掃描
這個是掃描器的準系統。Scan 方法首先會跳過空白字元。skipWhitespace 只是讓掃描器一次一個字元的向前移動,一直到第一個非空白字元。我利用 unicode.IsSpace 來達到這個目的。
接下來,看一下多字元元素吧。在這個例子中,只是尋找數字。然後尋找單字元元素,最終將所有內容打包到一起,彙報錯誤的字元或結束檔案處理。
每個部分結束,都需要通過調用 next 來增加掃描器的位置,並且返回掃描的結果。
我們還要有一個語言規格說明書在手邊。它告訴我們到底需要做什麼。如果你需要的話,可以跳回到第五部分找到它。
整數
如果我們遇到的是數字,我們就用 scanNumber 來掃描一個更長的串。
我選擇使用 unicode.IsDigit 函數,不過也可以編寫一個簡單的自己的實現。例如:return s.ch >= '0' && s.ch <= '9'
這樣簡單的代碼就可以滿足。scanNumber 會持續的向前移動掃描器,直到遇到第一個非數字。在迴圈後的 if 語句處理數字出現在檔案結尾的情況。
在之後版本的 Calc 裡,或者你自己的版本裡,我會擴充這個函數來包含其他格式的數字,比如浮點數或十六進位。
更多的掃描
如果沒有找到數字,那麼就繼續檢查單個字元。除了分號之外的其他內容都容易理解。
檔案中分號開始到行未的內容是注釋。在當前的掃描器實現中,注釋被直接丟棄,不過要保留它們也很容易。例如,Go 的掃描器將它發現的任何注釋都傳遞給解析器,這樣解析器就可以建立超好的、人人都愛的 Go 文檔了!
總結
這就是掃描器的全部。相當直白。
向解析器前進!