javascript編寫一個簡單的編譯器(理解抽象語法樹AST)

龍恩0707發表於2017-10-31

javascript編寫一個簡單的編譯器(理解抽象語法樹AST)

編譯器 是一種接收一段程式碼,然後把它轉成一些其他一種機制。
我們現在來做一個在一張紙上畫出一條線,那麼我們畫出一條線需要定義的條件如下:
使用 Paper定義紙的顏色,Pen定義筆的顏色,Line指畫出一條線,100指在顏色引數中代表100%的黑色 或 css中的rgb(0%,0%,0%). 那麼生成的線使用灰色來表示,那麼就是 50了,紙的面積是 100*100, 線條的寬度是1,線段的起點和終點是相對於左下角的x,y座標來定義。

Paper 0 (含義是: 定義紙的顏色是白色)
Pen 100 (含義是: 定義筆的顏色是黑色)
Line 0 50 100 50 (含義是:x軸0到100,說明是橫向從起點到終點,y軸是50到50,說明是一張紙的中點是一條直線)。

那麼編譯器是如何工作的?

編譯器一般會經過如下幾個步驟:
1. 詞法分析
2. 語法分析
3. 轉換
4. 程式碼生成

1-1 詞法分析(也可以叫做標記)
詞法分析將每個關鍵字(也可以叫標記)使用空格分開. 比如:

Paper 0
Pen 100
Line 0 50 100 50

如上,我們可以把 Paper, Pen,Line 的型別統一可以叫 word, 值就是各個單詞了 那麼 後面的數字型別我們可以統一叫 number;
比如我們輸入 "Paper 0", 那麼我們輸出的話就變成如下:

[
  {type: "word", value: "Paper"},
  {type: "number", value: "100"}
]

程式碼如下:

function lexical (code) {
  return code.split(/\s+/)
          .filter(function(t) {
            return t.length > 0
          }).map(function(t) {
            console.log(t);
            return isNaN(t) ? {type: 'word', value: t} : {type: 'number', value: t}
          });
}
var res = lexical("Paper 0");
console.log(res); // [{type: "word", value: "Paper"}, {type: "number", value: '100'}]

開啟控制檯檢視demo輸出

1-2 語法分析
語法分析是遍歷每個標記,尋找語法資訊,並且構建一個叫做AST(抽象語法樹)的物件。
下面我們對上面詞法分析生成的標記 [{type: "word", value: "Paper"}, {type: "number", value: '100'}] 這樣的資料,使用語法分析
構建一個AST(抽象語法樹)的物件。程式碼如下:

function parser(tokens) {
  var AST = {
    type: 'Drawing',
    body: []
  };
  // 迴圈依次取出第一個元素,然後刪除第一個元素
  while (tokens.length > 0) {
    var currentItem = tokens.shift();
    // 判斷型別,如果是單詞的話,我們就分析它的語法
    if (currentItem.type === 'word') {
      switch(currentItem.value) {
        case 'Paper' :
          var expression = {
            type: 'CallExpression',
            name: 'Paper',
            arguments: []
          }; 
          // 繼續陣列中欄位的型別
          var nextItem = tokens.shift();
          if (nextItem.type === 'number') {
            // 在expression物件內部加入引數資訊
            expression.arguments.push({
              type: 'NumberLiteral',
              value: nextItem.value
            })
            // 將expression物件放入我們的AST的body內
            AST.body.push(expression);
          } else {
            throw 'Paper command must be followed by a number.'
          }
          break;
        case 'Pen' : 
          /* 更多程式碼 */
          break;
        case 'Line': 
          /* 更多程式碼 */
          break;
      }
    }
  }
  return AST;
}
var data = [
  { type: 'word', value: 'Paper'},
  { type: 'number', value: 100}
];
var output = parser(data);
console.log(output);  
// 列印資訊如下
/*
 var output = {
   'type': 'Drawing',
   'body': [{
      "type": "CallExpression",
      "name": "Paper",
      "arguments": [{
        "type": "NumberLiteral",
        "value": "100"
      }]
   }]
 }
*/

開啟控制檯檢視demo輸出

1-3 轉換器函式
我們在語法分析上面通過詞法分析生成的物件後,在語法分析建立了一個AST(抽象語法樹)結構,但是上面的AST結構對我們建立SVG檔案沒有什麼用處,
在SVG中,我們可以使用元素(element)來表示一個Paper。那麼轉換器函式將AST轉換成另一種對SVG友好的AST。程式碼如下:

function transformer(ast) {
  var svg_ast = {
    tag: 'svg',
    attr: {
      width: 100,
      height: 100,
      viewBox: '0 0 100 100',
      xmlns: 'http://www.w3.org/2000/svg',
      version: '1.1'
    },
    body: []
  };
  // 迴圈呼叫ast表示式
  while (ast.body.length > 0) {
    // 依次取出陣列的第一個元素,然後在陣列中刪除該元素
    var node = ast.body.shift();
    switch (node.name) {
      case 'Paper' :
        var paper_color = 100 - node.arguments[0].value;
        // 在svg_ast的body內加入rect元素資訊
        svg_ast.body.push({
          tag: 'rest',
          attr: {
            x: 0,
            y: 0,
            width: 100,
            height: 100,
            fill: 'rgb(' + paper_color + '%,' + paper_color + '%,' + paper_color + '%)'
          }
        })
        break;

      case 'Pen' :
        var pen_color = 100 - node.arguments[0].value;
        /* 很多程式碼 */
        break;

      case 'Line' : 
        /* 很多程式碼 */
        break;
    }
  }
  return svg_ast;
}
var inputElem = {
  'type': 'Drawing',
   'body': [{
      "type": "CallExpression",
      "name": "Paper",
      "arguments": [{
        "type": "NumberLiteral",
        "value": "100"
      }]
   }]
};
var output = transformer(inputElem);
console.log(output);
 /*
  列印資訊如下:
  var output = {
    "tag": "svg",
    "attr": {
      "width": 100,
      "height": 100,
      "viewBox": "0 0 100 100",
      "xmlns": "http://www.w3.org/2000/svg",
      "version": "1.1"
    },
    "body": [{
      "tag": "rect",
      "attr": {
        "x": 0,
        "y": 0,
        "width": 100,
        "height": 100,
        "fill": "rgb(0%, 0%, 0%)"
      }
    }]
  }
 */

開啟控制檯檢視效果

1-4 生成器函式
作為編譯器的最後一步,生成器函式基於上一步產生的新AST來生成SVG程式碼。
程式碼如下:

function generator(svg_ast) {
   /*
    從attr 物件中建立屬性字串
    {"width": 100, "height": 100} => 'width="100"  height="100"'
   */
   function createAttrString(attr) {
     return Object.keys(attr).map(function(key) {
       return key + '="'+attr[key]+'"'
     }).join(' ');
   }
   // 為svg標籤建立屬性字串
   var svg_attr = createAttrString(svg_ast.attr);

   // width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" version="1.1"
   // console.log(svg_attr);  

   // 為每個 svg_ast body中的元素,生成svg標籤
   var elements = svg_ast.body.map(function(node) {
     return '<' + node.tag + ' ' + createAttrString(node.attr) + '></' + node.tag + '>'
   }).join('\n\t');
   // 使用開和關的svg標籤包裝來完成svg程式碼
   return '<svg '+ svg_attr +'>\n' + elements + '\n</svg>'
 }
 var svg_ast = {
   "tag": "svg",
   "attr": {
     "width": 100,
     "height": 100,
     "viewBox": "0 0 100 100",
     "xmlns": "http://www.w3.org/2000/svg",
     "version": "1.1"
    },
    "body": [{
      "tag": "rect",
      "attr": {
        "x": 0,
        "y": 0,
        "width": 100,
        "height": 100,
        "fill": "rgb(0%, 0%, 0%)"
      }
    }]
 }
 var g = generator(svg_ast);
 console.log(g);
/* 
  列印輸出如下:
  <svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" version="1.1">
    <rect x="0" y="0" width="100" height="100" fill="rgb(0%, 0%, 0%)"></rect>
  </svg>
 */

開啟控制檯檢視效果

1-5 把上面的 lexical.js , grammar.js, converter.js 和 generator.js 組裝在一起,來作為一個編譯器。
我們把這個編譯器取個名字叫 svgCompile 編譯器吧,我們逐步理解了編譯器的步驟,先是建立 詞法分析器,然後建立 語法分析器,接著是 轉換器,最後就是生成器方法,現在我們需要新增一個 compile(編譯)方法來鏈式呼叫這四個方法。

 function svgCompile(code) {
  return generator(transformer(parser(lexical(code))));
 }

下面是compile.html程式碼如下:

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script src="./js/lexical.js"></script>
    <script src="./js/parser.js"></script>
    <script src="./js/transformer.js"></script>
    <script src="./js/generator.js"></script>
    <script src="./js/compile.js"></script>
  </head>
  <body>
    <script>
      // 呼叫svgCompile編譯器
      var code = 'Paper 0 Pen 100 Line 0 50 100 50';
      var svg = svgCompile(code);
      console.log(svg);
      document.body.innerHTML = svg;
      /*
       列印資訊如下:
       <svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" version="1.1">
         <rest x="0" y="0" width="100" height="100" fill="rgb(100%,100%,100%)"></rest>
          <line x1="0" y1="50" x2="100" y2="50" stroke-linecap="round" stroke="rgb(0%,0%,0%)"></line>
       </svg>
       */
    </script>
  </body>
</html>

下面就是實現使用svg 畫一條線的demo.

檢視效果:

git上的程式碼

Tips: 通過網上的資料學習的,關鍵是想先來理解如何編寫一個簡單的編譯器,及簡單的理解AST(抽象語法樹)

相關文章