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

在現今前端項目中,模塊化是一個避不開的話題。因此就會出現AMD,CMD等模塊加載方式。同時因爲JS不停的在更新迭代。出現不少實用的新語法。可是因爲有些語法有些超前,JS的宿主環境(瀏覽器/Node沒有跟上JS更新步驟),可是爲了在項目中使用這些好用到使人髮指的新特性,來提升開發效率等。就出現了各類前端編譯插件(Babel)。html

Babel is a JavaScript compiler前端

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

  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處理結果是不同的。)bash

[
    { 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.requirejs

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都被字符化。

相關文章
相關標籤/搜索