編譯程式(compiler)的簡單分析

北宸南蓁發表於2018-08-07

在現今前端專案中,模組化是一個避不開的話題。所以就會出現AMD,CMD等模組載入方式。同時由於JS不停的在更新迭代。出現很多實用的新語法。但是由於有些語法有些超前,JS的宿主環境(瀏覽器/Node沒有跟上JS更新步驟),但是為了在專案中使用這些好用到令人髮指的新特性,來提高開發效率等。就出現了各種前端編譯外掛(Babel)。

Babel is a JavaScript compiler

大多數編譯程式(compiler)分為三個步驟:Parsing(分析階段)/Transformation(轉換)/Code Generation(程式碼生成或者說生成目的碼)

  1. Parsing將原始碼(raw code)轉換為AST(抽象語法樹)。
  2. Transformation接收Parsing生成的AST,並且按照compiler內定的規則進行程式碼的轉換。
  3. Code Generation 接受被compiler轉換過的程式碼,按照一定的規則將程式碼轉換為最終想要輸出的程式碼格式。 現在有一個場景: 我們將一些LISP(高階計算機程式語言)方法通過compiler轉碼為C語言(通用計算機程式語言)的方法。 假如我們有'add'和'subtract'方法

自然語言 LISP C
2+2 (add 2 2) add(2,2)
4-2 (subtract 4 2) subtract(4,2)
2 + (4 - 2) (add 2 (subtract 4 2)) add(2, subtract(4, 2))

Parsing(語法解析)

Parsing 一般被分成兩個步驟:Lexical Analysis(詞法分析)和 Syntactic Analysis(語法分析)

  1. Lexical Analysis 接受raw code 同時通過tokenizer(標記器)或者lexer(詞法分析器)將raw code 拆解為許多tokens。Tokens 是一系列描述獨立的語法的物件。他們可以是數字,標籤,標點符號,操作符等
  2. Syntactic Analysis 接收LA處理過的tokens並且將他們重新構建為能夠描述每一個語法代表什麼含義並且描繪每個語法之間是如何關聯的樹行結構-----將每一個token視為一個Node結點,各個token之間存在的關聯視為"樹枝",從而會構建一個能夠表明各個token含義同時各個token之間關係的樹形結構-------Abstract Syntax Tree(AST)。

AST 是一個層級很深的物件。


e.g. 對(add 2 (subtract 4 2))進行Parsing處理。 Tokens如下(Note:其實Token是根據lexer生成的,不同的lexer處理結果是不一樣的。)

[
    { type: 'paren',  value: '('        },
    { type: 'name',   value: 'add'      },
    { type: 'number', value: '2'        },
    { type: 'paren',  value: '('        },
    { type: 'name',   value: 'subtract' },
    { type: 'number', value: '4'        },
    { type: 'number', value: '2'        },
    { type: 'paren',  value: ')'        },
    { type: 'paren',  value: ')'        },
]
複製程式碼

對應的Abstract Syntax Tree (AST) 可能如下

{
      type: 'Program',
      body: [{
        type: 'CallExpression',
        name: 'add',
        params: [{
          type: 'NumberLiteral',
          value: '2',
        }, {
          type: 'CallExpression',
          name: 'subtract',
          params: [{
            type: 'NumberLiteral',
            value: '4',
          }, {
            type: 'NumberLiteral',
            value: '2',
          }]
        }]
      }]
    }

複製程式碼

Transformation(AST轉換)

transformation是compiler的第二個階段。他會接收經過SA處理生成的AST。在該階段能夠利用一些語法規則,將AST轉換為想被轉換的語言。

通過觀察AST會發現,每一個elements(從AST角度看)或者token(從LA角度看)都有一個type屬性。這些element是屬於AST的Node結點。這些nodes通過對type屬性賦特定的值將AST劃分成各自獨立的區塊。

e.g.

NumberLiteral 型別的Node

{

 type: 'NumberLiteral',

 value: '2',

 }
複製程式碼

CallExpression 型別的Node

{

 type: 'CallExpression',

 name: 'subtract',

 params: [...內嵌的node邏輯...],

 }
複製程式碼

在transforming AST過程中,我們可以通過adding/removing/replacing 屬性來修改nodes,同時我們可以add/remove nodes,甚至我們可以基於現有的AST來重新構建新的AST物件。

由於我們是需要將LISP語法的程式碼轉換為C的,所以我們的關注點就是基於SA輸出的AST構建一個全新的適用於目標語言的AST物件。


Traversal(遍歷)

為了能夠在transforming過程中檢測這些nodes。同時由於AST是一個層級很深的物件樹,所以需要對AST進行depth-first深度優先遍歷)。(其實這和React在Render階段是一樣的)

{
      type: 'Program',
      body: [{
        type: 'CallExpression',
        name: 'add',
        params: [{
          type: 'NumberLiteral',
          value: '2',
        }, {
          type: 'CallExpression',
          name: 'subtract',
          params: [{
            type: 'NumberLiteral',
            value: '4',
          }, {
            type: 'NumberLiteral',
            value: '2',
          }]
        }]
      }]
    }
複製程式碼

對於上述的AST,在traversal階段,範圍每個node的先後順序如下

  1. Program - Starting at the top level of the AST
  2. CallExpression (add) - Moving to the first element of the Program's body
  3. NumberLiteral (2) - Moving to the first element of CallExpression's params
  4. CallExpression (subtract) - Moving to the second element of CallExpression's params
  5. NumberLiteral (4) - Moving to the first element of CallExpression's params
  6. NumberLiteral (2) - Moving to the second element of CallExpression's params

Visitors(遊標)

為了用程式碼實現traversal過程,我們構建一個內建能夠接收不同node型別函式的"visitor"物件。

var visitor = {
      NumberLiteral() {},
      CallExpression() {},
    };
複製程式碼

當在遍歷AST的時候,在我們訪問對應的node結點時,就會觸發與之型別匹配的visitor中的方法。

如果只是單純的在訪問結點的時候觸發對應的方法,這種情況是無法紀錄訪問的"軌跡",所以需要對visitor進行改進。傳入被訪問的node結點,還有該node的直接父級結點。

var visitor = {
      NumberLiteral(node, parent) {},
      CallExpression(node, parent) {},
    };
複製程式碼

如果沒有返回處理,"遊標"在遍歷到最後的node就會停止,因為他不知道下一步該如何進行。

 - Program
      - CallExpression
        - NumberLiteral
        - CallExpression
          - NumberLiteral
          - NumberLiteral

複製程式碼

由於在遍歷AST的過程中是採用depth-first的方式,就需要在訪問到最後的node的時候,需要按照原路返回,直到返回到起點,這樣才能被程式識別,這顆樹被遍歷完成了。

-> Program (enter)
      -> CallExpression (enter)
        -> Number Literal (enter)
        <- Number Literal (exit)
        -> Call Expression (enter)
           -> Number Literal (enter)
           <- Number Literal (exit)
           -> Number Literal (enter)
           <- Number Literal (exit)
        <- CallExpression (exit)
      <- CallExpression (exit)
    <- Program (exit)

複製程式碼

為了實現上述邏輯,需要對visitor做額外的處理

 var visitor = {
      NumberLiteral: {
        enter(node, parent) {},
        exit(node, parent) {},
      },
      CallExpression:{
        enter(node, parent){},
        exit(node, parent){},
      },
    };

複製程式碼

Code Generation(生成指定格式的程式碼)

compiler的最後階段是code generation。有些compiler在CG階段做的工作會和transformation的重疊,但是大部分的CG的工作就是接收被處理過的AST然後將AST物件字元化(該操作類似於JSON.stringify(Object))。 一個高效的CG是能夠根據AST不同的node type輸出對應的code,同時能夠在樹內進行遞迴呼叫直到所有的node都被字元化。

相關文章