Derleyiciler bazen kara kutu gibi görünür: kodu yazarsın, bir şeyler olur ve işlemci “tamamdır” der. Oysa içeride oldukça sistemli bir mutfak çalışır. Bu yazıda C ile minicik bir dil motoru tasarlayarak SET x 5, ADD x 3, PRINT x gibi komutları okuyacak, sözdizimini analiz edecek ve bunları basit bir “makine dili” yani bytecode’a çevireceğiz.
``
Bir yorumlayıcının temel fikri şudur: metin hâlindeki program önce anlamlı parçalara ayrılır, sonra kurallara göre dizilir, en sonunda da çalıştırılabilir talimatlara dönüştürülür. Gerçek derleyicilerde bu süreç daha karmaşıktır; optimizasyon, tip denetimi ve hedef mimari gibi konular devreye girer. Bizim oyuncak dilimizde ise değişkenler, sayılar ve birkaç komut yeterli olacak.
Teorik olarak kaynak koddan çalışmaya giden yol şöyle modellenebilir:
\[Kaynak\ Kod \rightarrow Tokenlar \rightarrow AST/Komutlar \rightarrow Bytecode \rightarrow VM\]Burada “VM”, sanal makine demektir. Gerçek CPU yerine kendi küçük işlemcimizi C içinde simüle ederiz. Mesela ADD x 3 komutu, bellekteki x değerini alıp üzerine 3 ekleyen bir bytecode talimatına dönüşür.
| Aşama | Görevi | Örnek |
|---|---|---|
| Lexer | Metni parçalara ayırır | ADD, x, 3 |
| Parser | Sıralamanın doğru olup olmadığını kontrol eder | ADD IDENT NUMBER |
| Codegen | Komutu bytecode’a çevirir | OP_ADD, var_id, 3 |
| VM | Bytecode’u çalıştırır | x = x + 3 |
Önce komut kümemizi tanımlayalım. Bu dilde üç işlem olsun: değişkene değer atama, toplama ve ekrana yazdırma. Matematiksel olarak belleği bir fonksiyon gibi düşünebiliriz: $M(v)$, v değişkeninin değeridir. ADD x 3 çalışınca yeni durum $M’(x)=M(x)+3$ olur.
typedef enum {
OP_SET,
OP_ADD,
OP_PRINT,
OP_HALT
} OpCode;
typedef struct {
OpCode op;
int var;
int value;
} Instruction;
Bu yapı bizim mini makine dilimizdir. op hangi işlemin yapılacağını söyler, var değişkenin kimliğini, value ise sayısal argümanı taşır. Gerçek makinelerde bunlar baytlara sıkıştırılır; biz okunabilir kalsın diye struct kullandık.
Şimdi basit bir değişken tablosu yazalım. Oyuncak örnekte değişkenleri tek harf kabul etmek işi çok kolaylaştırır: a, b, x gibi. Böylece x - 'a' bize 0-25 arası bir indeks verir.
int var_id(const char *name) {
if (name[0] >= 'a' && name[0] <= 'z' && name[1] == '\0')
return name[0] - 'a';
return -1;
}
Parser tarafında satırı okuyup ilk kelimeye göre karar verebiliriz. Bu, tam teşekküllü bir parser değildir ama sözdizimi analizinin özünü gösterir: beklenen kalıba uyuyor mu?
int compile_line(char *line, Instruction *out) {
char cmd[16], name[16];
int value;
if (sscanf(line, "SET %15s %d", name, &value) == 2) {
out->op = OP_SET;
out->var = var_id(name);
out->value = value;
return out->var >= 0;
}
if (sscanf(line, "ADD %15s %d", name, &value) == 2) {
out->op = OP_ADD;
out->var = var_id(name);
out->value = value;
return out->var >= 0;
}
if (sscanf(line, "PRINT %15s", name) == 1) {
out->op = OP_PRINT;
out->var = var_id(name);
out->value = 0;
return out->var >= 0;
}
return 0;
}
Bu kodun eğlenceli yanı, sscanf ile minik bir dilbilgisi kontrolü yapmasıdır. Daha ciddi bir projede token üretir, ardından recursive descent parser yazardık. Örneğin expr -> term ((+|-) term)* gibi kurallarla matematiksel ifadeleri ayrıştırmak mümkün olurdu.
Son adımda bytecode’u çalıştıran sanal makineyi yazıyoruz:
void run(Instruction *code, int count) {
int memory[26] = {0};
for (int ip = 0; ip < count; ip++) {
Instruction in = code[ip];
switch (in.op) {
case OP_SET:
memory[in.var] = in.value;
break;
case OP_ADD:
memory[in.var] += in.value;
break;
case OP_PRINT:
printf("%d\n", memory[in.var]);
break;
case OP_HALT:
return;
}
}
}
Buradaki ip, instruction pointer’dır; yani sıradaki talimatın adresini gösterir. CPU’larda da benzer bir mantık vardır. Program sayacı ilerler, talimat çözülür, işlem yapılır. Bizim VM bunu C döngüsüyle taklit eder.
Örnek program şöyle olabilir:
SET x 10
ADD x 7
PRINT x
Derleme sonucunda yaklaşık olarak şu bytecode dizisi oluşur:
| Kaynak Satır | Bytecode |
|---|---|
SET x 10 |
OP_SET, 23, 10 |
ADD x 7 |
OP_ADD, 23, 7 |
PRINT x |
OP_PRINT, 23, 0 |
Çıktı doğal olarak 17 olur. Küçük ama güçlü bir fikir yakaladık: sözdizimi sadece metin kontrolü değildir; anlamlı eylemlere açılan kapıdır. Buradan sonra ifadeler, koşullar, döngüler ve hata mesajları ekleyerek mini dilinizi büyütebilirsiniz. Bir noktadan sonra fark edeceksiniz: derleyiciler büyü yapmıyor, sadece çok disiplinli bir çeviri işi gerçekleştiriyor.
Yorumlar