200行代碼實現超輕量級編譯器

前言

本篇內容主要由 the-super-tiny-compiler中的註釋翻譯而來,該項目實現了一款包含編譯器核心組成的極簡的編譯器。但願可以給想要初步瞭解編譯過程的同窗提供到一些幫助。java

概要

  1. 本篇和你們一塊兒學習寫一款超級簡單輕量,去掉註釋只有不到200行代碼的編譯器。
  2. 該編譯器將類 lisp 語法函數調用 編譯爲 類C語言函數調用
  3. 若是不熟悉上述的兩種語法的其中任意一種,下面給出了簡單的介紹
  4. 例若有兩個函數 addsubtract 他們用對應的語言分別實如餘下:
內容 類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))
  1. 本篇要實現編譯的所有語法如上所示。雖然既不涵蓋完整的lisp語法和c語法,可是足夠展現一個現代編譯器須要的主要組成部分

編譯器組成

大部分的編譯器能夠粗略的劃分爲3個階段: 解析 Parsing,翻譯 Transformation,代碼生成Code Generationnode

  1. 解析 獲取原始代碼並將其轉化爲一個更抽象的代碼表示
  2. 翻譯 用抽象的代碼表示爲編譯器想要完成的操做作準備
  3. 代碼生成 將翻譯過的抽象表示轉化爲新的要編譯的代碼

解析 Parsing


解析過程一般被分爲兩個部分: 詞法分析語法分析python

  1. 詞法分析 獲取原始代碼 ,且將代碼分割爲一個一個詞[token]

由這些詞構成的詞組用來描述語法,他們能夠是數字,文本,標點符號,運算符等等git

  1. 語法分析 獲取詞組[tokens]且將他們從新格式化爲一個表示形式,該表示形式描述語法的每一個部分及其相互之間的關係。這稱爲中間表示或抽象語法樹。

抽象語法樹(簡稱AST)是一個嵌套很深的對象,它以一種既容易使用又能告訴咱們不少信息的方式表示代碼。github

示例語法

(add 2 (subtract 4 2))
tokens表示以下express

{ 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: ')'        },
   ]

抽象語法樹表示以下

{
      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並對其進行更改。它能夠用同一種語言操縱AST,也能夠將AST翻譯成一種全新的語言。數組

讓咱們看看如何轉換AST。函數

你可能會注意到咱們的AST中有看起來很是類似的元素。這些對象具備類型屬性。每一個節點都稱爲AST節點。這些節點定義了描述樹的一個獨立部分的屬性。學習

咱們有一個數字節點 "NumberLiteral"ui

{
    type: 'NumberLiteral',
    value: '2',
}

或者一個調用表達式節點

{
     type: 'CallExpression',
     name: 'subtract',
     params: [...nested nodes here...],
  }

轉換AST時,咱們能夠經過添加/刪除/替換屬性來操縱節點,能夠添加新節點,刪除節點,也能夠不使用現有的AST直接基於它建立一個全新的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,咱們將:

  1. Program - 從AST的頂層開始
  2. CallExpression (add) - 轉到程序的第一個元素
  3. NumberLiteral (2) - 移至CallExpression參數的第一個元素
  4. CallExpression (subtract) - 移至CallExpression參數的第二個元素
  5. NumberLiteral (4) - 移至CallExpression參數的第一個元素
  6. NumberLiteral (2) - 移至CallExpression參數的第二個元素

若是咱們直接操做此AST,而不是建立單獨的AST,則可能會在這裏引入各類抽象。 可是僅訪問樹中的每一個節點就足以完成咱們要嘗試的操做。

我之因此使用「訪問」一詞,是由於存在這種模式來表示對象結構元素上的操做。

Visitors

這裏的基本思想是,咱們將建立一個「訪客」對象,該對象的方法將接受不一樣的節點類型。

var visitor = {
     NumberLiteral() {},
     CallExpression() {},
};

可是,也有可能在「退出」時調用相應的操做。 想象一下之前以列表形式的樹結構:

  • Program

    • CallExpression
    • NumberLiteral
    • CallExpression

      • NumberLiteral
      • NumberLiteral

當咱們往下遍歷時,咱們遍歷盡全部分支時。咱們「退出」它。 所以,沿着樹下來,咱們「進入[enter]」每一個節點,而後「退出[exit]」。

-> 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)

爲了支持進入和退出操做,咱們將vistitor定義調整以下

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

代碼生成


  • 編譯器的最後一步就是代碼生成.有時,編譯器會執行與轉換重疊的操做,可是在大多數狀況下,代碼生成只是意味着將AST抽象語法樹字符串化代碼化。
  • 代碼生成器以幾種不一樣的方式工做,一些編譯器將複用前面獲取的tokens,另外一些建立單獨表示,以便它們能夠線性打印節點,可是據我所知,大多數將使用咱們剛建立的AST, 這也是咱們後續主要關注的方式,
  • 綜上就是一個編譯器應該具有的核心部分.

請注意,並非說每一個編譯器看起來都和這裏描述的徹底同樣。編譯器根據目的不一樣有不少種,可能須要好比下詳細介紹的步驟更多的步驟。

代碼

如今您應該對編譯器的主要外觀有一個大體的整體瞭解。

通過上面的解釋和介紹,如今能夠開始編寫本身的編譯器了,那麼開始代碼走起。

第一步獲取token

咱們將獲取咱們的代碼串將其解析成token數組

(add 2 (subtract 4 2)) => [{ type: 'paren', value: '(' }, ...]

function tokenizer(input) {
    // current變量,用來標記當前讀入代碼的字符位置的遊標
    let current = 0;
    // tokens數組變量,用來存入解析的token詞組
    let tokens = [];

    // 開啓一個while循環,將current設置爲循環內部的增量
    while(current < input.length){
        // 獲取當前遊標對應的字符
        let char = input[current];
        // 檢查當前字符是不是一個括號
        if(char=== "("){
          // 若是是括號,則新增一個`paren`括號類型的,值爲作括號的詞到tokens詞組  
          tokens.push({
            type: 'paren',
            value: '(',
          }); 
          //而後遊標向後前進一位
          current ++;
          // 進入下一循環
          continue;
        }
        // 檢查是否右括號,如是則新增一個右括號詞組,增長遊標,繼續下一次循環
        if (char === ')') {
            tokens.push({
                type: 'paren',
                value:')'
            })
            current++;
            continue;
        }
        // 檢查當前字符是否空格,若是是空格則直接跳過,遊標後移
        //  (add 123 456)
        //       ^^^ ^^^  number
        let WHITESPACE = /\s/;
        if (WHITESPACE.test(char)) {
            current++;
            continue;
        }
        
        //下一個將檢測的類型是number數字.和以前不一樣的是number類型可能由多個數字字符組成,咱們須要
        // 獲取整個連續的數字串做爲一個number類型的詞token

        let NUMBERS = /[0-9]/;
        if(NUMBERS.test(char)){
            //新建一個value串用來設置數字字符串
            let value='';
            while(NUMBERS.test(char)){
                value += char;
                char = input[++current];
            }
            tokens.push({type:'number',value});
            continue;
        }

        // 在將要實現的編譯器中也支持被雙引號括起來的字符串
        //  (concat "foo" "bar")
        //          ^^^^  ^^^^  支付串

        if (char === '"') {
            let value = '';
            char = input[++current];
            while (char != '"') {
                value += char;
                char = input[++current]
            }
            // 遊標跳過終結的引號
            char = input[++current];
            tokens.push({
                type:'string',
                value
            })
            continue;
        }
        // 最後一個類型的token是`name`類型.由一串字母構成。該類型用做本編譯器
        // 的lisp語法風格的函數名
        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;

        }

        // 若是不匹配上述任意類型拋出類型異常,介紹循環
        throw new TypeError('I dont know what this character is: ' + char);
    }

    return tokens;
}

parse 抽象語法樹

function parser(tokens) {
    // 新建current變量做爲遊標
    let current = 0;
    // 該方法中將用遞歸代替while循環,先定義一個walk方法
    function walk() {
        //獲取當前token
        let token = tokens[current];
        // 從number類型的token開始,將不一樣類型的token置入代碼的不一樣位置
        if (token.type === 'number') {
            // 若是當前是number類型,遊標向前
            current++;
            // 返回一個number類型的AST 節點
            return {
                type: 'NumberLiteral',
                value: token.value
            }
        }
        // 字符token返回一個字符類型的AST節點
        if (token.type === 'string') {
            current++;

            return {
                type: 'StringLiteral',
                value: token.value
            }
        }
        // 下面檢查是否調用表達式.先判斷是不是一個括號類型,且是左括號token
        if (
            token.type === 'paren' && 
            token.value === '('
        ) {
            // 跳過當前左括號遊標,獲取下一個token
            token = tokens[++current];
            let node = {
                type:'CallExpression',
                name: token.value,
                params: []
            }
            // 遊標向前移一位跳過 name類型的token
            token = tokens[++current];
            // 如今開始遍歷 CallExpression的參數,直到遇到右括號
            // 這裏開始會存在遞歸,咱們經過遞歸解決嵌套節點問題。

            // 爲了解釋這一點,讓咱們採用咱們的Lisp代碼。 您能夠看到
            // add的參數是一個數字和一個包含本身的參數的嵌套的CallExpression。
            //   [
            //     { 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: ')'        }, <<< Closing parenthesis
            //     { type: 'paren',  value: ')'        }, <<< Closing parenthesis
            //   ]

            // 咱們經過遞歸調用walk方式,去向前遍歷內嵌的`CallExpression`.
            // 這裏咱們建立一個While循環遍歷直到遇到左括號
            while(
                (token.type !== 'paren')||
                (token.type === 'paren' && token.value !==')')){   
                node.params.push(walk());
                token = tokens[current];
            }
            current++;
            return node;
        }
        // 若是不是以上檢測的類型則拋出異常
        throw new TypeError(token.type);
    }
    let ast = {
        type:'Program',
        body:[]
    }
    while(current < tokens.length){
        ast.body.push(walk());
    }
    return ast;
}

轉換抽象語法樹

/***
 * ===================================
 *             ⌒(❀>◞౪◟<❀)⌒
 *           THE TRAVERSER!!!
 * ===================================
 * 如今經過parser有了一顆AST抽象語法樹,如今經過vistor訪問
 * 每個節點
 *   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) {
    function traverseArray(array, parent) {
        array.forEach(child => {
            traverseNode(child, parent);
        });
    }
    function traverseNode(node, parent) {
        let methods = visitor[node.type];

        if(methods && methods.enter){
            methods.enter(node,parent);
        }
        switch(node.type){
            case 'Program':
                traverseArray(node.body,node);
                break;
            case 'CallExpression':
                traverseArray(node.params, node);
                break;
            case 'NumberLiteral':
            case 'StringLiteral':
                break;
            default:
                throw new TypeError(node.type);                
        }
        if(methods && methods.exit) {
            methods.exit(node,parent);
        }
    }
    traverseNode(ast, null);
}

/**
 * ============================================================================
 *                                   ⁽(◍˃̵͈̑ᴗ˂̵͈̑)⁽
 *                              THE TRANSFORMER!!!
 * ============================================================================
 */

/**
 * 下一步Ast轉化. 將已經構建好的Ast樹經過visitor轉化成一顆新的Ast抽象語法樹
 *
 * ----------------------------------------------------------------------------
 *   Original AST                     |   Transformed AST
 * ----------------------------------------------------------------------------
 *   {                                |   {
 *     type: 'Program',               |     type: 'Program',
 *     body: [{                       |     body: [{
 *       type: 'CallExpression',      |       type: 'ExpressionStatement',
 *       name: 'add',                 |       expression: {
 *       params: [{                   |         type: 'CallExpression',
 *         type: 'NumberLiteral',     |         callee: {
 *         value: '2'                 |           type: 'Identifier',
 *       }, {                         |           name: 'add'
 *         type: 'CallExpression',    |         },
 *         name: 'subtract',          |         arguments: [{
 *         params: [{                 |           type: 'NumberLiteral',
 *           type: 'NumberLiteral',   |           value: '2'
 *           value: '4'               |         }, {
 *         }, {                       |           type: 'CallExpression',
 *           type: 'NumberLiteral',   |           callee: {
 *           value: '2'               |             type: 'Identifier',
 *         }]                         |             name: 'subtract'
 *       }]                           |           },
 *     }]                             |           arguments: [{
 *   }                                |             type: 'NumberLiteral',
 *                                    |             value: '4'
 * ---------------------------------- |           }, {
 *                                    |             type: 'NumberLiteral',
 *                                    |             value: '2'
 *                                    |           }]
 *  (sorry the other one is longer.)  |         }
 *                                    |       }
 *                                    |     }]
 *                                    |   }
 * ----------------------------------------------------------------------------
 */

//該方法接收類lisp抽象語法樹,轉化爲類c語言的ast樹
function transformer(ast) {
    //建立新的ast節點
    let newAst = {
        type: 'Program',
        body: []
    }
    // 將新ast樹的body做爲原ast樹的_context屬性
    ast._context = newAst.body;

    traverser(ast,{
        // 第一個接收數值類型的參數
        NumberLiteral:{
            enter(node, parent) {
                parent._context.push({
                    type: 'NumberLiteral',
                    value:node.value
                })
            }
        },
        StringLiteral:{
            enter(node, parent){
                parent._context.push({
                    type:'StringLiteral',
                    value: node.value
                })
            }
        },
        CallExpression:{
            enter(node,parent){
                let expression = {
                    type: 'CallExpression',
                    callee: {
                      type: 'Identifier',
                      name: node.name,
                    },
                    arguments: [],
                  };
          
                  // 接下來,咱們將在原CallExpression節點
                  //定義一個上下文,引用expression的參數,以便設置參數。
                  node._context = expression.arguments;
          
                  // 檢測父節點是否CallExpresssion,若是不是執行下列代碼
                  if (parent.type !== 'CallExpression') {
          
                    // 用`ExpressionStatement`節點包裹 `CallExpression`
                    // 作這一步轉換的緣由是調用表達式最終是一個語句
                    expression = {
                      type: 'ExpressionStatement',
                      expression: expression
                    };
                  }
                  parent._context.push(expression);
            }
        }
    })

    return newAst;
}

代碼生成

/**
 * 這裏開始最後一步代碼:代碼生成
 */
function codeGenerator(node) {
    switch (node.type) {
        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;
}

總結

如上即用javscript完成了一個簡單的編譯器,若是你習慣用其餘的語言如java,go,python等等,能夠嘗試改寫一下。固然以上介紹分享的內容只包含了編譯器的主要步驟,至關於一個編譯器的hello world,可是經過代碼實現有一個更直觀的感覺。後續有須要實現一些可能與編譯有關的功能能夠起到必定的幫助。

相關文章
相關標籤/搜索