Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

為什麼 Zig 編譯器程式碼那麼小?

Zig 編譯器的程式碼量確實比其他主流編譯器小很多,這背後有幾個關鍵設計決策。


1. 程式碼量對比

Zig 編譯器:     ~30 萬行程式碼
Rust 編譯器:    ~200 萬行程式碼
LLVM:          ~1000 萬行程式碼
GCC:           ~1500 萬行程式碼
Clang:         ~100 萬行程式碼

2. 核心設計哲學

🎯 簡單勝於複雜(Simplicity over Complexity)

Zig 的設計原則:

  • 沒有隱藏控制流:沒有異常、沒有隱式記憶體分配
  • 沒有巨集系統:不需要複雜的巨集展開引擎
  • 沒有運算子重載:減少語意分析複雜度
  • 沒有型別推導:明確的型別系統更容易實作
// Zig:明確、簡單
const allocator = std.heap.page_allocator;
const memory = try allocator.alloc(u8, 100);
defer allocator.free(memory);
#![allow(unused)]
fn main() {
// Rust:複雜的所有權系統需要更多編譯器邏輯
let mut vec = Vec::new();
vec.push(1);
// 編譯器需要追蹤 lifetime、borrowing、ownership
}

3. 技術實現策略

A. 直接使用 LLVM 後端

Zig 不自己實作程式碼生成:

其他編譯器架構:
Source → Lexer → Parser → AST → IR → 優化 → 程式碼生成 → Machine Code
                                    ↑ 自己實作這些部分

Zig 架構:
Source → Lexer → Parser → AST → LLVM IR → (交給 LLVM 處理)
                                ↑ 只做到這裡

節省的程式碼量

  • ✅ 不需要實作多平臺程式碼生成器
  • ✅ 不需要實作複雜的優化 pass
  • ✅ 不需要維護平臺相關的組合語言輸出

B. 沒有複雜的型別系統

// Zig:簡單的型別系統
fn add(a: i32, b: i32) i32 {
    return a + b;
}

對比 Rust 編譯器需要處理的複雜度:

#![allow(unused)]
fn main() {
// Rust:需要處理 lifetime、trait bounds、泛型約束
fn add<T>(a: T, b: T) -> T 
where 
    T: std::ops::Add<Output = T> + Copy 
{
    a + b
}

// 編譯器需要:
// 1. Trait resolution(特徵解析)
// 2. Lifetime inference(生命週期推導)
// 3. Borrow checker(借用檢查器)
// 4. Monomorphization(單態化)
}

Zig 編譯器省略的功能

  • ❌ 沒有 Trait 系統
  • ❌ 沒有 Lifetime 分析
  • ❌ 沒有 Borrow Checker
  • ❌ 沒有複雜的泛型推導

C. Comptime:編譯期執行,而非複雜推導

Zig 的天才設計:用編譯期執行取代複雜的型別系統

// Zig:直接在編譯期執行程式碼
fn max(comptime T: type) type {
    return struct {
        pub fn get(a: T, b: T) T {
            return if (a > b) a else b;
        }
    };
}

// 編譯器只需要:
// 1. 在編譯期執行這段程式碼
// 2. 生成結果
// 不需要複雜的型別推導引擎!

對比 C++ 的模板系統:

// C++:需要複雜的 template instantiation 引擎
template<typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

// 編譯器需要:
// 1. Template parsing
// 2. SFINAE (Substitution Failure Is Not An Error)
// 3. Template specialization resolution
// 4. Two-phase lookup
// 這些都需要大量程式碼實作!

D. 自舉(Self-Hosting)的優勢

Zig 編譯器是用 Zig 寫的:

// Zig 編譯器本身就是 Zig 程式
// 可以使用 Zig 的 comptime 功能來簡化實作

const Parser = struct {
    tokens: []Token,
    pos: usize,
    
    // 編譯期生成 parsing 表格
    const parse_table = comptime generateParseTable();
};

好處

  • ✅ 用 comptime 簡化編譯器邏輯
  • ✅ 不需要寫兩次(bootstrapping compiler + 正式 compiler)
  • ✅ 編譯器本身就是最好的測試案例

4. 與其他編譯器的對比

Rust 為什麼大?

#![allow(unused)]
fn main() {
// Rust 編譯器需要處理:

// 1. Borrow Checker(最複雜的部分)
fn process(data: &mut Vec<i32>) {
    // 編譯器需要追蹤每個變數的 lifetime
    // 驗證沒有 data race
    // 這需要大量程式碼!
}

// 2. Trait System
impl<T: Display + Debug> MyTrait for T { }
// 需要 trait coherence 檢查
// 需要 trait resolution 演算法

// 3. Async/Await
async fn fetch() -> Result<String, Error> { }
// 需要狀態機轉換
// 需要 Future trait 實作
}

C++ 為什麼大?

// C++ 需要支援 40 年的歷史包袱

// 1. 複雜的巨集系統
#define COMPLEX_MACRO(x) \
    template<typename T> \
    void func_##x(T t) { }

// 2. 多重繼承
class D : public A, public B, public C { };

// 3. Template Metaprogramming
template<int N>
struct Factorial {
    static const int value = N * Factorial<N-1>::value;
};

5. Zig 的聰明取捨

❌ 不實作的功能

// 1. 沒有異常處理
// C++/Java 需要實作複雜的 exception unwinding
// Zig:用 error union
fn readFile() ![]u8 {
    return error.FileNotFound;
}

// 2. 沒有運算子重載
// C++ 需要處理所有運算子的重載決議
// Zig:明確的函數呼叫
const result = vec.add(other_vec);

// 3. 沒有 OOP 繼承
// Java/C++ 需要虛函數表、動態分派
// Zig:用介面(interface)模式
const interface = MyInterface{
    .ptr = &obj,
    .vtable = &vtable,
};

6. 實際程式碼量統計

Zig 編譯器主要模組

src/
├── Ast.zig           (語法樹, ~5K lines)
├── Sema.zig          (語意分析, ~8K lines)  
├── Codegen.zig       (LLVM IR 生成, ~6K lines)
├── Type.zig          (型別系統, ~4K lines)
├── Module.zig        (模組系統, ~3K lines)
└── main.zig          (主程式, ~2K lines)

總計:~30 萬行(包含標準庫)

對比 Rust

rustc/
├── typeck/           (型別檢查, ~50K lines)
├── borrowck/         (借用檢查, ~40K lines)
├── traits/           (Trait 系統, ~30K lines)
├── mir/              (中間表示, ~25K lines)
└── ... (還有很多)

總計:~200 萬行

7. 設計哲學的影響

Zig 的核心信念

"如果你能在編譯期執行它,就不需要特殊的語言功能"

// 範例:不需要特殊的 foreach 語法
// 用 comptime 自己實作!

pub fn forEach(comptime T: type, items: []T, func: fn(T) void) void {
    for (items) |item| {
        func(item);
    }
}

// 使用
const numbers = [_]i32{1, 2, 3};
forEach(i32, &numbers, print);

結果

  • ✅ 語言功能少 → 編譯器簡單
  • ✅ 但透過 comptime 保持強大
  • ✅ 程式碼量小但功能不減

8. 總結:為什麼 Zig 能保持小巧

策略節省的程式碼量
使用 LLVM 後端~30萬行(優化+程式碼生成)
沒有複雜型別推導~20萬行(型別系統)
沒有 Borrow Checker~50萬行(所有權分析)
沒有異常處理~10萬行(unwinding)
沒有巨集系統~15萬行(巨集展開)
Comptime 取代泛型~25萬行(模板系統)

核心理念

簡單 ≠ 功能弱
簡單 = 更容易理解、維護、除錯

9. 對開發者的啟示

// Zig 的設計告訴我們:
// "正交性"(Orthogonality)的力量

// 不要:為每個需求加新功能
// 要:提供少數強大的基本功能,讓它們可以組合

// 範例:Zig 只有幾個核心概念
// 1. comptime(編譯期執行)
// 2. error union(錯誤處理)
// 3. 明確的記憶體分配
// 4. 簡單的型別系統

// 但這些可以組合出複雜功能!

結語

Zig 的小巧不是因為功能弱,而是因為聰明的設計:用簡單的機制組合出強大的功能,而不是為每個需求都加新的語言特性。這是非常值得學習的軟體設計哲學!🎯


本文檔探討了 Zig 程式語言編譯器如何透過精簡的設計哲學,在保持強大功能的同時維持小巧的程式碼基底。