利用LLVM實現JS的編譯器,創造屬於本身的語言

本文參考了官方教程Kaleidoscope語言的實現,本文只實現了JS的編譯器的demo,若是想要加深學習好比語言的JIT的實現和語言的代碼優化,我將官方教程和代碼集合打包在了 github.com/zy445566/ll… 中有興趣,能夠更加深刻的學習。ios

什麼是LLVM

像你們熟知的Swift就是依靠LLVM實現的一門語言,還有Rust也是將LLVM用於後端編譯。
一句話總結,它就是一種編譯器的基礎設施。可能有人說是gcc一類的東西麼?老實說最初它倒是用來取代gcc的,但它擁有的毫不是編譯而是擁有製造新語言能力的所有能力的一個工具。可讓人更加無痛的實現一門語言。
本文編譯器流程大概是【編寫AST用於分析語言結構】->【將分析的語言綁定生成IR(中間語言)】-> 【生成二進制或彙編代碼】
若是使用LLVM製做語言的虛擬機亦能夠實現JIT,或者是編譯器和虛擬機的結合體。git

準備工做

安裝LLVM

# centOS,ubuntu應該也可使用yum或apt-get進行安裝
# 有時間的話下載源碼編譯固然更好
brew install llvm
複製代碼

mac還須要安裝xcode命令行工具

# 兩臺電腦都裝了xcode,一臺編譯竟然找不到標準庫
# 這個問題我找了很久
xcode-select --install
複製代碼

編寫AST用於分析語言結構階段

先定義token類型,用於識別詞法結構,定義負數的緣由是ascii碼的字符都是正數github

enum Token{
    tok_eof = -1,
    // define
    tok_var = -2,
    tok_func = -3,
    // code type
    tok_id = -4,
    tok_exp = -5,
    tok_num = -6,
    // choose
    tok_if = -7,
    tok_else = -8,
    // interrupt
    tok_return = -9,
    // other
    tok_unkown = -9999
};
複製代碼

解析token的方法,也能夠用於字符跳躍express

static int gettoken() {
    LastChar = fgetc(fp);
    // 排除不可見字符
    while (isspace(LastChar))
    {
        LastChar = fgetc(fp);
    }
    // 排除註釋
    if (LastChar=='/' && (LastChar = fgetc(fp))=='/'){
        do{
            LastChar = fgetc(fp);
        } 
        while (!feof(fp) && LastChar != '\n' && LastChar != '\r' && LastChar != 10);
        // 吃掉不可見字符
        while (isspace(LastChar))
        {
            LastChar = fgetc(fp);
            if (LastChar=='/') {fseek(fp,-1L,SEEK_CUR);}
        }
    }
    // 解析[a-zA-Z][a-zA-Z0-9]*
    if (isalpha(LastChar)) {
        defineStr = LastChar;
        int TmpChar;
        while (isalnum((TmpChar = fgetc(fp))) && (LastChar = TmpChar))
        {
            defineStr += TmpChar;
        }
        fseek(fp,-1L,SEEK_CUR);
        if (defineStr == "var")
        {
            return tok_var;
        }
        if (defineStr == "function")
        {
            return tok_func;
        }
        if (defineStr == "if")
        {
            return tok_if;
        }
        if (defineStr == "else")
        {
            return tok_else;
        }
        if (defineStr == "return")
        {
            return tok_return;
        }
        return tok_id;
    }
    // 解析[0-9.]+
    if (isdigit(LastChar) || LastChar == '.') {
        std::string NumStr;
        do {
        NumStr += LastChar;
        LastChar = fgetc(fp);
        } while (isdigit(LastChar) || LastChar == '.');
        NumVal = strtod(NumStr.c_str(), nullptr);
        return tok_num;
    }
    if(feof(fp)){
        return tok_eof;
    }
    return LastChar;
}
複製代碼

再次定義語法結構數的語法,這個能夠根據本身的喜愛定義ubuntu

// AST基類
class ExprAST {
public:
  virtual ~ExprAST() = default;
  // 這是用於實現IR代碼生成的東西
  virtual llvm::Value *codegen() = 0;
};

// 定義解析的數字的語法樹
class NumberExprAST : public ExprAST {
  double Val;

public:
  NumberExprAST(double Val) : Val(Val) {}
  llvm::Value *codegen() override;
};

// 定義解析的變量的語法樹
class VariableExprAST : public ExprAST {
  std::string Name;

public:
  VariableExprAST(const std::string &Name) : Name(Name) {}
  llvm::Value *codegen() override;
};
// 還有不少語法類型,因爲太多,暫時不寫
...
複製代碼

循環獲取token並進入對應的方法後端

static void LoopParse() {
    while (true) {
        LastChar = gettoken();
        switch (LastChar) {
        case tok_eof:
            return;
        case ';':
            gettoken();
            break;
        case tok_func:
            HandleFunction();
            break;
        case tok_if:
            HandleIf();
            break;
        default:
            break;
        }
    }
}
複製代碼

解析JS方法的功能數組

static std::unique_ptr<FunctionAST> HandleFunction() {
    LastChar = gettoken();
    // 解析方法的參數
    auto Proto = ParsePrototype();
    if (!Proto){return nullptr;}
    // 吃掉方法的大括號
    gettoken();
    if (LastChar != '{'){return LogErrorF("Expected '{' in prototype");}
    // 定義方法的內容,這是一個數組,由於方法是多行的
    std::vector<FnucBody> FnBody;
    while(true){
        // 這是這一行代碼的類型,其中包含表達式和是否返回數據
        FnucBody fnRow;
        if (auto E = ParseExpression())
        {
            fnRow.expr_row = std::move(E);
            fnRow.tok = RowToken;
            RowToken = 0;
            FnBody.push_back(std::move(fnRow));
        } else {
            // 若是這一行是分號,讓下一次gettoken去吃掉分號
            if (LastChar == ';'){continue;}
            // 若是方法結束判斷是否有大括號,沒有則報異常
            if (LastChar != '}'){return LogErrorF("Expected '}' in prototype");}
            // 生成方法的AST
            auto FnAST = llvm::make_unique<FunctionAST>(std::move(Proto), std::move(FnBody));
            // 生成方法的代碼
            if (auto *FnIR = FnAST->codegen()) {
                //異常則輸出錯誤, 未出異常則輸出IR
                // FnIR->print(llvm::errs());
            }
            return FnAST;
        }
    }
    return nullptr;
}
複製代碼

而裏面比較複雜應該是ParseExpression,用於解析表達式的方法,複雜點在於表達式中可能還有表達式,表達式裏面還有表達式,有的時候思考下來,腦子裏面基本是無限遞歸,能讓腦子瞬間短路xcode

// 表達式解析
static std::unique_ptr<ExprAST> ParseExpression() {
    // 解析表達式的左邊
    auto LHS = ParsePrimary();
    if (!LHS){
        return nullptr;
    }
    // 解析表達式的操做符和表達式的右邊
    return ParseBinOpRHS(0, std::move(LHS));
}
// 判斷表達式左邊是什麼類型
static int RowToken = 0;
static std::unique_ptr<ExprAST> ParsePrimary() {
  int res = gettoken();
  switch (res) {
  default:
    return LogError("unknown token when expecting an expression");
  case tok_id:
    // 若是是變量或執行的方法
    return ParseIdentifierExpr();
  case tok_if:
    // 若是是if
    return HandleIf();
  case tok_num:
    // 若是是數字
    return ParseNumberExpr();
  case tok_return:
    // 若是是返回則標記,並繼續執行表達式左邊
    RowToken = tok_return;
    return ParsePrimary();
  case '}':
    // 符號跳過
    return nullptr;
  case ';':
    // 符號跳過
    return nullptr;
  case '(':
    // 做爲父表達式運行
    return ParseParenExpr();
  }
}
// 解析表達式的操做符和表達式的右邊
static std::unique_ptr<ExprAST> ParseBinOpRHS(
    int ExprPrec,
    std::unique_ptr<ExprAST> LHS
) {
  gettoken();
  while (true) {
    // 判斷操做符優先級
    int TokPrec = GetTokPrecedence();
    // 若是操做符優先級低,直接返回當前
    if (TokPrec < ExprPrec){return LHS;}
    // 若是操做符優先級高,繼續運算
    int BinOp = LastChar;
    // 分析右表達式
    auto RHS = ParsePrimary();
    if (!RHS){return nullptr;}
    // 繼續表表達式
    int NextPrec = GetTokPrecedence();
    // 繼續分析操做符優先級
    if (TokPrec < NextPrec) {
      RHS = ParseBinOpRHS(TokPrec + 1, std::move(RHS));
      if (!RHS){return nullptr;}
    }
    // 將左右表達式合併
    LHS = llvm::make_unique<BinaryExprAST>(BinOp, std::move(LHS),
                                           std::move(RHS));
  }
}

複製代碼

將分析的語言綁定生成IR(中間語言)

看完上面的是否是以爲有點慌,其實解析好了,生成IR很簡單。IR是一箇中間語言,簡單就是把一門語言轉換成另外一門語言,而解析好了的話,其實就只剩下綁定了。
先看看方法的AST的定義ide

// 方法中的一行的類型定義
struct FnucBody{
    // 是否有token
    int tok;
    // 這一行的表達式
    std::unique_ptr<ExprAST> expr_row;
};
class FunctionAST {
  // 參數列表定義
  std::unique_ptr<PrototypeAST> Proto;
  // 方法中所有表達式行
  std::vector<FnucBody> FnBody;

public:
  // 構造
  FunctionAST(std::unique_ptr<PrototypeAST> Proto,
              std::vector<FnucBody> FnBody)
      : Proto(std::move(Proto)), FnBody(std::move(FnBody)) {}
  // 定義IRcode的生成方法
  llvm::Function *codegen();
};
複製代碼

具體生成IR的方法函數

llvm::Function *FunctionAST::codegen() {
  // 獲取函數名,並檢測是不是已存在的函數
  llvm::Function *TheFunction = TheModule->getFunction(Proto->getName());
  
  // 若是函數不存在,則生成行數及參數並將函數從新賦值
  if (!TheFunction)
    TheFunction = Proto->codegen();
  
  // 若是沒生成成功,說明參數存在問題
  if (!TheFunction)
    return nullptr;

  // 在上下文中將entry語法塊插入方法中
  llvm::BasicBlock *BB = llvm::BasicBlock::Create(TheContext, "entry", TheFunction);
  Builder.SetInsertPoint(BB);

  // 將參數寫入map中
  NamedValues.clear();
  for (auto &Arg : TheFunction->args())
    NamedValues[Arg.getName()] = &Arg;

  // 遍歷每一行並生成代碼,若是token是return,則設置返回數據
  for (unsigned i = 0, e = FnBody.size(); i != e; ++i) {
    llvm::Value *RetVal = FnBody[i].expr_row->codegen();
    if (FnBody[i].tok==tok_return){
      Builder.CreateRet(RetVal);
    }
    // 若是所有的行執行完成則校驗方法並返回方法
    if(i+1==e){
      verifyFunction(*TheFunction);
      return TheFunction;
    }
    
  }
  // 發生錯誤移除方法
  TheFunction->eraseFromParent();
  return nullptr;
}
複製代碼

生成二進制文件

int destFile (std::string FileOrgin) {
  // 初始化發出目標代碼的全部目標
  llvm::InitializeAllTargetInfos();
  llvm::InitializeAllTargets();
  llvm::InitializeAllTargetMCs();
  llvm::InitializeAllAsmParsers();
  llvm::InitializeAllAsmPrinters();
  // 使用咱們的目標三元組來得到Target
  auto TargetTriple = llvm::sys::getDefaultTargetTriple();
  TheModule->setTargetTriple(TargetTriple);

  std::string Error;
  auto Target = llvm::TargetRegistry::lookupTarget(TargetTriple, Error);

  if (!Target) {
    llvm::errs() << Error;
    return 1;
  }

  auto CPU = "generic";
  auto Features = "";

  llvm::TargetOptions opt;
  auto RM = llvm::Optional<llvm::Reloc::Model>();
  // 將編譯的機器信息錄入
  auto TheTargetMachine =
      Target->createTargetMachine(TargetTriple, CPU, Features, opt, RM);
  // 經過了解目標和數據佈局,優化代碼
  TheModule->setDataLayout(TheTargetMachine->createDataLayout());
  
  // 定義文件流
  std::string  Filename = FileOrgin+".o";
  std::error_code EC;
  llvm::raw_fd_ostream dest(Filename, EC, llvm::sys::fs::F_None);

  if (EC) {
    llvm::errs() << "Could not open file: " << EC.message();
    return 1;
  }
  
  // 代碼寫入流中
  llvm::legacy::PassManager pass;
  auto FileType = llvm::TargetMachine::CGFT_ObjectFile;

  if (TheTargetMachine->addPassesToEmitFile(pass, dest, FileType)) {
    llvm::errs() << "TheTargetMachine can't emit a file of this type";
    return 1;
  }
  // 完成並清除流
  pass.run(*TheModule);
  dest.flush();
  // 輸出完成提示
  llvm::outs() << "Wrote " << Filename << "\n";
  return 0;
}
複製代碼

編譯編譯器

將咱們作好的編譯器編譯出來,生成jsvm文件

clang++ -g -O3 jsvm.cpp  `llvm-config --cxxflags --ldflags --system-libs --libs all` -o jsvm
複製代碼

使用咱們寫好的編譯器編譯js文件

編譯js

js文件以下

// fibo.js 這是斐波納切數
function fibo(num) {
    if (num<3) {
        return 1;
    } else {
        return fibo(num-1)+fibo(num-2);
    }
}
複製代碼

開始編譯js文件,將生成 fibo.js.o,以下

./jsvm fibo.js
複製代碼

使用c引用js文件,並編譯成二進制文件

c代碼以下:

// main.cpp
#include <iostream>

extern "C" {
    double fibo(double);
}

int main() {
    std::cout << "fibo(9) is: " << fibo(9) << std::endl;
}
複製代碼

編譯並運行,以下:

clang++ main.cpp fibo.js.o -o main && ./main
複製代碼

總結

第一次寫編譯器感受很凌亂,編譯器自己來講還算是一個相對複雜的工程,加上js語言的靈活多變性,實現起來可能更加困難,不過這做爲一個學習的例子應該是不錯的,遂與你們分享。
相信llvm未來也是能爲JS助力的,事實上已經有人有很大膽的想法去使用llvm編譯JS,前段時間facebook的prepack就有這樣一個PR【facebook/prepack/pull/2264】去實現用llvm將js編譯成二進制而無需運行時。兄弟們!JS自舉的路或許不會太遠了。

相關文章
相關標籤/搜索