👓 词法分析和语法分析#
由於是簡單地記錄一下這個我花了好久終於想通的知識點,所以我不會在這裡長篇大論地展開這兩個可以非常深入去聊的話題,只是為了幫助本文讀者更好地理解。
詞法分析器,英語裡一般稱為 Lexer 或 Scanner,主要用於將源代碼拆成一個列表,每一項都是不可再分的「詞素」,英語對應詞為 Token。
我們用 C 語言舉個例子,源代碼如下:
#include <stdio.h>
int main() {
printf("Hello world!");
}
這裡是上面源代碼中所有 Token 的信息:
<預處理指令 #include> <空格> <左尖括號 '<'> <名稱 stdio.h> <右尖括號 '>'> <換行符>
<換行符>
<關鍵字 int> <空格> <名稱 main> <左圓括號 '('> <右圓括號 ')'> <空格> <左花括號 '{'> <換行符>
<空格> <空格> <名稱 printf> <左圓括號 '('> <字符串字面量> <右圓括號 ')'> <分號 ';'> <換行符>
<右花括號 '}'>
詞法分析器的輸入是一段字符串內容,輸出是一個 Token 列表。而對於每一個 Token 來說,其關鍵屬性為 位置(Position)、內容(Raw content)、類型(Token Type) 三項,記錄位置主要是為了之後代碼包含錯誤時能夠方便程式設計師定位問題。
而語法分析器,英語裡一般稱為 Parser,它的工作過程可以理解為根據程式語言的設計方案(Specification)將符合定義的幾個 Token 組合為一個語法節點,最終組合成一棵「抽象語法樹」(AST,Abstract Syntax Tree)
🥵 為什麼模板字符串不易處理#
下面我都將以 JavaScript 的模板字符串語法定義為例。
在實現 Lexer 的一開始,我以為只要根據輸入的內容逐個讀取,一定可以獲取到一一對應的詞素,但對模板字符串時我卻發現不一樣了。
你很難從詞法分析階段將下面這樣一個甚至包含嵌套的模板字符串讀取為一個「不可分割」的詞素,顯然它是可分割的。
`my name is ${"David" + ` - ${firstName}`}, nice to meet you!`
🤔️ 應該怎麼做?#
要想仍然順次地讀取出 Token,我們應該設立一些變數表示當前解析所處的狀態,並且不要按照和一般字串解析一樣的方式去看待模板字符串。
可以確定的是,模板字符串的處理一定是在語法分析階段,最終會形成一個包含如下兩種內容語法節點:
- 可能有多段分散的字串文本
- 插值表達式,而表達式同樣也是一個語法節點
interface TemplateStringNode {
quasis: TemplateElement[]
expressions: ExpressionNode[]
}
所以最好的方式是找到一個標誌性的 Token,告訴 Parser 在解析到這個符號時需要開始解析模板字符串,並根據狀態信息處理可能存在的嵌套情況。
我們設立以下兩個關鍵信息:
isReadingText
,即是否正需要讀取文本nested
,嵌套的層數
按照上面這樣一張圖的推演分析,我們可以得出以下結論:
模板字符串引號
會置反isReadingText
的狀態- 插值表達式開始標誌
${
會增加一層nested
,並將isReadingText
設為false
,因為即將讀取的是一個插值表達式。 右花括號
會減少一層nested
,並將isReadingText
設回true
,因為又回到了模板字符串的文本讀取中。
直到遇見某個模板字符串引號,將 isReadingText
設為 false
,而 nested 層數也為 0 時,一個模板字符串節點的解析就結束了。
參考#
兩篇 Stackoverflow 的問答: