Bangun kompiler C menggunakan MoonBit
Perkenalan
Bahasa C adalah fondasi dunia pemrograman, dan banyak sistem serta aplikasi dibangun di atasnya. Bagi mahasiswa atau insinyur ilmu komputer, memahami proses kompilasi bahasa C adalah topik klasik dan menantang.
MoonBit adalah bahasa pemrograman baru yang dirancang untuk komputasi cloud dan edge. Ini memiliki tipe data aljabar (ADT) dan pencocokan pola yang kuat, fitur fungsional yang ringkas, dan rantai alat yang lengkap. Jadi bagaimana Anda menggunakan MoonBit untuk menulis kompiler C? Keuntungan unik apa yang akan dihasilkannya?
Mengapa MoonBit
Dibandingkan dengan banyak bahasa tradisional, banyaknya fitur MoonBit mempermudah pembuatan alat bahasa yang kompleks seperti kompiler. Fitur MoonBit yang paling saya hargai adalah: ADT, pencocokan pola, loop fungsional, dan pengikatan llvm. Hal ini sangat penting untuk mengimplementasikan kompiler.
Pencocokan pola yang kuat: Ucapkan selamat tinggal pada string panjang dan Pengunjung
Di front-end kompiler, kita selalu dihadapkan pada “pola”: Apakah urutan Token ini merupakan definisi fungsi atau deklarasi variabel? Apakah simpul AST ini merupakan ekspresi biner atau pemanggilan fungsi?
MoonBit memiliki kemampuan pencocokan pola yang melampaui banyak bahasa tradisional. Ia dapat secara langsung melakukan dekonstruksi struktur, array, dan bahkan string, menjadikan logika analisis sintaksis menjadi sangat intuitif.
Anggaplah kita telah mengubah kode C menjadi rangkaian Token dan sekarang perlu menguraikannyaint main() ... model ini.
Di MoonBit, Anda dapat menulis:
fn parse_main(toks: Array[Token]) -> CFunction?
match toks[:]
// Directly match the token array structure
[KeyWord("int"), Identifier("main"), LParen, RParen, LBrace, ..body_toks] =>
// Logic for parsing the function body...
_ => None // Not a match
Cara penulisan seperti ini hampir merupakan terjemahan langsung dari paradigma BNF yang jelas, aman, dan tidak rawan kesalahan.
Di C++, kita biasanya harus mengelola iterator atau indeks secara manualdan melalui serangkaianifPernyataan untuk memeriksa jenis dan nilai Token satu per satu. Tidak hanya kodenya yang bertele-tele, tetapi detail seperti pemeriksaan batas dan perkembangan indeks dapat dengan mudah menimbulkan bug.
// Hypothetical C++ implementation
CFunction* parse_main(const std::vector<Token>& toks, size_t& pos)
// Manual boundary checks and verbose comparisons
if (pos + 4 < toks.size() &&
toks[pos].type == TokenType::Keyword && toks[pos].value == "int" &&
toks[pos+1].type == TokenType::Identifier && toks[pos+1].value == "main" &&
toks[pos+2].type == TokenType::LParen &&
toks[pos+3].type == TokenType::RParen &&
toks[pos+4].type == TokenType::LBrace)
pos += 5; // Manually advance the position
// Logic for parsing function body...
return new CFunction(...);
return nullptr;
Sebaliknya, pencocokan pola MoonBit membebaskan pengembang dari logika prosedural yang membosankan dan lebih berfokus pada struktur inti tata bahasa.
Loop Fungsional: Implementasi State Machine yang Elegan
Analisis leksikal pada dasarnya adalah mesin keadaan yang memindai string kode sumber dan menentukan tindakan selanjutnya berdasarkan keadaan dan karakter yang ditemui saat ini. “Lingkaran fungsional” unik MoonBit (loopEkspresi) cocok untuk jenis tugas ini. Dibutuhkan status loop (misalnya, tampilan string yang tersisa) sebagai parameter dan “menghasilkan” status baru pada setiap iterasi, sehingga memajukan mesin status secara deklaratif.
Di MoonBit, loop inti yang mengimplementasikan penganalisis leksikal (tokenizer) adalah sebagai berikut:
pub fn tokenize(code: String) -> Array[Token]
let tokens: Array[Token] = []
// The state is the remaining string view, which is passed to the next iteration
loop code[:] '_', ..] as id_part =>
// The `tokenize_identifier` function returns the new string view
let rest = tokenize_identifier(id_part, tokens)
continue rest
[] => tokens.push(EOF); break // End of stream
[other, ..rest] => raise LexError("Invalid character");
tokens
Tidak ada variabel indeks variabel di seluruh proses, transfer keadaan transparan, dan logika terungkap secara alami seperti reaksi berantai.
Di C++, penerapan logika yang sama adalah rumus prosedural yang umum**while**siklusmemerlukan sebuah variabelposVariabel untuk melacak secara manual posisi saat ini dalam string.
// Hypothetical C++ implementation
std::vector<Token> tokenize(const std::string& code)
std::vector<Token> tokens;
size_t pos = 0;
while (pos < code.length())
if (std::isspace(code[pos]))
pos++; // Manual state update
continue;
if (code.substr(pos, 2) == "->")
tokens.push_back(TokenType::Arrow);
pos += 2; // Manual state update
continue;
// ... and so on
return tokens;
Meskipun manajemen status penting ini merupakan rutinitas sehari-hari bagi pemrogram C++, dibandingkan dengan loop fungsional MoonBit, hal ini tidak diragukan lagi meningkatkan beban mental dan mempermudah terjadinya kesalahan satu per satu.
LLVM Binding: Pembuatan IR yang aman dan ringkas
LLVM adalah landasan kompiler modern, tetapi C++ API terkenal buruk dan memiliki kurva pembelajaran yang curam. Pengembang harus berurusan dengan banyak file header, pointer mentah, dan manajemen memori manual.
MoonBit secara resmi menyediakan perpustakaan pengikatan yang konsisten dengan gaya API LLVM C++ tetapi lebih aman dan mudah digunakan. Ia menggunakan sistem tipe MoonBit untuk merangkum operasi yang tidak aman.
Bangun satu menggunakan pengikatan LLVM MoonBit**add**fungsi:
test "Simple LLVM Add"
let ctx = Context::new()
let mod = ctx.addModule("demo")
let builder = ctx.createBuilder()
let i32ty = ctx.getInt32Ty()
let fty = ctx.getFunctionType(i32ty, [i32ty, i32ty])
let func = mod.addFunction(fty, "add")
let bb = func.addBasicBlock(name="entry")
builder.setInsertPoint(bb)
let arg0 = func.getArg(0).unwrap()
let arg1 = func.getArg(1).unwrap()
let add_res = builder.createAdd(arg0, arg1, "add_res")
let _ = builder.createRet(add_res)
// Easily inspect the result in tests
inspect(func, content=...)
Kodenya ringkas dan jelas, tidak ada instruksi mentah,Context, Module, BuilderSiklus hidup objek lainnya dikelola oleh MoonBit, dan pengembang dapat fokus pada konstruksi logis IR.
Di C++, operasi yang sama lebih rumit:
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/Verifier.h"
// ... potentially more includes
// (Inside a function)
using namespace llvm;
static LLVMContext TheContext;
static IRBuilder<> Builder(TheContext);
static std::unique_ptr<Module> TheModule = std::make_unique<Module>("my_module", TheContext);
Function* createAddFunc()
std::vector<Type*> ArgTypes(2, Type::getInt32Ty(TheContext));
FunctionType* FT = FunctionType::get(Type::getInt32Ty(TheContext), ArgTypes, false);
Function* F = Function::Create(FT, Function::ExternalLinkage, "add", TheModule.get());
BasicBlock* BB = BasicBlock::Create(TheContext, "entry", F);
Builder.SetInsertPoint(BB);
// Argument access is less direct
auto Args = F->arg_begin();
Value* Arg1 = Args++;
Value* Arg2 = Args;
Value* Sum = Builder.CreateAdd(Arg1, Arg2, "addtmp");
Builder.CreateRet(Sum);
verifyFunction(*F);
return F; // Caller needs to manage the lifetime of the module
Dari manajemen file header, namespace, manajemen memori manual (std::unique_ptr) untuk membuat penanda fungsi dan mengambil parameter, kode versi C++ lebih kompleks di setiap aspek. Pustaka pengikatan MoonBit menurunkan ambang batas penggunaan LLVM.
mbtcc: Kompiler AC diimplementasikan dengan MoonBit
Alamat proyek:
Berdasarkan kelebihan di atas, kita dapat mencoba menggunakan MoonBit untuk mengimplementasikan kompiler C11 yang dapat mengkompilasi proyek C besar. Selanjutnya, kita akan memeriksa modul inti dan melihat detail desainnya.
Analisis leksikal
Analisis leksikal adalah proses mengubah string kode C mentah menjadi rangkaian Token. Pertama, kita mendefinisikan besarTokenPencacahan, yang mencakup semua kata kunci, simbol, literal, dll dalam bahasa C. Untuk memfasilitasi debugging dan pelaporan kesalahan, kami menggunakanmbtccInformasi lokasi dilampirkan pada setiap Token.
// A simplified Token definition
pub enum Token
// Keywords
Int; Return; If; Else, ...
// Symbols
Plus; Minus; LBrace; RBrace; Semi; ...
// Literals, Identifiers and more
Identifier(String)
IntLit(String)
StringLiteral(String)
// Special tokens for macros
Hash // #
Hash2 // ##
// End of File
EOF,
Kemudian, kami menggunakan loop fungsionalloopUntuk meneruskan semua string kode sumber, kode di bawah ini ditampilkanmbtccpada hakikatnya, pada kenyataannyambtccloop ini juga akan mencatat informasi seperti nomor baris dan kolom, yang dapat memberikan petunjuk kesalahan selama analisis sintaksis.
// Simplified logic from `lexer.mbt`
pub fn ParserContext::tokenize(self: Self) -> Unit
...
loop self.code[:] '\t', .. rest] => continue rest
// Skip comments
[.. "//", ..] => continue self.skip_line_comment()
[.. "/*", ..] => continue self.skip_block_comment()
// Match operators
[.. "->", .. rest] => self.add_tok(Arrow); continue rest
['-', ..rest ] => self.add_tok(Minus); continue rest
...
// Match keywords or identifiers
['a'..'z'
Ekspansi makro
Inti dari proses perluasan makro adalah melalui aliran Token lagi dan mengekstrak data sesuai dengan instruksi makro (mis#define, #include) Konversi aliran Token:
Beberapa pembaca mungkin memiliki pertanyaan, bukankah seharusnya proses kompiler C dilakukan dan kemudian analisis leksikal? Faktanya, ekspansi makro itu sendiri dalam preprocessing memerlukan analisis leksikal terlebih dahulu. Setelah mendapatkan pesanan Token, Token khusus segera diganti. Misalnya,
[Hash, Identifier("include"), StringLit(file), NewLine]Keempat Token ini digantikan oleh Token yang diperoleh melalui analisis leksikal dalam file file. Dalam beberapa implementasi kompiler C, analisis leksikal yang digunakan untuk prapemrosesan ekspansi makro mungkin sama dengan analisis leksikal yang digunakan dalam kode C, dan beberapa mungkin berbeda. MBTCC menggunakan strategi sebelumnya, yaitu menggunakan analisis leksikal yang sama. Ini lebih mudah untuk diterapkan.
// Simplified logic from `preprocess.mbt`
pub fn ParserContext::preprocess(self: Self) -> Unit
let new_toks: Array[Token] = []
// A map to store macro definitions, from name to its token sequence
let def_toks: Map[String, Array[Token]] = Map::new()
loop self.toks[:]
// Handle `#include`, for example #include "file.h"
[Hash, Identifier("include"), StringLiteral(f), ..rest] =>
// Lex & preprocess the included file, then push tokens into `new_toks`
...
continue rest
// Handle `#define` for example #define PI 3.14
[Hash, Identifier("define"), Identifier(def_name), ..def_body] =>
// Store the macro body in `def_toks`
...
continue rest // The #define directive itself is consumed
// Handle #ifdef MACRO
[Hash, Identifier("ifdef"), Identifier(def_name), ..rest] => ...
// An identifier that might be a macro that needs expansion
[Identifier(name), ..rest] if def_toks.contains_key(name) =>
new_toks.push_all(def_toks[name]) // Replace with macro body
continue rest
// Other tokens are passed through directly
[tok, ..rest] => new_toks.push(tok); continue rest
[] => break,
self.toks = new_toks // Replace the old token stream
Analisis tata bahasa
Setelah mendapatkan aliran Token terakhir, memasuki tahap analisis sintaksis, yang bertujuan untuk membangun pohon sintaksis abstrak (AST). InimbtccBagian yang paling menjelaskan keunggulan bahasa MoonBit.mbtccParser dirancang dengan mengacu pada paradigma BNF C11.
Bantuan utamanya adalahParserContextyang menjalankan seluruh proses penguraian dan penyimpanan informasi sepertitypedefsInformasi penting seperti itu. Dalam bahasa C,typedefPengidentifikasi dapat berupa nama tipe atau nama variabel. Diskriminasi yang benar merupakan kesulitan utama dalam analisis sintaksis.
// From `Context.mbt`
struct ParserContext
// ... other fields
// A set of all identifiers that have been declared as a type via typedef
typedefs: Set[String]
Ambil penguraian ekspresi utama (Ekspresi Primer) sebagai contoh, fungsi penguraianPrimExpr::parseDengan mendemonstrasikan kekuatan pencocokan pola:
// Simplified logic from `parser.mbt`
fn ParserContext::parse_prim_expr(
self: Self, toks: ArrayView[Token]
) -> (PrimExpr, ArrayView[Token]) raise {
match toks
// Identifier, but it must NOT be a type name.
// This is where `ctx.typedefs` becomes crucial.
[Identifier(name), ..rest] if !ctx.typedefs.contains(name) =>
(PrimExpr::Identifier(name), rest)
// A constant value
[Constant(c), ..rest] => (Constant(c), rest)
// String literals can be concatenated, e.g., "hello" " world".
// A nested loop handles this concatenation.
[StringLiteral(lit), ..rest] =>
let mut s = lit
let rest = loop rest
[StringLiteral(next_lit), ..next_rest] =>
s += next_lit
continue next_rest
r => break r
(PrimExpr::StringLiteral(s), rest)
// A parenthesized expression: ( expr )
[LParen, ..rest] =>
let (expr, rest_after_expr) = Expr::parse(rest, ctx)?
// Use `guard` to ensure a closing parenthesis exists
guard rest_after_expr is [RParen, ..rest_after_paren] else
raise ParseError("Expected ')'")
(ParenExpr(expr), rest_after_paren)
// Other cases for __builtin_offsetof, etc.
_ => raise ParseError("Unrecognized primary expression")
}
Dengan cara ini, kita dapat mendeskripsikan aturan sintaksis dengan cara yang hampir deklaratif, membuat kode lebih mudah dibaca dan dipelihara.
pembuatan kode
Saat AST dibuat, tahap akhir mengubah AST menjadi LLVM IR masuk.mbtccSimpan satu untuk setiap fungsiCodeGenContextyang paling penting adalah tabel simbolsym_tableyang bertanggung jawab untuk memetakan nama variabel dalam kode sumber ke nilai di dunia LLVM.
traitalias @IR.Value as LLVMValue
struct CodeGenContext
// ... llvm context, builder, module
sym_table: Map[String, &LLVMValue]
// ... other fields like current function, parent context etc.
Proses pembuatan kode adalah proses rekursif yang melintasi AST. Misalnya, ketika kita menemukan referensi variabel, kita memulainya dengansym_tableTemukan kecocokanLLVMValue. ketika bertemu dengan aifpernyataan, kami menghasilkan blok dasar yang sesuai dan instruksi lompatan bersyarat.
untuk satuifLogika kode pembuatan pernyataan kira-kira sebagai berikut:
// Simplified code generation for an if statement
fn IfStmt::codegen(self: IfStmt, ctx: CodeGenContext) -> Unit
let builder = ctx.builder
let func = builder.getInsertBlock().getParent()
// Create basic blocks for the branches and the merge point
let then_bb = func.addBasicBlock(name="then")
let else_bb = func.addBasicBlock(name="else")
let merge_bb = func.addBasicBlock(name="ifcont")
// Codegen for the condition, resulting in a boolean value
let cond_val = self.cond.codegen(ctx)
// ... convert cond_val to a 1-bit integer (i1)
// Create the conditional branch instruction
builder.createCondBr(cond_bool, then_bb, else_bb)
// Populate the 'then' block
builder.setInsertPoint(then_bb)
self.then_branch.codegen(ctx)
builder.createBr(merge_bb) // Jump to merge block
// Populate the 'else' block
builder.setInsertPoint(else_bb)
if self.else_branch is Some(e) e.codegen(ctx)
builder.createBr(merge_bb) // Jump to merge block
// Continue code generation from the merge block
builder.setInsertPoint(merge_bb)
Jaminan Kualitas: Cara Menguji Kompiler
Kebenaran kompiler sangat penting.mbtccProgram pengujian end-to-end yang ketat diadopsi untuk memastikan kebenaran hasil kompilasi. Proses ini tertulis ditest.shDalam naskah, ide utamanya adalah——Dan**gcc**Pertandingan menembak.
“Bandingkan”, yaitu membandingkan prosedur kita dengan prosedur yang sudah diakui dan matang (dalam hal inigcc) jalankan input yang sama dan bandingkan outputnya. Jika keluarannya sama persis, kami yakin program kami benar untuk pengujian ini.
mbtccProses pengujiannya adalah sebagai berikut:
- Siapkan kasus uji: kita
ctest/Banyak file sumber bahasa C disiapkan di direktori. File-file ini mencakup berbagai fitur tata bahasa dan algoritmik umum bahasa C, sepertiquick_sort.c,hash_table.c,kruskal.cTunggu. - Mintalah jawaban standar: Skrip pengujian pertama akan digunakan
gccKompilasi setiap file pengujian C dan jalankan file yang dapat dieksekusi, memberikan output sebagai “Output yang Diharapkan”. - Kompilasi dan jalankan**
mbtcc**: Selanjutnya script akan memanggilmbtccuntuk mengkompilasi file C yang sama dan mengonversinya menjadi LLVM IR. - menghasilkan**
mbtcc**file yang dapat dieksekusi: Kalau begitu, gunakanlahclangakanmbtccLLVM IR yang dihasilkan dikompilasi menjadi file akhir yang dapat dieksekusi. - Dapatkan hasil nyata: jalan ini
mbtccFile eksekusi yang dihasilkan mendapat “Output Aktual”. - Hasilnya membandingkan: Terakhir, skrip menyatakan bahwa “output yang diharapkan” dan “output aktual” sama persis. Jika tidak konsisten, pengujian gagal dan segera dihentikan; jika konsisten, ujiannya berhasil.
Melalui proses pengambilan gambar otomatis ini,mbtccHal ini memungkinkan kami dengan cepat memverifikasi kebenaran fungsi inti setelah setiap perubahan kode, memastikan bahwa kami membuat kemajuan yang stabil menuju tujuan kompiler C yang andal.
mencoba
Mari kita gunakanmbtccKompilasi fungsi Fibonacci klasik:
int fib(int n)
if (n <= 2) return 1;
return fib(n-1) + fib(n-2);
menggunakanmbtccKompilasi kode di atas dan dapatkan dengan cepatllvm IR:
define i32 @fib(i32 %n)
entry:
%cmp = icmp sle i32 %n, 2
br i1 %cmp, label %if.then, label %if.else
if.then:
ret i32 1
if.else:
%sub1 = sub nsw i32 %n, 1
%call1 = call i32 @fib(i32 %sub1)
%sub2 = sub nsw i32 %n, 2
%call2 = call i32 @fib(i32 %sub2)
%add = add nsw i32 %call1, %call2
ret i32 %add
Inilah IR yang Anda inginkan.
Pandangan
saat ini,mbtccProyek ini telah mampu merakit banyak struktur data yang umum digunakan, termasuk daftar tertaut, tabel hash, dan tumpukan. Anda juga dapat mengumpulkan beberapa kalkulus seperti Quick Sort, Merge Sort, Dijkstra, Kruskal, Prim, dll. Kelayakan dan keunggulan penggunaan MoonBit untuk mengembangkan compiler C telah diverifikasi sebelumnya.
Proyek ini awalnya menunjukkan potensi MoonBit di bidang implementasi bahasa pemrograman. masa depan,mbtccIni akan terus ditingkatkan dan diarahkan untuk mendukung standar C11 secara penuh. Jika Anda tertarik dengan proyek ini, selamat datang untuk mengunjungi repositori kode sumber,
Referensi
mbtcc: /pohon/masterllvm.mbt:C11 BNF: /blob/master/parser/c.g4- BulanBit:
PakarPBN
A Private Blog Network (PBN) is a collection of websites that are controlled by a single individual or organization and used primarily to build backlinks to a “money site” in order to influence its ranking in search engines such as Google. The core idea behind a PBN is based on the importance of backlinks in Google’s ranking algorithm. Since Google views backlinks as signals of authority and trust, some website owners attempt to artificially create these signals through a controlled network of sites.
In a typical PBN setup, the owner acquires expired or aged domains that already have existing authority, backlinks, and history. These domains are rebuilt with new content and hosted separately, often using different IP addresses, hosting providers, themes, and ownership details to make them appear unrelated. Within the content published on these sites, links are strategically placed that point to the main website the owner wants to rank higher. By doing this, the owner attempts to pass link equity (also known as “link juice”) from the PBN sites to the target website.
The purpose of a PBN is to give the impression that the target website is naturally earning links from multiple independent sources. If done effectively, this can temporarily improve keyword rankings, increase organic visibility, and drive more traffic from search results.