使用 Rust 和 LLVM 構建編譯器

Cyclang 是一個簡單的靜態類型編程語言,支持基本的語言構造,例如函數、控制流和算術操作。該語言的功能設計非常簡潔,完整的語言規範可以參考官方文檔。本項目的重點是從零開始使用 Rust 和 LLVM 構建一個編譯器的過程,而非語言本身的複雜性。

以下是 Cyclang 代碼的一個示例:

fn fib(i32 n) -> i32 {  
    if (n < 2) {  
        return n;  
    }  
    return fib(n - 1) + fib(n - 2);  
}  
print(fib(20));

爲什麼選擇 Rust?

Rust 是構建編譯器的理想選擇,原因有很多。GitHub 的一篇文章詳細解釋了 “爲什麼 Rust 是開發者最受歡迎的語言”。

Rust 的一個主要優勢是模式匹配(Pattern Matching)。Rust 的窮盡模式匹配(Exhaustive Pattern Matching)可以確保在編譯時處理所有可能的情況,從而避免運行時錯誤。以下是一個簡單的示例:

enum Token {  
    Number(i64),  
    Plus,  
    Minus,  
    LeftParen,  
    RightParen,  
}

fn evaluate_token(token: Token) -> String {  
    match token {  
        Token::Number(n) => format!("Found number: {}", n),  
        Token::Plus | Token::Minus ="Found operator".to_string(),  
        Token::LeftParen ="Found opening parenthesis".to_string(),  
        Token::RightParen ="Found closing parenthesis".to_string(),  
        // 編譯器確保所有情況都被處理  
    }  
}

Rust 的類型系統和所有權模型使其非常適合構建健壯的解析器和編譯器。零成本抽象、內存安全保障以及豐富的代數數據類型的結合,使得可以清晰高效地表達複雜的語言構造。

例如,可以輕鬆擴展令牌解析器爲完整的表達式求值器:

enum Expr {  
    Binary(Box<Expr>, Token, Box<Expr>),  
    Literal(i64),  
    Group(Box<Expr>)  
}

fn evaluate(expr: &Expr) -> Result<i64, String> {  
    match expr {  
        Expr::Literal(n) => Ok(*n),  
        Expr::Binary(left, Token::Plus, right) =>   
            Ok(evaluate(left)? + evaluate(right)?),  
        Expr::Group(expr) => evaluate(expr),  
        _ => Err("Invalid expression".into())  
    }  
}

架構:從源代碼到機器代碼

讓我們深入瞭解 Cyclang 如何將代碼轉換爲可執行指令:

使用 Pest 進行解析

編譯器的第一步是將源代碼解析爲結構化格式。對於 Cyclang,我選擇了 Pest 解析器,這是一個現代的 Rust 解析框架。之前我曾使用過 LALRPOP,但 Pest 提供了一種更直觀的語法定義方式。

解析過程主要包括兩個部分:

在開發過程中,我發現隨着語言複雜度的增加,維護語法文件變得越來越困難。回頭來看,手寫一個解析器可能會提供更多的靈活性,但 Pest 的聲明式方法非常適合快速原型開發。

集成 LLVM

構建 Cyclang 最有趣(也是最具挑戰性)的部分是與 LLVM 的集成。我選擇使用 llvm-sys,它爲 LLVM 的 C API 提供了原始的 Rust 綁定。

爲什麼選擇 LLVM?

LLVM(Low Level Virtual Machine)是一種編譯器基礎架構,提供了以下功能:

使用 LLVM API

直接通過 llvm-sys 使用 LLVM 的 C API 既有優點也有缺點:

優點:

缺點:

爲了管理複雜性,我將 LLVM 相關代碼組織到一個專門的代碼生成模塊。

實現亮點

控制流的翻譯

一個有趣的實現細節是如何將高級控制結構翻譯爲 LLVM IR。例如,Cyclang 的for循環在 IR 層面實際上被 “降級” 爲while循環:

// Cyclang的for循環  
for (i32 i = 0; i < 10; i = i + 1) {  
    print(i);  
}

// 轉換爲等效的IR邏輯:  
{  
    i32 i = 0;  
    while (i < 10) {  
        print(i);  
        i = i + 1;  
    }  
}

完整實現可以參考 builder.rs。

處理複雜類型

實現複雜類型(如字符串)是最具挑戰性的部分之一。LLVM 在非常底層的層面上操作,甚至基本的字符串操作也需要仔細的內存管理和指針操作。可以參考這篇文檔瞭解相關的 LLVM IR 實現。

爲了簡化複雜操作的實現,我採用了一種混合方法:通過 C 文件生成 LLVM 位碼(bitcode)處理複雜操作,然後將其與其餘 IR 加載並鏈接。這種方法顯著簡化了標準庫的實現。

收穫與反思

解析器實現

雖然 Pest 是一個功能強大的解析庫,但我計劃在下一個項目中實現一個手寫解析器。這將加深我對解析技術(特別是遞歸下降解析和優先級解析)的理解,併爲解析過程提供更細粒度的控制。

測試自動化

儘管 Cyclang 擁有全面的測試覆蓋率,但我意識到基於文件的測試生成比手動編寫單獨的 Rust 測試用例更高效。這種方法可以使測試套件更易於維護和擴展。

LLVM 集成

直接與 LLVM API 集成增加了代碼生成模塊的複雜性。在未來的項目中,我計劃將這部分抽象爲一個獨立的庫,或者使用現有的解決方案(如 Inkwell)來改進代碼的組織和可維護性。

結論

Cyclang 從一個學習 Rust 和 LLVM 的玩具項目開始,逐漸演變爲探索編譯器設計的旅程。儘管項目規模較小,但從零開始構建它讓我學到了許多關於語言設計、底層編程以及現代編譯器複雜性的寶貴經驗。希望我的分享能爲有興趣進行類似探索的讀者提供一些有用的見解。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/uLyl3S1cXgqIkG0Y9bpNOg