Lexer–Parser Mimarisi: Kaynak Kod Nasıl Anlam Kazanır?

Bilgisayar için toplam = 5 + 3 ifadesi başlangıçta yalnızca karakterlerden oluşan bir dizidir. İnsan gözü bunun bir değişkene toplama sonucunu atadığını hemen anlar; işlemciyse büyücü değildir. Karakterlerin önce sözcüklere, ardından yapısal bir modele dönüştürülmesi gerekir. İşte lexer ve parser, kaynak kod ile anlam arasındaki bu köprünün iki ana mimarıdır.

``

Büyük resim: Karakterden anlama

Tipik bir dil işleme hattı şu şekilde ilerler:

Kaynak kod → Lexer → Token dizisi → Parser → AST → Anlamsal analiz → Çalıştırma/Kod üretimi

Lexer ve parser bazen tek bir araç gibi düşünülür. Oysa farklı sorulara cevap verirler:

Bileşen Temel sorusu Girdi Çıktı
Lexer “Buradaki sözcükler neler?” Karakterler Token dizisi
Parser “Bu sözcükler geçerli bir yapı oluşturuyor mu?” Token dizisi Parse tree veya AST
Anlamsal analiz “Bu yapı gerçekten anlamlı mı?” AST Doğrulanmış/açıklanmış AST

Örneğin 12 + fiyat metni lexer tarafından NUMBER(12), PLUS, IDENTIFIER(fiyat) biçiminde parçalanır. Boşluklar çoğunlukla atılır; fakat Python gibi girintinin anlam taşıdığı dillerde boşluk bile token üretebilir.

Lexer: Karakter avcısı

Lexer, soldan sağa ilerleyerek belirli karakter örüntülerini tanır. Sayılar için [0-9]+, değişken adları için [a-zA-Z_][a-zA-Z0-9_]* benzeri düzenli ifadeler kullanılabilir. Kuramsal olarak lexer kuralları çoğunlukla düzenli dillerle ve sonlu durum makineleriyle modellenir.

$n$ karakterlik bir kaynak metin tek geçişte taranıyorsa ideal zaman karmaşıklığı yaklaşık $O(n)$ olur. Her karakter yalnızca birkaç kez incelendiği için lexer oldukça hızlıdır.

Aşağıdaki küçük Python fonksiyonu sayı, toplama işareti ve isimleri token hâline getirir:

import re

TOKEN_RULES = [
    ("NUMBER", r"\d+"),
    ("PLUS", r"\+"),
    ("IDENTIFIER", r"[A-Za-z_]\w*"),
    ("SPACE", r"\s+"),
]

def tokenize(source):
    pattern = "|".join(
        f"(?P<{name}>{rule})" for name, rule in TOKEN_RULES
    )

    for match in re.finditer(pattern, source):
        token_type = match.lastgroup
        value = match.group()
        if token_type != "SPACE":
            yield token_type, value

print(list(tokenize("12 + fiyat")))

Kod, kuralları isimlendirilmiş gruplar hâlinde birleştirir. Eşleşen grubun adı token türünü, eşleşen metin ise token değerini verir.

Parser: Dilbilgisi hakemi

Tokenların bulunması tek başına yeterli değildir. 12 + + fiyat dizisindeki her parça tanınabilir; ancak bütün ifade dilbilgisel olarak hatalıdır. Parser, tokenların grammar adı verilen kurallara uyup uymadığını denetler.

Basit bir toplama dili şöyle tanımlanabilir:

expression → term (PLUS term)*
term       → NUMBER | IDENTIFIER

Bu kurala göre ifade bir term ile başlamalı, ardından sıfır veya daha fazla PLUS term çifti gelmelidir. Parser başarılı olduğunda genellikle gereksiz noktalama ayrıntılarını dışarıda bırakan bir Soyut Sözdizimi Ağacı, yani AST üretir:

Add
├── Number(12)
└── Identifier(fiyat)

Ağaç yapısı işlem önceliğini de görünür kılar. 2 + 3 * 4 ifadesinin doğru yorumu $2 + (3 \times 4) = 14$ olmalıdır; $(2 + 3) \times 4 = 20$ değil. Grammar, çarpma düğümünün toplama düğümünden daha derinde yer almasını sağlayarak bu farkı kodlar.

Sözdizimi anlam demek değildir

yas + 1 sözdizimsel olarak geçerli olabilir. Ancak yas tanımlanmamışsa veya metin türündeyse anlamsal analiz itiraz eder. Bu aşamada sembol tabloları, kapsam kuralları ve tür denetimi devreye girer.

Örnek Lexer Parser Anlamsal analiz
3 + 4 Başarılı Başarılı Başarılı
3 + + 4 Başarılı Hata Çalışmaz
olmayan + 4 Başarılı Başarılı Tanımsız isim hatası
"merhaba" - 2 Başarılı Başarılı Tür hatası olabilir

Kısacası lexer kodun kelimelerini bulur, parser bu kelimelerle cümle kurar, anlamsal analiz ise cümlenin mantıklı olup olmadığını sorgular. Derleyiciler, yorumlayıcılar, IDE renklendirmeleri ve kod analiz araçları bu katmanlardan yararlanır. Kaynak kodun “anlam kazanması” tek bir sihirli adım değil, düzenli ve denetlenebilir bir dönüşüm zinciridir.

lexerparser-mimarisi-kaynak-24

Yorumlar