動手寫一個簡單的編譯器:在JavaScript中使用Swift的尾閉包語法

首先跟你們說一下我爲何會有這個想法吧,由於最近在空閒時間學習SwiftSwiftUI的時候會常用到這種叫作尾閉包的語法,就以爲頗有趣。同時由於很早以前看過jamiebuildsthe-super-tiny-compiler,就想着能不能本身也實現一個相似的有趣好玩簡單的編譯器。因此就有了js-trailing-closure-toy-compiler這個項目,以及今天的這篇文章。javascript

對於不熟悉Swift的同窗來講,我先來解釋一下什麼是尾閉包。簡單來講,就是若是一個函數的最後一個參數也是一個函數,那麼咱們就可使用尾閉包的方式來傳遞最後一個函數。你們能夠看下面的代碼示例:html

// 例子中的 a 表示一個函數

// #1:簡單版
// Swift 的方式
a(){
  // 尾閉包的內容
}
// JavaScript 的方式
a(() => {})

// #2:函數帶參數
// Swift 的方式,這裏先忽略參數的類型
a(1, 2, 3){
  // 尾閉包的內容
}
// JavaScript 的方式
a(1, 2, 3, () => {})

// #3:尾閉包帶有參數
// Swift 的方式,這裏先忽略參數的類型
a(1, 2, 3){ arg1, arg2 in
  // 尾閉包的內容
}
// JavaScript 的方式
a(1, 2, 3, (arg1, arg2) => {})

若是關於Swift的尾閉包還有什麼疑問的話,你們能夠看一下官方的文檔Closures,裏面解釋的也很清楚。前端

我記得本身很早以前就看過the-super-tiny-compiler項目的源碼,不過當時只是簡單的看了一遍。就覺得本身掌握了裏面的一些知識和原理。可是當我想實現我本身心中的這個想法的時候。卻發現以前並無把這個項目裏面實踐的一些方法和技巧掌握好。因此我決定先好好的把這個項目的源碼看懂,而後本身先實現一個跟原來的項目功能同樣的樣例以後纔開始着手實現本身的小編譯器。java

友情提示,接下來的文章內容比較長,建議收藏後再仔細閱讀node

編譯器的實現過程

the-super-tiny-compiler咱們能夠了解到,對於通常的編譯器來講。主要有四個步驟去完成編譯的過程,這四個步驟分別是:git

  • tokenizer:將咱們的代碼文本字符串轉換成一個個有意義的單元(也就是token。好比if"hello"123letconst等等。
  • parser:將上一步獲取到的token轉換成當前語言的抽象語法樹,也就是AST( Abstract Syntax Tree )。爲何要這麼作呢?由於這樣處理以後,咱們就知道代碼中語句的前後關係和層級關係。也知道運行的順序,以及上下文等等相關的信息了。
  • transformer:將上一步獲取到的AST轉換成目標語言的AST。爲何要作這一步呢?對於相同功能的程序語句來講,若是選擇實現的語言不同,那它們的語法大機率也是不同的。這就致使了它們對應的抽象語法樹也是不同的。因此咱們須要作一次轉換,爲了下一步生成目標語言的代碼作好準備。
  • codeGenerator:這一步相對來講比較簡單,知道了目標語言的語法以後,咱們根據上一步驟生成的新的抽象語法樹,能夠方便快捷的生成咱們想要的目標語言的代碼。

上面的步驟就是編譯器的大概工做流程了,可是僅僅知道這些流程仍是不夠的,還須要咱們親自動手實踐一下。若是看到這裏你有興趣的話,能夠點擊這裏JavaScript Trailing Closure Toy Compiler先體驗一下最後實現的效果。若是你對具體的實現過程感興趣的話,能夠繼續下面的閱讀,相信看過以後你會有很大的收穫的,也許會想要本身也實現一個有趣的編譯器呢。github

Tokenizer:將代碼字符串轉換爲token

首先咱們須要明白爲何要把字符串轉換爲一個個的token,由於若是不作轉換,咱們就不知道這段程序要表示的是什麼意思,由於token是理解一段程序的必要條件。swift

這就比如console.log("hello world!")這個語句來講,咱們一眼就知道它是幹嗎的,可是咱們是怎麼思考的呢?是否是首先是console咱們知道是console對象,而後是.咱們知道是獲取對象的屬性操做符,再而後是log方法,而後方法的調用須要(左括號做爲開始,而後是hello world!字符串爲參數,而後遇到了後面的)右括號表示結束。segmentfault

代碼字符串轉換爲token

因此把字符串轉換成token就是爲了讓咱們知道這段程序要表示的是什麼意思。由於根據每個token的值,以及token所處的位置,咱們能夠準確知道這個token表示的是什麼,它有什麼做用。設計模式

那對於咱們這個編譯器來講,第一步須要把咱們所須要的token作一個劃分,那麼根據上面的代碼示例。咱們能夠知道,咱們須要的token的類型有這麼幾種:

  • 數字:好比166等。
  • 字符串:好比"hello"等。
  • 標識符:好比a,在咱們這個編譯器的環境下,通常表示函數名或者變量名。
  • 小括號(),在這裏用來表示函數的調用。
  • 花括號{},在這裏用來表示函數體。
  • 逗號,,用來分割參數。
  • 空白符 ,用來區分不一樣的token

由於咱們這個編譯器暫時只專一於咱們想要的尾閉包的實現,因此暫時只須要關注上面這些token的類型就能夠了。

這一步其實比較簡單,就是按照咱們的需求,循環讀取token,代碼部分以下所示:

// 將字符串解析爲Tokens
const tokenizer = (input) => {
    // 簡單的正則
    const numReg = /\d/;
    const idReg = /[a-z]/i;
    const spaceReg = /\s/;

    // Tokens 數組
    const tokens = [];

    // 判斷 input 的長度
    const len = input.length;
    if (len > 0) {
        let cur = 0;
        while(cur < len) {
            let curChar = input[cur];

            // 判斷是不是數字
            if (numReg.test(curChar)) {
                let num = '';
                while(numReg.test(curChar) && curChar) {
                    num += curChar;
                    curChar = input[++cur];
                }
                tokens.push({
                    type: 'NumericLiteral',
                    value: num
                });
                continue;
            }

            // 判斷是不是標識符
            if (idReg.test(curChar)) {
                let idVal = '';
                while(idReg.test(curChar) && curChar) {
                    idVal += curChar;
                    curChar = input[++cur];
                }

                // 判斷是不是 in 關鍵字
                if (idVal === 'in') {
                    tokens.push({
                        type: 'InKeyword',
                        value: idVal
                    });
                } else {
                    tokens.push({
                        type: 'Identifier',
                        value: idVal
                    });
                }
                continue;
            }

            // 判斷是不是字符串
            if (curChar === '"') {
                let strVal = '';
                curChar = input[++cur];
                while(curChar !== '"') {
                    strVal += curChar;
                    curChar = input[++cur];
                }
                tokens.push({
                    type: 'StringLiteral',
                    value: strVal
                });
                // 須要處理字符串的最後一個雙引號
                cur++;
                continue;
            }

            // 判斷是不是左括號
            if (curChar === '(') {
                tokens.push({
                    type: 'ParenLeft',
                    value: '('
                });
                cur++;
                continue;
            }

            // 判斷是不是右括號
            if (curChar === ')') {
                tokens.push({
                    type: 'ParenRight',
                    value: ')'
                });
                cur++;
                continue;
            }

            // 判斷是不是左花括號
            if (curChar === '{') {
                tokens.push({
                    type: 'BraceLeft',
                    value: '{'
                });
                cur++;
                continue;
            }

            // 判斷是不是右花括號
            if (curChar === '}') {
                tokens.push({
                    type: 'BraceRight',
                    value: '}'
                });
                cur++;
                continue;
            }

            // 判斷是不是逗號
            if (curChar === ',') {
                tokens.push({
                    type: 'Comma',
                    value: ','
                });
                cur++;
                continue;
            }

            // 判斷是不是空白符號
            if (spaceReg.test(curChar)) {
                cur++;
                continue;
            }

            throw new Error(`${curChar} is not a good character`);
        }
    }

    console.log(tokens, tokens.length);
    return tokens;
};

上面的代碼雖然不是很複雜,可是有一些須要注意的點,若是不細心很容易出錯或者進入一個死循環。下面是我以爲一些容易出現問題的地方:

  • 外層使用了while循環,每次循環開始時會首先獲取當前下標對應的字符。之因此沒有使用for循環是由於這裏關於當前字符的下標cur是由裏面的判斷來推動的,使用while更方便一些。
  • 若是讀取到字符串,數字以及標識符的話,須要進行內循環進行連續讀取,直到下一個字符不是當前想要的類型爲止。由於若是讀取的類型可能不止一個字符的話,就須要判斷下一個字符是否符合當前的類型。若是不符合的話,就終止當前類型的讀取,且須要跳出當前循環,進行下一輪的外循環。
  • 對於字符串來講,在字符串的開頭和結尾的"須要跳過,不計入字符串的值裏面。遇到空白符須要跳過

這個過程技術難度不大,須要多一點耐心。實現完成以後,咱們能夠測試一下:

tokenizer(`a(1){}`)

能夠看到輸出的結果以下:

(6) [{…}, {…}, {…}, {…}, {…}, {…}]
0: {type: "Identifier", value: "a"}
1: {type: "ParenLeft", value: "("}
2: {type: "NumericLiteral", value: "1"}
3: {type: "ParenRight", value: ")"}
4: {type: "BraceLeft", value: "{"}
5: {type: "BraceRight", value: "}"}

能夠看到輸出的結果是咱們想要的結果,到這裏咱們已經成功了25%了。接下來就是把獲得的token數組轉換爲AST抽象語法樹。

Parser:將token數組轉換爲AST抽象語法樹

接下來的步驟就是把token數組轉換爲AST(抽象語法樹)了,進行了上一個步驟以後,咱們把代碼字符串,轉變爲一個個有意義的token。當咱們獲得了這些token以後,就能夠根據每個token表示的意義進而推導出整個抽象語法樹。

好比咱們遇到了{,咱們就知道在遇到下一個}爲止,這中間的全部的token表示的是一個函數的函數體(暫時不考慮其它狀況)。

下圖所示的token示例:

token示例

表示的程序語句應該是:

a(1) {
  // block
};

那麼它所對應的抽象語法樹應該是這個樣子的:

{
 "type": "Program",
 "body": [
   {
     "type": "CallExpression",
     "value": "a",
     "params": [
       {
         "type": "NumericLiteral",
         "value": "1",
         "parentType": "ARGUMENTS_PARENT_TYPE"
       }
     ],
     "hasTrailingBlock": true,
     "trailingBlockParams": [],
     "trailingBody": []
   }
 ]
}

咱們能夠簡單的看一下上面的抽象語法樹,首先最外層的類型是Program,而後body裏面的內容就表示咱們的代碼內容。在這裏咱們的body數組只有一個元素,表示的是CallExpression,也就是一個函數調用。

這個CallExpression的函數名字是a,而後函數第一個參數類型值是NumericLiteral,數值是1。這個參數的父節點類型是ARGUMENTS_PARENT_TYPE,下面還會對這個屬性進行解釋。而後這個CallExpressionhasTrailingBlock值爲true,表示這是一個尾閉包函數調用。而後trailingBlockParams表示尾閉包沒有參數,trailingBody表示尾閉包裏面的內容爲空。

上面只是一個簡單的解釋,詳細的代碼部分以下所示:

// 將 Tokens 轉換爲 AST
const parser = (tokens) => {
    const ast = {
        type: 'Program',
        body: []
    };

    let cur = 0;

    const walk = () => {
        let token = tokens[cur];

        // 是數字直接返回
        if (token.type === 'NumericLiteral') {
            cur++;
            return {
                type: 'NumericLiteral',
                value: token.value
            };
        }

        // 是字符串直接返回
        if (token.type === 'StringLiteral') {
            cur++;
            return {
                type: 'StringLiteral',
                value: token.value
            };
        }

        // 是逗號直接返回
        if (token.type === 'Comma') {
            cur++;
            return;
        }

        // 若是是標識符,在這裏咱們只有函數的調用,因此須要判斷函數有沒有其它的參數
        if (token.type === 'Identifier') {
            const callExp = {
                type: 'CallExpression',
                value: token.value,
                params: [],
                hasTrailingBlock: false,
                trailingBlockParams: [],
                trailingBody: []
            };
            // 指定節點對應的父節點的類型,方便後面的判斷
            const specifyParentNodeType = () => {
                // 過濾逗號
                callExp.params = callExp.params.filter(p => p);
                callExp.trailingBlockParams = callExp.trailingBlockParams.filter(p => p);
                callExp.trailingBody = callExp.trailingBody.filter(p => p);

                callExp.params.forEach((node) => {
                    node.parentType = ARGUMENTS_PARENT_TYPE;
                });
                callExp.trailingBlockParams.forEach((node) => {
                    node.parentType = ARGUMENTS_PARENT_TYPE;
                });
                callExp.trailingBody.forEach((node) => {
                    node.parentType = BLOCK_PARENT_TYPE;
                });
            };
            const handleBraceBlock = () => {
                callExp.hasTrailingBlock = true;
                // 收集閉包函數的參數
                token = tokens[++cur];
                const params = [];
                const blockBody = [];
                let isParamsCollected = false;
                while(token.type !== 'BraceRight') {
                    if (token.type === 'InKeyword') {
                        callExp.trailingBlockParams = params;
                        isParamsCollected = true;
                        token = tokens[++cur];
                    } else {
                        if (!isParamsCollected) {
                            params.push(walk());
                            token = tokens[cur];
                        } else {
                            // 處理花括號裏面的數據
                            blockBody.push(walk());
                            token = tokens[cur];
                        }
                    }
                }
                // 若是 isParamsCollected 到這裏仍是 false,說明花括號裏面沒有參數
                if (!isParamsCollected) {
                    // 若是沒有參數 收集的就不是參數了
                    callExp.trailingBody = params;
                } else {
                    callExp.trailingBody = blockBody;
                }
                // 處理右邊的花括號
                cur++;
            };
            // 判斷後面緊接着的 token 是 `(` 仍是 `{`
            // 須要判斷當前的 token 是函數調用仍是參數
            const next = tokens[cur + 1];
            if (next.type === 'ParenLeft' || next.type === 'BraceLeft') {
                token = tokens[++cur];
                if (token.type === 'ParenLeft') {
                    // 須要收集函數的參數
                    // 須要判斷下一個 token 是不是 `)`
                    token = tokens[++cur];
                    while(token.type !== 'ParenRight') {
                        callExp.params.push(walk());
                        token = tokens[cur];
                    }
                    // 處理右邊的圓括號
                    cur++;
                    // 獲取 `)` 後面的 token
                    token = tokens[cur];
                    // 處理後面的尾部閉包;須要判斷 token 是否存在 考慮`func()`
                    if (token && token.type === 'BraceLeft') {
                        handleBraceBlock();
                    }
                } else {
                    handleBraceBlock();
                }
                // 指定節點對應的父節點的類型
                specifyParentNodeType();
                return callExp;
            } else {
                cur++;
                return {
                    type: 'Identifier',
                    value: token.value
                };
            }
        }

        throw new Error(`this ${token} is not a good token`);
    };

    while (cur < tokens.length) {
        ast.body.push(walk());
    }

    console.log(ast);
    return ast;
};

爲了方便你們理解,我把一些關鍵的地方都添加了一些註釋。下面再次對上面的代碼作一些簡單的解釋。

首先咱們須要對tokens數組進行遍歷,咱們首先定義了抽象語法樹的最外層的結構是:

const ast = {
  type: 'Program',
  body: []
};

這樣定義是爲了後續的節點對象可以按照必定的規則添加到咱們的抽象語法樹上。

而後咱們定義了一個walk函數用來對tokens數組中的元素進行遍歷。對於walk函數來講,若是直接遇到數字字符串逗號的話都是直接返回的。當遇到的token是一個標識符的話,須要判斷的狀況比較多。

對於一個標識符來講,在咱們這種情境下有兩種處理:

  • 一種狀況就是一個單獨的標識符,這時候標識符後面緊跟的token既不是表示(的,也不是表示{
  • 另外一種狀況表示的是一個函數的調用,對於函數的調用來講,咱們須要考慮如下這幾種狀況

    • 一種狀況是函數只有一個尾閉包,不含有其它的參數。好比a{}
    • 另外一種狀況是函數的調用不含有尾閉包,能夠含有參數也能夠不帶參數。好比a()或者a(1);
    • 最後一種就是函數的調用含有尾閉包。好比a{},a(){},a(1){}等等。對於有尾閉包的狀況還須要考慮尾部閉包有沒有參數,好比a(1){b, c in }

接下來主要對token是標識符類型的處理作一個簡單的解釋,若是判斷token的類型是標識符的話,咱們會先定義一個CallExpression類型的對象callExp,這個對象就是用來表示咱們函數調用的語法樹對象。這個對象有如下幾個屬性:

  • type:表示節點的類型
  • value:表示節點的名稱,這裏表示函數名
  • params:表示函數調用的參數
  • hasTrailingBlock:表示當前函數調用是否包含尾閉包
  • trailingBlockParams:表示尾閉包是否含有參數
  • trailingBody:尾閉包裏面的內容

接下來判斷標識符後面的token類型是什麼,若是是函數的調用的話,當前token後面的token必須是(或者是{。若是不是的話,咱們直接返回這個標識符。

若是是函數的調用,咱們須要作兩個事情,一個是收集函數調用的參數,一個是判斷函數調用後面是否是含有尾閉包。對於函數參數的收集比較簡單,首先判斷當前token後面的token是否是表示的是(,若是是的話,開始收集參數,直到遇到下一個token的類型是)表示參數收集結束。還要注意的一點是,由於參數有多是一個函數,因此咱們須要在收集參數的時候再次調用walk函數,來幫助咱們遞歸的進行參數的處理

接下來就是判斷函數調用後面是否含有尾閉包,對於尾閉包的判斷有兩種狀況須要考慮:一種就是函數的調用含有有參數,在參數的後面含有尾閉包;另外一種是函數的調用沒有參數,直接就是一個尾閉包。因此咱們須要對這兩種狀況都作一下處理

既然有兩個地方都要進行是不是尾閉包的判斷,咱們能夠把這部分的邏輯抽離到handleBraceBlock函數中,這個函數就是幫助咱們來進行尾閉包的處理。接下來來解釋一下尾閉包是如何進行處理的。

若是咱們判斷下一個token{那麼說明咱們須要進行尾閉包的處理了,咱們首先把callExp對象的hasTrailingBlock屬性的值設置爲true;而後須要判斷尾閉包是否含有參數,而且須要處理尾閉包的內部內容。

如何收集尾閉包的參數呢?咱們須要判斷在尾閉包裏面是否含有in關鍵字,若是含有in關鍵字,那就說明尾閉包裏面含有參數,若是沒有就表示尾閉包裏不含有參數,只須要處理尾閉包內部的內容便可。

又由於咱們剛開始不知道尾閉包中是否含有in關鍵字,因此咱們一開始收集的內容多是尾閉包裏面的內容,也有多是參數;因此當在遇到}尾閉包的結束token以後,這期間若是沒有in關鍵字,那說明咱們收集到的都是尾閉包的內容。

不管是收集尾閉包的參數仍是內容,咱們都須要使用walk函數來進行遞歸操做,由於參數和內容均可能不是基本的數值類型值(爲了簡化操做,咱們這裏對尾閉包的參數也使用walk來進行遞歸操做)。

在返回callExp對象以前,咱們須要使用specifyParentNodeType幫助函數額外的作一下處理。第一個處理是去掉表示,token,另外一個操做就是須要給callExp對象的paramstrailingBlockParamstrailingBody屬性中的節點指定一下父節點的類型,對於paramstrailingBlockParams來講,它們的父節點類型都是ARGUMENTS_PARENT_TYPE類型的;對於trailingBody來講,它的父節點類型是BLOCK_PARENT_TYPE類型的。這樣的處理方便咱們進行下一步的操做。在進行下面步驟講解的時候咱們會再次對其進行說明。

Transformer:將舊 AST 轉換爲目標語言的 AST

接下來就是把咱們獲取到的原始AST轉換爲目標語言的AST,那麼咱們爲何要作這一步處理呢?這是由於一樣的編碼邏輯,在不一樣的宿主語言的表現是不同的。因此咱們要把原始的AST轉換成咱們目標語言的AST

那咱們怎麼進行操做呢?原始的AST是一個樹形的結構,咱們須要對這個樹形的結構進行遍歷;遍歷須要使用深度優先的遍歷,由於對於一個嵌套的結構來講,只有將裏面的內容肯定了以後,外面的內容纔可以隨之肯定。

這裏對樹形結構的遍歷咱們會用到一種設計模式,那就是訪問者模式。咱們須要一個訪問者對象對咱們的樹形對象進行深度優先的遍歷,這個訪問者對象有針對不一樣類型節點的處理函數,當遇到一個節點的時候,咱們就會根據當前節點的類型,從訪問者對象身上獲取相應的處理函數對這個節點進行處理。

咱們首先看一下如何對原始的樹形結構進行遍歷,對於原來的樹形結構來講,每個節點要麼是一個具體類型的對象,要麼是一個數組。因此咱們要對這兩種狀況分別進行處理。咱們首先肯定如何進行樹形結構的遍歷,這部分的代碼以下所示:

// 遍歷節點
const traverser = (ast, visitor) => {
    const traverseNode = (node, parent) => {

        const method = visitor[node.type];
        if (method && method.enter) {
            method.enter(node, parent);
        }

        const t = node.type;
        switch (t) {
            case 'Program':
                traverseArr(node.body, node);
                break;
            case 'CallExpression':
                // 處理 ArrowFunctionExpression
                // TODO 考慮body 裏面存在尾部閉包
                if (node.hasTrailingBlock) {
                    node.params.push({
                        type: 'ArrowFunctionExpression',
                        parentType: ARGUMENTS_PARENT_TYPE,
                        params: node.trailingBlockParams,
                        body: node.trailingBody
                    });
                    traverseArr(node.params, node);
                } else {
                    traverseArr(node.params, node);
                }
                break;
            case 'ArrowFunctionExpression':
                traverseArr(node.params, node);
                traverseArr(node.body, node);
                break;
            case 'Identifier':
            case 'NumericLiteral':
            case 'StringLiteral':
                break;
            default:
                throw new Error(`this type ${t} is not a good type`);
        }

        if (method && method.exit) {
            method.exit(node, parent);
        }
    };
    const traverseArr = (arr, parent) => {
        arr.forEach((node) => {
            traverseNode(node, parent);
        });
    };
    traverseNode(ast, null);
};

我來簡單解釋一下這個traverser函數,這個函數內部定義了兩個函數,一個是traverseNode,一個是traverseArrtraverseArr函數的做用是,若是當前的節點是一個數組的話,咱們須要對數組裏面的每個節點分別進行處理。

節點的主要處理邏輯都在traverseNode裏面,咱們來看一下這個函數都作了哪些事情?首先根據節點的類型,從visitor對象上獲取對應節點的處理方法。而後對接點類型進行判斷,若是節點的類型是基本類型的話,就不作處理;若是節點的類型是ArrowFunctionExpression箭頭函數的話,須要依次遍歷這個節點的paramsbody屬性。若是節點的類型是CallExpression的話,表示當前的節點是一個函數調用節點,那麼咱們就須要判斷這個函數調用是否包含尾閉包,若是包含尾閉包的話,那就說明咱們原來的函數調用須要額外添加一個參數,這個參數是一個箭頭函數。因此會有下面這樣一段代碼進行判斷:

// ...
if (node.hasTrailingBlock) {
    node.params.push({
        type: 'ArrowFunctionExpression',
        parentType: ARGUMENTS_PARENT_TYPE,
        params: node.trailingBlockParams,
        body: node.trailingBody
    });
    traverseArr(node.params, node);
} else {
    traverseArr(node.params, node);
}
// ...

而後就是對這個CallExpression節點的params屬性進行遍歷。當函數的調用包含尾閉包的時候,咱們往節點的params屬性裏添加了一個類型是ArrowFunctionExpression的對象,並且這個對象的parentType的值是ARGUMENTS_PARENT_TYPE,由於這樣咱們就知道這個對象的父節點類型,方便咱們下面進行語法樹轉換時使用。

再接下來就是定義訪問者對象上面不一樣節點類型的處理方法了,具體的代碼以下:

const transformer = (ast) => {
    const newAst = {
        type: 'Program',
        body: []
    };

    ast._container = newAst.body;

    const getNodeContainer = (node, parent) => {
        const parentType = node.parentType;
        if (parentType) {
            if (parentType === BLOCK_PARENT_TYPE) {
                return parent._bodyContainer;
            }
            if (parentType === ARGUMENTS_PARENT_TYPE) {
                return parent._argumentsContainer;
            }
        } else {
            return parent._container;
        }
    };

    traverser(ast, {
        NumericLiteral: {
            enter: (node, parent) => {
                getNodeContainer(node, parent).push({
                    type: 'NumericLiteral',
                    value: node.value
                });
            }
        },
        StringLiteral: {
            enter: (node, parent) => {
                getNodeContainer(node, parent).push({
                    type: 'StringLiteral',
                    value: node.value
                });
            }
        },
        Identifier: {
            enter: (node, parent) => {
                getNodeContainer(node, parent).push({
                    type: 'Identifier',
                    name: node.value
                });
            }
        },
        CallExpression: {
            enter: (node, parent) => {
                // TODO 優化一下
                const callExp = {
                    type: 'CallExpression',
                    callee: {
                        type: 'Identifier',
                        name: node.value
                    },
                    arguments: [],
                    blockBody: []
                };
                // 給參數添加 _container
                node._argumentsContainer = callExp.arguments;
                node._bodyContainer = callExp.blockBody;
                getNodeContainer(node, parent).push(callExp);
            }
        },
        ArrowFunctionExpression: {
            enter: (node, parent) => {
                // TODO 優化一下
                const arrowFunc = {
                    type: 'ArrowFunctionExpression',
                    arguments: [],
                    blockBody: []
                };
                // 給參數添加 _container
                node._argumentsContainer = arrowFunc.arguments;
                node._bodyContainer = arrowFunc.blockBody;
                getNodeContainer(node, parent).push(arrowFunc);
            }
        }
    });
    console.log(newAst);
    return newAst;
};

咱們首先定義了新的AST的外層屬性,而後是ast._container = newAst.body,這個操做的做用是將舊的AST和新的AST最外層進行關聯,由於咱們遍歷的是舊的AST。這樣咱們就能夠經過_container屬性指向新的AST。這樣咱們向_container裏面添加元素的時候,實際上就是在新的AST上添加對應的節點。這樣處理對咱們來講相對比較簡單一點。

而後就是getNodeContainer函數,這個函數的做用就是獲取當前節點的父節點的_container屬性,若是當前節點的parentType屬性不爲空,那說明當前節點的父節點表示的多是函數調用的參數,也有多是尾閉包裏面的內容。這時能夠根據node.parentType的類型進行判斷。若是當前節點的parentType屬性爲空,那就說明當前節點的父節點的_container屬性就是父節點的_container屬性。

接下來就是visitor對象上面不一樣節點類型的處理方法了,對於基本類型仍是直接返回對應的節點就能夠了。若是是CallExpressionArrowFunctionExpression類型的話,就須要一些額外的處理了。

首先對於ArrowFunctionExpression類型節點來講,首先聲明瞭一個arrowFunc對象,而後將對應節點的_argumentsContainer屬性指向arrowFunc對象的arguments屬性;將節點的_bodyContainer屬性指向arrowFunc對象的blockBody屬性。而後獲取當前節點的父節點的_container屬性,最後將arrowFunc添加到這個屬性上。對於節點類型是CallExpression的節點的處理跟上面的相似,只不過定義的對象多了一個callee屬性,代表函數調用的函數名稱。

到此爲止將舊的AST轉換爲新的AST就完成了。

CodeGenerator:遍歷新的 AST 生成代碼

這一步就比較簡單了,根據節點的類型拼接對應類型的代碼就能夠了;詳細的代碼以下所示:

const codeGenerator = (node) => {
    const type = node.type;
    switch (type) {
        case 'Program':
            return node.body.map(codeGenerator).join(';\n');
        case 'Identifier':
            return node.name;
        case 'NumericLiteral':
            return node.value;
        case 'StringLiteral':
            return `"${node.value}"`;
        case 'CallExpression':
            return `${codeGenerator(node.callee)}(${node.arguments.map(codeGenerator).join(', ')})`;
        case 'ArrowFunctionExpression':
            return `(${node.arguments.map(codeGenerator).join(', ')}) => {${node.blockBody.map(codeGenerator).join(';')}}`;
        default:
            throw new Error(`this type ${type} is not a good type`);
    }
};

須要注意的可能就是對於CallExpressionArrowFunctionExpression節點的處理了,對於CallExpression須要添加函數的名稱,而後接下來就是函數調用的參數了。對於ArrowFunctionExpression來講,須要處理箭頭函數的參數以及函數體的內容。相比上面的三個步驟來講,這個步驟仍是相對比較簡單的。

接下來就是將這四個步驟組合一下,這個簡單的編譯器就算完成了。具體的代碼以下所示:

// 組裝
const compiler = (input) => {
    const tokens = tokenizer(input);
    const ast = parser(tokens);
    const newAst = transformer(ast);
    return codeGenerator(newAst);
};

// 導出對應的模塊
module.exports = {
    tokenizer,
    parser,
    transformer,
    codeGenerator,
    compiler
};

簡單總結

若是你有耐心看完的話,你會發現完成一個簡單的編譯器其實也沒有很複雜。咱們須要把這四個過程要作什麼理清楚,而後注意一些特殊的地方須要作特殊的處理,還有一個就是須要一點耐心了

固然咱們這個版本的實現只是簡單的完成了咱們想要的那部分功能,實際上真正的編譯器要考慮的東西是很是多的。上面這個版本的代碼有不少地方也不是很規範,當初實現的時候先考慮如何實現,細節和可維護性沒有考慮太多。若是你有什麼好的想法,或者發現了什麼錯誤歡迎給這個小項目提Issues或者Pull Request,讓這個小項目變得更好一點。也歡迎你們在文章下面留言,看看是否是能碰撞出什麼新的思路與想法。

一些同窗可能會說,學習這種東西有什麼做用?其實用途有不少,首先咱們如今前端的構建基本上離不開BabelJavaScript新特性的支持,而Babel的做用其實就是一個編譯器的做用,把咱們語言新的特性轉換成目前的瀏覽器能夠支持的一些語法,讓咱們能夠方便的使用新的語法,也減輕了前端開發的一些負擔

另外一方面你若是知道這些原理,你不只能夠很容易看懂一些Babel的語法轉換插件的源代碼,你還能夠本身親自動手實現一個簡單的語法轉換器或者一些有意思的插件。這會讓你的前端能力有一個大的提高。

時間過得好快,距離上次發佈文章已通過去兩個月了😂,這篇文章也是過完年後的第一篇文章,但願之後還可以持續的輸出一些高質量的文章。固然以前的設計模式大冒險系列還會持續更新,也歡迎你們繼續保持關注。

今天的文章到這裏就結束了,若是你對這篇文章有什麼意見和建議,歡迎在文章下面留言,或者在這裏提出來。也歡迎你們關注個人公衆號關山不難越,若是你以爲這篇文章寫的不錯,或者對你有所幫助,那就點贊分享一下吧~

參考:

相關文章
相關標籤/搜索