如何編寫簡單的parser(基礎篇)

什麼是parser?

簡單的說,parser的工作即是將代碼片段轉換成計算機可讀的數據結構的過程。這個“計算機可讀的數據結構”更專業的說法是“抽象語法樹(abstract syntax tree)”,簡稱AST。AST是代碼片段具體語義的抽象表達,它不包含該段代碼的所有細節,比如縮進、換行這些細節,所以,我們可以使用parser轉換出AST,卻不能使用AST還原出“原”代碼,當然,可以還原出語義一致的代碼,就如同將ES6語法的js代碼轉換成ES5的代碼。

parser的結構

一般來說,一個parser會由兩部分組成:

  • 詞法解析器(lexer/scanner/tokenizer)
  • 對應語法的解釋器(parser)

在解釋某段代碼的時候,先由詞法解釋器將代碼段轉化成一個一個的詞組流(token),再交由解釋器對詞組流進行語法解釋,轉化爲對應語法的抽象解釋,即是AST了。

爲了讓大家更清楚的理解parser兩部分的工作順序,我們通過一個例子來進行說明:

437 + 734

在parser解析如上的計算表達式時,詞法解析器首先依次掃描到“4”、“3”、“7”直到一個空白符,這時,詞法解析器便將之前掃描到的數字組成一個類型爲“NUM”的詞組(token);接下來,詞法解析器繼續向下掃描,掃描到了一個“+”,對應輸出一個類型爲“PLUS”的詞組(token);最後,掃描“7”、“3”、“4”輸出另一個類型爲“NUM”的詞組(token)。
clipboard.png
語法解釋器在拿到詞法解析器輸出的詞組流後,根據詞組流的“NUM”,“PLUS”,“NUM”的排列順序,解析成爲加法表達式。

由上的例子我們可以看出,詞法解析器根據一定的規則對字符串進行解析並輸出爲詞組(token),具體表現爲連續不斷的數字組合(“4”、“3”、“7”和“7”、“3”、“4”)即代表了數字類型的詞組;語法解釋器同樣根據一定的規則對詞組的組合進行解析,並輸出對應的表達式或語句。在這裏,詞法解析器應用的規則即爲詞彙語法(Lexical Grammar)的定義,語法解釋器應用的規則即爲表達式(Expressions)、語句(Statements)、聲明(Declarations)和函數(Functions)等的定義。

ECMAScript標準

看到這裏大家可能會感覺到奇怪,爲什麼講parser講的好好的,又跑到ECMAScript標準上來了呢?因爲以上提到的詞彙語法(Lexical Grammar)、表達式(Expressions)、語句(Statements)、聲明(Declarations)和函數(Functions)等都是ECMAScript標準中的所定義的,這其實也是ECMAScript標準的作用之一,即定義JavaScript的標準語法。

詞彙詞法(Lexical Grammar)

ECMAScript的詞彙詞法規定了JavaScript中的基礎語法,比如哪些字符代表了空白(White Space),哪些字符代表了一行終止(Line Terminators),哪些字符的組合代表了註釋(Comments)等。具體的規定說明,可以在ECMAScript標準11章中找到。

這裏我們不仔細研讀每個語法的定義,只需知道詞法解析器(lexer)判讀詞組(token)的依據來源於此即可,爲了讓大家有一定的瞭解,這裏,我們拿上面例子中的數字字面量(Numeric Literals)來進行說明:
ECMAScript標準中,對數字字面量的定義如下:
clipboard.png
該定義需要自上向下解讀:

首先,規則定義了數字字面量(Numeric Literal)可以是十進制字面量(Decimal Literal)、二進制整數字面量(Binary Integer Literal)、八進制整數字面量(Octal Integer Literal)、十六進制整數字面量(Hex Integer Literal);

在我們的例子中,我們只關心十進制的字面量,所以,接下來,規則定義十進制字面量(Decimal Literal)可以是包含小數點與不包含小數點的組合,這裏我們只需關注不包含小數點的定義,即十進制整數字面量(Decimal Integer Literal) + 可選的指數部分(Exponent Part);

最後,規則定義十進制整數字母量由非零數字(Non Zero Digit)+ 十進制數字(Decimal Digit)或十進制數字組(Decimal Digits)組成,非零數字是由1~9的數字組成,十進制數字是由0~9組成。

將上面的定義重新整合後,就能得到我們需要的數字字面量的定義規則:

非零數字(1~9)+十進制數字組(0~9)

需要注意的是,這是簡化版的數字字面量定義,完整版的需要加上以上規則中的所有分支條件。

表達式(Expressions)、語句(Statements)

ECMAScript標準12~13章包含了表達式和語句的相關定義,之前由詞法解析器(lexer)處理後生成的詞組流(token)交由語法解釋器(parser)處理的主要內容,即是處理詞組流構成的表達式與語句。在這裏,我們需要稍加明確一下表達式與語句之間的不同與關係:

首先,語句包含表達式,大部分語句是由關鍵字+表達式或語句組成,而表達式則是由字面量(Literal)、標識符(Identifier)、符號(Punctuators)等低一級的詞組組成;

其次,表達式一般來講會產生一個值,而語句不總有值。

理解第一點對於我們寫語法解釋器很重要,由於語句是由表達式組成的,而表達式是有詞組組成的,詞組是有詞法解析器進行解析生成的,所以,在語法解釋器中,將以表達式爲切入點,由表達式解析再深入到語句解析中。

抽象語法樹(AST)

瞭解一個parser的結構,以及parser解析語法所依賴的規則後,接下來,我們需要了解一下一個parser所生產出來的結果——抽象語法樹。在文章的開頭,我有簡單的解釋抽象語法樹即是具體代碼片段的抽象表達,那它具體是長什麼樣的呢?

function sum (a , b) {
  return a+b;
}

以上的代碼片段,AST樹的描述如下(使用babylon7-7.0.0-beta.44,結果進行了簡化):

{
      "type": "Program",
    "body": [
      {
        "type": "FunctionDeclaration",
        "id": {
          "type": "Identifier",
          "name": "sum"
        },
        "params": [
          {
            "type": "Identifier",
            "name": "a"
          },
          {
            "type": "Identifier",
            "name": "b"
          }
        ],
        "body": {
          "type": "BlockStatement",
          "body": [
            {
              "type": "ReturnStatement",
              "argument": {
                "type": "BinaryExpression",
                "left": {
                  "type": "Identifier",
                  "name": "a"
                },
                "operator": "+",
                "right": {
                  "type": "Identifier",
                  "name": "b"
                }
              }
            }
          ]
        }
      }
    ]
}

對該AST仔細觀察一番,便會明白,AST其實即是我們在已經ECMAScript標準對代碼進行解析後,將標識符(identifier)、聲明(declaration)、表達式(expression)、語句(statement)等按代碼表述的邏輯整理成爲樹狀結構。就拿上面的例子來說,當語法解析器識別了一個二元表達式(Binary Expression),便將這個二元表達式所攜帶的信息——左值,右值,操作符按照固定的計算機可讀的數據格式保存下來,即是我們看到的AST樹了。

當然,AST也需要具備固定的格式,這樣計算機才能依照該格式閱讀AST並進行接下來的編譯工作,當然,有一些AST也被用來轉義(如babel)。關於AST定義的規則,我們可以參考babel的定義,這也是後面我們實現parser時,所參考的標準。

接下來

理解完以上相關的知識,我們便具備編寫一個parser的先決條件了,那在下一章,我們將實際操作一番,編寫一個簡易版本的JavaScript語言parser。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章