程式碼來構建一個簡單的compiler

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

關於compiler原理可以參考這篇文章。或者微信搜尋訂閱號'北宸南蓁'或者搜尋wDxKn89。

Parsing

我們來構建一個tokenizer用於進行lexical analysis(詞法分析)

設計思路

我們向tokenizer中傳遞需要轉換的字元型別的code,並且將code通過一些分解規則,拆分為tokens陣列。

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

tokenizer程式碼實現

function tokenizer(input) {

  //該'current'變數用於追蹤程式碼走到哪裡,可以類比SQL中的遊標(https://blog.csdn.net/DreamLLOver/article/details/51523887)
  let current = 0;

  //用於存放tokens的陣列
  let tokens = [];

  //通過判斷'current'與input.length的長度來控制迴圈次數和對tokens的處理次數
  while (current < input.length) {

    //取出指定遊標下需要處理的code.
    let char = input[current];
    //首先我們向要校驗是否是'(',之後會被'CallExpression'使用,但是我們現在只關心這個字元是什麼。
   
    if (char === '(') {

   
      //如果滿足條件,我們向token陣列push一個type值為'parn',同時value為'('的token物件。
      tokens.push({
        type: 'paren',
        value: '(',
      });

      // 更新遊標的值。
      current++;

      // 繼續處理剩餘的code
      continue;
    }

    //道理同上
    if (char === ')') {
      tokens.push({
        type: 'paren',
        value: ')',
      });
      current++;
      continue;
    }
 
  
    //由於在程式碼中會存在空格/tab等製造的空格,同時我們在構建token的時候,是不必要關注空格的,因為空格本身對程式碼執行沒有任何影響。
    let WHITESPACE = /\s/;
    if (WHITESPACE.test(char)) {
      current++;
      continue;
    }

    //由於我們實現的是基於兩個數字引數的轉換,對於一些非數字會有其他匹配規則或者類似'12sdsd4'是不滿足執行規則的,所以需要挑選出滿足條件的數字token
    let NUMBERS = /[0-9]/;
    if (NUMBERS.test(char)) {

      //用於存放數字字元
      let value = '';

      //然後會不停的去loop code 序列
      while (NUMBERS.test(char)) {
        value += char;
        char = input[++current];
      }

      // 將'number'token存放到tokens中
      tokens.push({ type: 'number', value });

      continue;
    }

   
    //同時為了滿足compiler的多樣性,我們還支援String的轉換,只要滿足字串被double quotes 包裹
    if (char === '"') {
      
      let value = '';

      // 跳過引號
      char = input[++current];

      //紀錄String
        value += char;
        char = input[++current];
      }

      // 跳過引號
      char = input[++current];

      // 新增StringToken
      tokens.push({ type: 'string', value });

      continue;
    }

    
    //最後我們需要處理function name,用於標識操作動作
    let LETTERS = /[a-z]/i;
    if (LETTERS.test(char)) {
      let value = '';

      while (LETTERS.test(char)) {
        value += char;
        char = input[++current];
      }

      tokens.push({ type: 'name', value });

      continue;
    }
    //最後如果code沒有滿足條件說明,該code是一個'髒code'
    throw new TypeError('I dont know what this character is: ' + char);
  }

//最後返回處理過的tokens
  return tokens;
}
複製程式碼

Parser的程式碼實現

我們已經有了tokenizer(已經將raw code格式化為tokens array),既然烹飪材料已經有了,就需要對'材料'進行進一步加工,從而轉換為AST。

 [{ type: 'paren', value: '(' }, ...] => { type: 'Program', body: [...] }
複製程式碼

我們定義一個接收tokens陣列的'parser'函式

function parser(tokens) {
  //與處理raw code 的方式一樣,需要一個"遊標"來跟蹤程式碼的執行軌跡
  let current = 0;
  //在處理raw code生成tokens是用的while對需要處理的char來根據不同的處理規格進行token的生成,現在我們處理token的時候,完全可以構建一個函式,這樣能夠使得程式碼更加的清晰
  function walk() {
   //獲取tokens陣列對應current下標的token物件
    let token = tokens[current];
    //我們根據用於標識token型別的type來進行token的分類
    if (token.type === 'number') {
      //更新遊標
      current++;
      //構建AST node結點,type是需要事先按照一定的規則進行賦值,value是token的value
      return {
        type: 'NumberLiteral',
        value: token.value,
      };
    }

    // 處理type為string的token,用於生成滿足對應type的AST node
    if (token.type === 'string') {
      current++;

      return {
        type: 'StringLiteral',
        value: token.value,
      };
    }
   //根據案例code,我們是將LISP的函式轉換為C語言的,根據LISP函式的特點,'('代表函式的開始,所以我們需要處理type為paren(括號)同時值為'('的token,用於構建一個代表函式的AST node。
    if (
      token.type === 'paren' &&
      token.value === '('
    ) {
     //這一步需要額外的注意,我們通過處理tokens用於構建對應的AST,我們只關心函式名是什麼,引數是什麼,而不關心'('或者')'這些不具備函式特性的東西,它們只是標識一個函式的函式開始和結束
      token = tokens[++current];
      //構建一個type為CallExpression Base node ,我們將raw code的函式名作為該node的name的值。同時params用於存放函式內部的引數或者是子函式的nodes.
      let node = {
        type: 'CallExpression',
        name: token.value,
        params: [],
      };

      // 剛才如果是處理'(',在處理完之後,current還是指向函式名,所以需要將遊標更新到最新
      token = tokens[++current];

     //由於raw code的形式是變化不定的,所以如果只是根據tokens.length來進行while處理是遠遠不夠,也不正確的,因為可能在LISP的一個函式中可能內嵌n多個子函式。
    
      //   (add 2 (subtract 4 2))
      //
       // 下面的程式碼中存在多個')',這種情況在parser的過程中是不可預知的。
      //   [
      //     { 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: ')'        }, <<< )最外層的)
      //   ]
      //所以,我們是不能通過簡單的迴圈來對tokens進行AST化,但是我們可以利用walk函式,進行遞迴呼叫,來讓walk來處理內嵌CallExpression的情況,只要控制好停止條件就可以。
      //通過while來控制遞迴是否終止
      while (
        (token.type !== 'paren') ||
        (token.type === 'paren' && token.value !== ')')
      ) {
        //呼叫walk(),將返回的node push到用於存放AST樹形結構的node.params中。
        node.params.push(walk());
        token = tokens[current];
      }
       //更新遊標,跳出tokens的資料範圍
      current++;

      // 返回處理之後的node結點。
      return node;
    }


    // 容錯處理.
    throw new TypeError(token.type);
  }

 
  //構建type為Program的root node。
  let ast = {
    type: 'Program',
    body: [],
  };
  
  //呼叫walk()來處理tokens,然後將處理之後的結果存放到AST root node的body屬性中。
  while (current < tokens.length) {
    ast.body.push(walk());
  }

  // 返回根據tokens處理過的AST物件。
  return ast;
}
複製程式碼

Transformation

通過Parsing對raw code的處理,生成對應的AST。但是我們需求是對AST進行處理來生成目標AST。但是現在有一個問題,如何才能遍歷這些樹形結構,總不能用while來處理,同時也不能保證node資訊的被按訪問順序的紀錄。
所以我們需要能利用visitor(針對樹形結構的遊標)來構建一個訪問AST同時按照訪問順序紀錄一些node資訊的"遍歷器"。

traverser的程式碼實現

大致思路,traverse接收ast,還有不同型別的visitor。

traverse(ast, {
      Program: {
        enter(node, parent) {
          // ...
        },
        exit(node, parent) {
          // ...
        },
      },
 
      CallExpression: {
        enter(node, parent) {
          // ...
        },
        exit(node, parent) {
          // ...
        },
      },
 
      NumberLiteral: {
        enter(node, parent) {
          // ...
        },
        exit(node, parent) {
          // ...
        },
      },
    });
複製程式碼

真實程式碼

function traverser(ast, visitor) {

  //該函式用於處理node.body/node.params,處理頂層的樹結構,將child剝離出來,然後進行traverseNode處理
  function traverseArray(array, parent) {
    array.forEach(child => {
      traverseNode(child, parent);
    });
  }

  //接收需要被處理的'node'結點,同時將該'node'的直接parent的結點傳入
  function traverseNode(node, parent) {

    //根據被遍歷的node結點的type,從visitor中獲取到對應type的處理物件
    let methods = visitor[node.type];

    //如果該處理物件存在同時有enter方法(也就是visitor匹配了node type了),
    //然後將node,node 的直接parent結點傳入,進行下一步處理
    if (methods && methods.enter) {
      methods.enter(node, parent);
    }
    
    //通過node type來進行不同的操作處理
    switch (node.type) {

      //從AST的頂層入口,頂層的type為`Program`,同時樹形的關聯關係和邏輯都被存放在body屬性中,所以我們需要對body進行traverseArray處理,由於traverseArray()內部呼叫traverseNode(),所以會對body內部所有的child進行遞迴traverseNode()處理。
      case 'Program':
        traverseArray(node.body, node);
        break;

      //對AST中的CallExpression進行處理
      case 'CallExpression':
        traverseArray(node.params, node);
        break;
        
      //由於`NumberLiteral` 和`StringLiteral`沒有任何child node,所以不需要進行traverseArray()處理,直接跳過
      case 'NumberLiteral':
      case 'StringLiteral':
        break;

      //容錯處理
      default:
        throw new TypeError(node.type);
    }

    //由於遍歷AST是採用depth-first,所以需要在處理完所有child的時候,進行推出操作
    if (methods && methods.exit) {
      methods.exit(node, parent);
    }
  }

  //我們通過啟動traverseNode()來觸發遍歷操作,由於頂層的AST是不存在parent node,所以直接傳入null
  traverseNode(ast, null);
}
複製程式碼

transformer

通過構建了traverser(),我們現在有能力可以對AST進行有目的的遍歷,同時還可以保證他們直接存在的原有關聯不被破壞。接下來,我們就需要利用traverser()來對AST進行有目的的改造(生成新的AST)。

原始的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'
          }]
        }]
      }]
    }
複製程式碼

轉換後的AST

{
    type: 'Program',
        body: [{
            type: 'ExpressionStatement',
            expression: {
                type: 'CallExpression',
                callee: {
                    type: 'Identifier',
                    name: 'add'
                },
                arguments: [{
                    type: 'NumberLiteral',
                    value: '2'
                }, {
                    type: 'CallExpression',
                    callee: {
                        type: 'Identifier',
                        name: 'subtract'
                    },
                    arguments: [{
                        type: 'NumberLiteral',
                        value: '4'
                    }, {
                        type: 'NumberLiteral',
                        value: '2'
                    }]
                }
             }
        }]
}
複製程式碼

transformer的程式碼實現(接收lisp ast)

function transformer(ast) {

  //首先先構建一個 newAst和lisp ast擁有相同的program node
  let newAst = {
    type: 'Program',
    body: [],
  };

  //我們採用直接在old ast中設定一個context(或者說在每一級的parent中設定一個用於接收處理過的AST),context 是從old ast轉換為new ast的引用
  ast._context = newAst.body;
  
  //呼叫traverser對ast在特定的visior下針對滿足條件的node 結點進行處理。
  traverser(ast, {

    // 處理type為 `NumberLiteral`的node,
    NumberLiteral: 
      enter(node, parent) {
      
        //重新構建了一個type'NumberLiteral'並push到用於紀錄樹的關聯關係的context中。
        parent._context.push({
          type: 'NumberLiteral',
          value: node.value,
        });
      },
    },

    // 構建處理 `StringLiteral`
    StringLiteral: {
      enter(node, parent) {
        parent._context.push({
          type: 'StringLiteral',
          value: node.value,
        });
      },
    },

    // 處理 `CallExpression`.
    CallExpression: {
      enter(node, parent) 
      
        //構建一個新增了內建Identifier的node 結點
        let expression = {
          type: 'CallExpression',
          callee: {
            type: 'Identifier',
            name: node.name,
          },
          arguments: [],
        };

        
        /在原始的expression node中新增一個context,用於存放函式的引數nodes
        node._context = expression.arguments;

       
        //判斷parent node的type是否是'CallExpression'(可能會存在如下的lisp (add substricl(2,3),但是是不滿足情況的,由於在ast生成的階段只是根據type來構建,沒有進行語法的校驗)
        if (parent.type !== 'CallExpression') {

          //我們將`CallExpression`node 包裝在ExpressionStatement中。(CallExpression在JS中保留宣告)
          expression = {
            type: 'ExpressionStatement',
            expression: expression,
          };
        }

        
        //將處理之後的node更新到parent的context中
        parent._context.push(expression);
      },
    }
  });


  //返回處理之後的ast
  return newAst;
}
複製程式碼

Code Generator

我們採用遞迴呼叫code generator將new ast中的node字元化。 codeGenerator的程式碼實現

function codeGenerator(node) {

 // 根據type來區分不同的輸出處理
 switch (node.type) {

   //如果遇到`Program` node,將body的陣列中的item通過再次呼叫codeGenerator來進行類輸出,(可以認為是遞迴呼叫)
   case 'Program':
     return node.body.map(codeGenerator)
       .join('\n'
       //處理
   case 'ExpressionStatement':
     return (
       codeGenerator(node.expression) +
       ';' // 為了程式碼格式更加的符合開發規範
     );

   //處理函式
   case 'CallExpression':
     return (
       codeGenerator(node.callee) +
       '(' +
       node.arguments.map(codeGenerator)
         .join(', ') +
       ')'
     );

   // 其實是返回了函式名
   case 'Identifier':
     return node.name;

   // 直接返回資料
   case 'NumberLiteral':
     return node.value;

   // 處理字串
   case 'StringLiteral':
     return '"' + node.value + '"';

   // 容錯處理
   default:
     throw new TypeError(node.type);
 }
}


複製程式碼

程式碼回顧

最後我們構建了一個compiler函式。他們直接的互動順序和方式如下:

  1. input(資料來源) => tokenizer => tokens
  2. tokens => parser => ast
  3. ast => transformer => newAst
  4. newAst => generator => output(目標)
function compiler(input) {
  let tokens = tokenizer(input);
  let ast    = parser(tokens);
  let newAst = transformer(ast);
  let output = codeGenerator(newAst);

  
  return output;
}

複製程式碼

我們也可以將這些方法進行匯出

module.exports = {
  tokenizer,
  parser,
  traverser,
  transformer,
  codeGenerator,
  compiler,
};
複製程式碼

相關文章