實現一個簡單的編譯器

簡單的說 編譯器 就是語言翻譯器,它通常將高級語言翻譯成更低級的語言,如 GCC 可將 C/C++ 語言翻譯成可執行機器語言,Java 編譯器能夠將 Java 源代碼翻譯成 Java 虛擬機能夠執行的字節碼。html

編譯器如此神奇,那麼它究竟是如何工做的呢?本文將簡單介紹編譯器的原理,並實現一個簡單的編譯器,使它能編譯咱們自定義語法格式的源代碼。(文中使用的源碼都已上傳至 GitHub 以方便查看)。c++

自定義語法

爲了簡潔易懂,咱們的編譯器將只支持如下簡單功能:git

  • 數據類型只支持整型,這樣不須要數據類型符;github

  • 支持 加(+)減(-)乘(*)除(/) 運算正則表達式

  • 支持函數調用ubuntu

  • 支持 extern(爲了調用 printf 打印計算結果)bash

如下是咱們要支持的源碼實例 demo.xy框架

extern printi(val)

sum(a, b) {
  return a + b
}

mult(a, b) {
  return a * b
}

printi(mult(4, 5) - sum(4, 5))

編譯原理簡介

通常編譯器有如下工做步驟:ide

  1. 詞法分析(Lexical analysis): 此階段的任務是從左到右一個字符一個字符地讀入源程序,對構成源程序的字符流進行掃描而後根據構詞規則識別 單詞(Token),完成這個任務的組件是 詞法分析器(Lexical analyzer,簡稱Lexer),也叫 掃描器(Scanner)函數

  2. 語法分析(Syntactic analysis,也叫 Parsing): 此階段的主要任務是由 詞法分析器 生成的單詞構建 抽象語法樹(Abstract Syntax Tree ,AST),完成此任務的組件是 語法分析器(Parser)

  3. 目標碼生成: 此階段編譯器會遍歷上一步生成的抽象語法樹,而後爲每一個節點生成 機器 / 字節碼

編譯器完成編譯後,由 連接器(Linker) 將生成的目標文件連接成可執行文件,這一步並非必須的,一些依賴於虛擬機運行的語言(如 Java,Erlang)就不須要連接。

工具簡介

對應編譯器工做步驟咱們將使用如下工具,括號裏標明瞭所使用的版本號:

  • Flex(2.6.0): Flex 是 Lex 開源替代品,他們都是 詞法分析器 製做工具,它能夠根據咱們定義的規則生成 詞法分析器 的代碼;

  • Bison(3.0.4) Bison 是 語法分析器 的製做工具,一樣它能夠根據咱們定義的規則生成 語法分析器 的代碼;

  • LLVM(3.8.0) LLVM 是構架編譯器的框架系統,咱們會利用他來完成從 抽象語法樹 生成目標碼的過程。

在 ubuntu 上能夠經過如下命令安裝這些工具:

sudo apt-get install flex
sudo apt-get install bison
sudo apt-get install llvm-3.8*

介紹完工具,如今咱們能夠開始實現咱們的編譯器了。

詞法分析器

前面提到 詞法分析器 要將源程序分解成 單詞,咱們的語法格式很簡單,只包括:標識符,數字,數學運算符,括號和大括號等,咱們將經過 Flex 來生成 詞法分析器 的源碼,給 Flex 使用的規則文件 lexical.l 以下:

%{
#include <string>
#include "ast.h"
#include "syntactic.hpp"

#define SAVE_TOKEN  yylval.string = new std::string(yytext, yyleng)
#define TOKEN(t)    (yylval.token = t)
%}

%option noyywrap

%%

[ \t\n]                 ;
"extern"                return TOKEN(TEXTERN);
"return"                return TOKEN(TRETURN);
[a-zA-Z_][a-zA-Z0-9_]*  SAVE_TOKEN; return TIDENTIFIER;
[0-9]+                  SAVE_TOKEN; return TINTEGER;

"="                     return TOKEN(TEQUAL);
"=="                    return TOKEN(TCEQ);
"!="                    return TOKEN(TCNE);

"("                     return TOKEN(TLPAREN);
")"                     return TOKEN(TRPAREN);
"{"                     return TOKEN(TLBRACE);
"}"                     return TOKEN(TRBRACE);

","                     return TOKEN(TCOMMA);

"+"                     return TOKEN(TPLUS);
"-"                     return TOKEN(TMINUS);
"*"                     return TOKEN(TMUL);
"/"                     return TOKEN(TDIV);

.                       printf("Unknown token!\n"); yyterminate();

%%

咱們來解釋一下,這個文件被 2 個 %% 分紅 3 部分,第 1 部分用 %{%} 包括的是一些 C++ 代碼,會被原樣複製到 Flex 生成的源碼文件中,還能夠在指定一些選項,如咱們使用了 %option noyywrap,也能夠在這定義宏供後面使用;第 2 部分用來定義構成單詞的規則,能夠看到每條規都是一個 正則表達式動做,很直白,就是 詞法分析器 發現了匹配的 單詞 後執行相應的 動做 代碼,大部分只要返回 單詞 給調用者就能夠了;第 3 部分能夠定義一些函數,也會原樣複製到生成的源碼中去,這裏咱們留空沒有使用。

如今咱們能夠經過調用 Flex 生成 詞法分析器 的源碼:

flex -o lexical.cpp lexical.l

生成的 lexical.cpp 裏會有一個 yylex() 函數供 語法分析器 調用;你可能發現了,有些宏和變量並無被定義(如 TEXTERNyylvalyytext 等),其實有些是 Flex 會自動定義的內置變量(如 yytext),有些是後面 語法分析器 生成工具裏定義的變量(如 yylval),咱們後面會看到。

語法分析器

語法分析器 的做用是構建 抽象語法樹,通俗的說 抽象語法樹 就是將源碼用樹狀結構來表示,每一個節點都表明源碼中的一種結構;對於咱們要實現的語法,其語法樹是很簡單的,以下:

圖片描述

如今咱們使用 Bison 生成 語法分析器 代碼,一樣 Bison 須要一個規則文件,咱們的規則文件 syntactic.y 以下,限於篇幅,省略了某些部分,能夠經過連接查看完整內容:

%{
    #include "ast.h"
    #include <cstdio>

...

    extern int yylex();
    void yyerror(const char *s) { std::printf("Error: %s\n", s);std::exit(1); }
%}

...

%token <token> TLPAREN TRPAREN TLBRACE TRBRACE TCOMMA

...

%%

program:
  stmts { programBlock = $1; }
        ;
...

func_decl:
  ident TLPAREN func_decl_args TRPAREN block { $$ = new NFunctionDeclaration(*$1, *$3, *$5); delete $3; }
;

...

%%

是否是發現和 Flex 的規則文件很像呢?確實是這樣,它也是分 3 個部分組成,一樣,第一部分的 C++ 代碼會被複制到生成的源文件中,還能夠看到這裏經過如下這樣的語法定義前面了 Flex 使用的宏:

%token <token> TLPAREN TRPAREN TLBRACE TRBRACE TCOMMA

比較不一樣的是第 2 部分,不像 Flex 經過 正則表達式 經過定義規則,這裏使用的是 巴科斯範式(BNF: Backus-Naur Form) 的形式定義了咱們識別的語法結構。以下的語法表示函數:

func_decl:
  ident TLPAREN func_decl_args TRPAREN block { $$ = new NFunctionDeclaration(*$1, *$3, *$5); delete $3; }
;

能夠看到後面大括號中間的也是 動做 代碼,上例的動做是在 抽象語法樹 中生成一個函數的節點,其實這部分的其餘規則也是生成相應類型的節點到語法樹中。像 NFunctionDeclaration 這是一個咱們本身定義的節點類,咱們在 ast.h 中定義了咱們所要用到的節點,一樣的,咱們摘取一段代碼以下:

...

class NFunctionDeclaration : public NStatement {
public:
    const NIdentifier& id;
    VariableList arguments;
    NBlock& block;
    NFunctionDeclaration(const NIdentifier& id,
            const VariableList& arguments, NBlock& block) :
        id(id), arguments(arguments), block(block) { }
    virtual llvm::Value* codeGen(CodeGenContext& context);
};

...

能夠看到,它有 標識符(id)參數列表(arguments)函數體(block) 這些成員,在語法分析階段會設置好這些成員的內容供後面的 目標碼生成 階段使用。還能夠看到有一個 codeGen() 虛函數,你可能猜到了,後面就是經過調用它來生成相應的目標代碼。

咱們能夠經過如下命令調用 Bison 生成 語法分析器 的源碼文件,這裏咱們使用 -d 使頭文件和源文件分開,由於前面 詞法分析器 的源碼使用了這裏定義的一些宏,因此須要使用這個頭文件,這裏將會生成 syntactic.cppsyntactic.hpp

bison -d -o syntactic.cpp syntactic.y

目標碼生成

這是最後一步了,這一步的主角是前面提到 LLVM,LLVM 是一個構建編譯器的框架系統,咱們使用他遍歷 語法分析 階段生成的 抽象語法樹,而後爲每一個節點生成相應的 目標碼。固然,沒法避免的是咱們須要使用 LLVM 提供的函數來編寫生成目標碼的源碼,就是實現前面提到的虛函數 codeGen(),是否是有點拗口?不過確實是這樣。咱們在 gen.cpp 中編寫了不一樣節點的生成代碼,咱們摘取一段看一下:

...

Value *NMethodCall::codeGen(CodeGenContext &context) {
    Function *function = context.module->getFunction(id.name.c_str());
    if (function == NULL) {
        std::cerr << "no such function " << id.name << endl;
    }
    std::vector<Value *> args;
    ExpressionList::const_iterator it;
    for (it = arguments.begin(); it != arguments.end(); it++) {
        args.push_back((**it).codeGen(context));
    }
    CallInst *call = CallInst::Create(function, makeArrayRef(args), "", context.currentBlock());
    std::cout << "Creating method call: " << id.name << endl;
    return call;
}

...

看起來有點複雜,簡單來講就是經過 LLVM 提供的接口來生成 目標碼,須要瞭解更多的話能夠去 LLVM 的官網學習一下。

至此,咱們全部的工做基本都作完了。簡單回顧一下:咱們先經過 Flex 生成 詞法分析器 源碼文件 lexical.cpp,而後經過 Bison 生成 語法分析器 源碼文件 syntactic.cpp 和頭文件 syntactic.hpp,咱們本身編寫了 抽象語法樹 節點定義文件 ast.h目標碼 生成文件 ast.cpp,還有一個 gen.h 包含一點 LLVM 環境相關的代碼,爲了輸出咱們程序的結果,還在 printi.cpp 裏簡單的經過調用 C 語言庫函數實現了輸出一個整數。

對了,咱們還須要一個 main 函數做爲編譯器的入口函數,它在 main.cpp 裏:

...

int main(int argc, char **argv) {
    yyparse();
    InitializeNativeTarget();
    InitializeNativeTargetAsmPrinter();
    InitializeNativeTargetAsmParser();
    CodeGenContext context;
    context.generateCode(*programBlock);
    context.runCode();

    return 0;
}

咱們能夠看到其調用了 yyparse()語法分析,(yyparse() 內部會先調用 yylex()詞法分析);而後是一系列的 LLVM 初始化代碼,context.generateCode(*programBlock) 是開始生成 目標碼;最後是 context.runCode() 來運行代碼,這裏使用了 LLVM 的 JIT(Just In Time) 來直接運行代碼,沒有連接的過程。

如今咱們能夠用這些文件生成咱們的編譯器了,須要說明一下,由於 詞法分析器 的源碼使用了一些 語法分析器 頭文件中的宏,因此正確的生成順序是這樣的:

bison -d -o syntactic.cpp syntactic.y
flex -o lexical.cpp lexical.l syntactic.hpp
g++ -c `llvm-config --cppflags` -std=c++11 syntactic.cpp gen.cpp lexical.cpp printi.cpp main.cpp
g++ -o xy-complier syntactic.o gen.o main.o lexical.o printi.o `llvm-config --libs` `llvm-config --ldflags` -lpthread -ldl -lz -lncurses -rdynamic

若是你下載了 GitHub 的源碼,那麼直接:

cd src
make

就能夠完成以上過程了,正常會生成一個二進制文件 xy-complier,它就是咱們的編譯器了。

編譯測試

咱們使用以前提到實例 demo.xy 來測試,將其內容傳給 xy-complier 的標準輸入就能夠看到運行結果了:

cat demo.xy | ./xy-complier

也能夠直接經過

make test

來測試,輸出以下:

...

define internal i64 @mult(i64 %a1, i64 %b2) {
entry:
  %a = alloca i64
  %0 = load i64, i64* %a
  store i64 %a1, i64* %a
  %b = alloca i64
  %1 = load i64, i64* %b
  store i64 %b2, i64* %b
  %2 = load i64, i64* %b
  %3 = load i64, i64* %a
  %4 = mul i64 %3, %2
  ret i64 %4
}
Running code:
11
Exiting...

能夠看到最後正確輸出了指望的結果,至此咱們簡單的編譯器就完成了。

參考

相關文章
相關標籤/搜索