Takip et

Derleyici Geliştirme: Sözcüksel Analiz Nasıl Yapılır?

Kodunuzun bir bilgisayar tarafından anlaşılabilir ve çalıştırılabilir hale geldiğini hiç düşündünüz mü? Bu karmaşık sürecin ilk ve en temel adımlarından biri sözcüksel analizdir. Bu makale, derleyici geliştirme serimizin üçüncü bölümünde, sözcüksel analizin ne olduğunu, nasıl çalıştığını ve kendi derleyicinizin temelini atarken neden vazgeçilmez bir bileşen olduğunu detaylıca ele alacaktır. Hadi, bu heyecan verici dünyaya birlikte adım atalım ve kodun ilk dönüşümünü keşfedelim!

Yazılım geliştirme dünyasında, yazdığımız üst düzey kodların (C++, Python, Java vb.) makineler tarafından anlaşılan düşük seviyeli komutlara çevrilmesi gerekiyor. İşte bu çeviri işlemini gerçekleştiren yazılımlara derleyici (compiler) diyoruz. Derleyiciler, bu karmaşık görevi birkaç aşamaya bölerek yerine getirirler ve bu aşamaların ilki “Sözcüksel Analiz” veya “Lexical Analysis” olarak adlandırılır. Bir kod dosyasının ham metin girdisinden anlamlı parçacıklar çıkarmayı hedefleyen bu aşama, aslında derleyicinin kodla ilk tanıştığı noktadır. Bu yüzden de büyük bir titizlik ve doğrulukla yapılması hayati önem taşır. Sözcüksel analiz, adeta bir dildeki kelimeleri ve noktalama işaretlerini birbirinden ayıklama işlemine benzer; böylece daha sonraki aşamalarda bu parçalar üzerinde sentaktik ve semantik incelemeler yapılabilir.

Bu makalede, sözcüksel analiz sürecini sıfırdan ele alacak, temel kavramları açıklayacak ve kendi sözcüksel çözümleyicinizi (lexer) oluşturmak için pratik bilgiler sunacağız. Ayrıca, bu aşamada karşılaşılabilecek olası zorlukları ve performans ipuçlarını da inceleyeceğiz. Derleyici geliştirme serimizin bu önemli durağında edineceğiniz bilgiler, hem kendi programlama dilinizi oluşturma hayalinize bir adım daha yaklaştıracak hem de mevcut dillerin nasıl çalıştığına dair derinlemesine bir anlayış kazandıracaktır. Hazırsanız, kodun atomlarını keşfetmeye başlayalım!

Sözcüksel Analizin Temel Taşları: Token, Lexeme ve Ayraç Kavramları Nelerdir?

Sözcüksel analizin kalbinde yatan üç temel kavram vardır: “Token” (belirteç), “Lexeme” (sözcük birimi) ve “Delimiter” (ayraç). Bu kavramları anlamak, bir lexer’ın tam olarak ne iş yaptığını kavramanın anahtarıdır. Aslında, bu terimler yazılım mühendisliğinde sıkça karşımıza çıksa da, derleyici bağlamında özel anlamlar taşırlar ve kodun işlenmesinde kritik roller üstlenirler.

Token (Belirteç) Nedir?

Bir token, programlama dilinin en küçük anlamlı birimidir. Düşünün ki bir cümleyi oluşturan kelimeler gibidirler. Örneğin, bir değişkene atama yapan x = 10; ifadesinde, ‘x’, ‘=’, ’10’ ve ‘;’ her biri ayrı birer tokendir. Bir token sadece bir metin parçası değildir; aynı zamanda bir “tür” ve bazen de bir “değer” taşır. Örneğin, ‘x’ bir ‘IDENTIFIER’ (tanımlayıcı) türünde tokendir, ’10’ bir ‘INTEGER_LITERAL’ (tam sayı sabiti) türünde tokendir ve ‘=’ bir ‘ASSIGNMENT_OPERATOR’ (atama operatörü) türündedir. Bu tür bilgisi, derleyicinin sonraki aşamalarında (sözdizimsel analiz) hangi tokenin ne anlama geldiğini anlaması için hayati derecede önemlidir.

Tokenlerin yapısı genellikle bir tip (type) ve bir değer (value) içerir. Örneğin:

  • { type: "KEYWORD", value: "if" }
  • { type: "IDENTIFIER", value: "sayac" }
  • { type: "INTEGER_LITERAL", value: "100" }
  • { type: "OPERATOR", value: "+" }

Bu yapı, derleyicinin programın mantıksal akışını ve veri manipülasyonunu anlamasını sağlar.

Lexeme (Sözcük Birimi) Nedir?

Lexeme ise, kaynak kod içinde bir tokeni oluşturan gerçek metin parçasıdır. Yani, ‘IDENTIFIER’ türündeki tokenin lexeme’si ‘x’dir. ‘INTEGER_LITERAL’ türündeki tokenin lexeme’si ’10’dur. Aslında, lexeme’ler kaynak koddan doğrudan alınan dizelerdir ve tokenlar bu lexeme’lerin anlamsal etiketleridir. Bir lexer, kaynak kodu tarayarak lexeme’leri belirler ve bu lexeme’lere karşılık gelen tokenleri üretir. Aynı lexeme’e sahip farklı token türleri olabileceği gibi (örneğin, “a” bir değişken adı veya bir anahtar kelime olabilir), farklı lexeme’lere sahip aynı token türleri de olabilir (örneğin, “x” ve “y” her ikisi de tanımlayıcı tokenlerdir).

Delimiter (Ayraç) Nedir?

Ayraçlar, kaynak kodda lexeme’leri birbirinden ayırmak için kullanılan karakterlerdir. En yaygın ayraçlar boşluk (whitespace), sekme (tab), yeni satır karakterleri (newline) ve yorum satırlarıdır. Lexical analiz sırasında bu ayraçlar genellikle göz ardı edilir veya tek bir “whitespace” tokeni olarak kabul edilir ve genellikle sonraki aşamalara aktarılmazlar. Bununla birlikte, programlama dillerinde noktalı virgül (;), parantezler ((), {}, []) ve virgül (,) gibi karakterler de birer ayraç görevi görebilirler; ancak bunlar genellikle kendi başlarına birer operatör veya noktalama tokeni olarak işlenirler.

Uzman İpucu: Boşluklar ve yorum satırları genellikle lexer tarafından tamamen yok sayılır. Ancak, bazı dillerde (örneğin Python’da girintileme) boşlukların anlamsal bir önemi olabilir. Kendi dilinizi tasarlarken bu durumu göz önünde bulundurmalısınız.

Şimdi bir örnekle bu üç kavramı daha iyi anlayalım:


        int sayi = 20; // Bu bir atama islemidir
    

Bu kod parçacığında sözcüksel analiz şu tokenleri üretecektir:

Lexeme (Sözcük Birimi) Token Türü (Type) Açıklama
int KEYWORD Bir anahtar kelime (integer tipi)
sayi IDENTIFIER Bir tanımlayıcı (değişken adı)
= OPERATOR Bir operatör (atama operatörü)
20 INTEGER_LITERAL Bir tam sayı sabiti
; PUNCTUATION Bir noktalama işareti (ifade sonu)
// Bu bir atama islemidir COMMENT (ignored) Bir yorum satırı, genellikle yok sayılır

Gördüğünüz gibi, sözcüksel analiz kaynak kodu bir ham metin yığınından alıp, derleyicinin sonraki aşamalarında kolayca işleyebileceği yapılandırılmış bir token akışına dönüştürüyor. Bu ayrıştırma işlemi, derleyicinin programın sözdizimini ve anlambilimini anlaması için ilk ve en kritik adımı teşkil eder.

Düzenli İfadeler (Regular Expressions) ile Sözcükleri Tanıma: Pratik Bir Yaklaşım Nasıl Olur?

Sözcüksel analizde lexeme'leri tanımak için en güçlü ve yaygın kullanılan yöntemlerden biri düzenli ifadelerdir (Regular Expressions veya Regex). Regex, belirli bir desene uyan metin dizilerini tanımlamak için kullanılan özel bir karakter dizisidir. Bir lexer yazarken, her farklı token türü için (anahtar kelimeler, tanımlayıcılar, sayılar, operatörler vb.) bir düzenli ifade tanımlayarak, kaynak kod akışında bu desenleri arar ve eşleşen parçaları tokenlere dönüştürürüz. Bu yaklaşım, özellikle metin tabanlı verilerle uğraşırken inanılmaz derecede esneklik ve güç sağlar.

Regex Temelleri ve Lexer Uygulaması

Düzenli ifadeler, karmaşık karakter dizilerini kısa ve anlaşılır bir şekilde tanımlamamızı sağlar. Örneğin:

  • [a-zA-Z_][a-zA-Z0-9_]*: Bir tanımlayıcıyı (değişken veya fonksiyon adı) temsil eder. Bir harf veya alt çizgi ile başlar, ardından sıfır veya daha fazla harf, rakam veya alt çizgi gelebilir.
  • [0-9]+: Bir veya daha fazla rakamdan oluşan bir tam sayıyı temsil eder.
  • \/\/.*: Tek satırlık bir yorumu temsil eder (// ile başlar, ardından herhangi bir karakter gelebilir).
  • "([^"\\]|\\.)*": Tırnak içinde metin dizisini (string literal) temsil eder, kaçış karakterlerini de dikkate alır.
  • \+|-|\*|\/|=: Basit aritmetik operatörleri veya atama operatörünü temsil eder.

Bir lexer tasarlarken, bu düzenli ifadeleri belirli bir öncelik sırasına göre uygulamak önemlidir. Örneğin, anahtar kelimeler (if, while) genellikle tanımlayıcılardan önce kontrol edilmelidir, çünkü "if" kelimesi aslında tanımlayıcı regex'iyle de eşleşir. Ancak biz "if"i bir anahtar kelime olarak tanımak isteriz. Bu önceliklendirme, lexer'ın doğru tokenleri üretmesini sağlar.

Uzman İpucu: Karmaşık regex desenleri oluştururken, okuyuculuktan ödün vermemeye dikkat edin. Açıklamalar ve modüler yaklaşımlar, daha sonraki bakımları kolaylaştıracaktır. Ayrıca, bazı regex motorları "geri izleme" (backtracking) konusunda performans sorunları yaşayabilir. Basit ve doğrudan desenler tercih etmek genellikle daha verimlidir.

Vaka Analizi: Basit Bir Hesap Makinesi Dili Lexer'ı

Diyelim ki sadece toplama, çıkarma, çarpma ve bölme işlemleri yapabilen basit bir hesap makinesi dili için bir lexer yazıyoruz. Dilimiz şu token türlerini içerecek: NUMBER, PLUS, MINUS, MULTIPLY, DIVIDE, LPAREN, RPAREN, EOF (dosya sonu).

İşte bu dil için kullanabileceğimiz bazı düzenli ifade desenleri ve bir Python benzeri sözde kodla basit bir lexer örneği:


        import re

        # Token türleri ve onlara karşılık gelen regex desenleri
        # Öncelik sırası önemlidir: Uzun desenler kısa desenlerden önce gelmeli
        TOKEN_SPECS = [
            ('NUMBER',      r'\d+(\.\d*)?'),  # Tamsayı veya ondalıklı sayı
            ('PLUS',        r'\+'),         # Toplama operatörü
            ('MINUS',       r'-'),          # Çıkarma operatörü
            ('MULTIPLY',    r'\*'),         # Çarpma operatörü
            ('DIVIDE',      r'\/'),         # Bölme operatörü
            ('LPAREN',      r'\('),         # Sol parantez
            ('RPAREN',      r'\)'),         # Sağ parantez
            ('WHITESPACE',  r'\s+'),        # Boşluk karakterleri (genellikle atlanır)
            ('INVALID',     r'.')           # Tanınmayan herhangi bir karakter
        ]

        class Token:
            def __init__(self, type, value):
                self.type = type
                self.value = value

            def __repr__(self):
                return f"Token({self.type}, {repr(self.value)})"

        class Lexer:
            def __init__(self, text):
                self.text = text
                self.pos = 0
                self.tokens = []

            def tokenize(self):
                while self.pos < len(self.text):
                    match = None
                    for token_type, pattern in TOKEN_SPECS:
                        regex = re.compile(pattern)
                        m = regex.match(self.text, self.pos)
                        if m:
                            match = m
                            break

                    if match:
                        lexeme = match.group(0)
                        self.pos = match.end()

                        if token_type == 'WHITESPACE':
                            continue # Boşlukları atla
                        elif token_type == 'INVALID':
                            raise Exception(f"Geçersiz karakter: {lexeme} konum: {match.start()}")
                        else:
                            self.tokens.append(Token(token_type, lexeme))
                    else:
                        raise Exception(f"Beklenmeyen karakter: {self.text[self.pos]} konum: {self.pos}")

                self.tokens.append(Token('EOF', None)) # Dosya sonu belirteci
                return self.tokens

        # Kullanım örneği:
        # source_code = "10 + (2 * 5) - 3 / 1"
        # lexer = Lexer(source_code)
        # tokens = lexer.tokenize()
        # for token in tokens:
        #     console.log(token)
    

Yukarıdaki örnekte, TOKEN_SPECS listesi her bir token türü için bir düzenli ifade tanımlar. Lexer, kaynak kodu baştan sona tararken, her bir pozisyonda TOKEN_SPECS listesindeki desenleri sırayla dener. İlk eşleşen deseni bulduğunda, ilgili lexeme'i çıkarır, bir token oluşturur ve metin akışında ilerler. Boşluk karakterleri özel olarak ele alınır ve token listesine eklenmez. Tanınmayan bir karakterle karşılaşıldığında ise bir hata fırlatılır.

Sonlu Otomatlar (Finite Automata) ve Lexer Tasarımı: Makine Öğrenimi Mantığı Nasıl Uygulanır?

Düzenli ifadeler teorik olarak sonlu otomatlara (Finite Automata - FA) dayanır. Her düzenli ifade, eşdeğer bir sonlu otomat ile temsil edilebilir. Sonlu otomatlar, belirli bir durum kümesine ve bu durumlar arasında geçişleri tanımlayan kurallara sahip soyut makinelerdir. Lexer'lar, genellikle Durum Geçiş Diyagramları (State Transition Diagrams) olarak görselleştirilebilen bu otomatlar aracılığıyla lexeme'leri tanır.

Bir NFA (Non-deterministic Finite Automaton) veya DFA (Deterministic Finite Automaton) oluşturarak, her bir lexeme türü için nasıl bir karakter dizisi beklediğimizi modelleyebiliriz. Örneğin, bir tanımlayıcıyı tanımak için, 'başlangıç' durumundan bir harf veya alt çizgi ile 'tanımlayıcı_okunuyor' durumuna geçilir. Ardından, 'tanımlayıcı_okunuyor' durumunda iken harf, rakam veya alt çizgi gelmeye devam ettikçe bu durumda kalınır. Farklı bir karakter geldiğinde ise tanımlayıcı tamamlanmış olur ve bir 'IDENTIFIER' tokeni üretilir. Bu, lexer'ın "makine öğrenimi" gibi, girdi akışına göre durumunu değiştirmesi ve belirli bir deseni tanıması anlamına gelir.

Lex veya Flex gibi lexer üreteçleri, bize düzenli ifadeleri girdi olarak verip, otomatik olarak bu ifadeleri sonlu otomatlara dönüştüren ve ardından hızlı bir lexer kodu üreten araçlardır. Bu araçlar, elle bir lexer yazmaktan çok daha verimli ve hatasız bir şekilde lexer oluşturmamızı sağlar. Dolayısıyla, derinlemesine bir anlayış için sonlu otomat teorisini bilmek faydalı olsa da, pratik uygulamalarda hazır araçlar büyük kolaylık sağlar.

Hata Yönetimi ve Performans Optimizasyonu: Lexical Analyzer'da Karşılaşılan Zorluklar Nelerdir?

Bir lexer tasarlarken sadece doğru tokenleri üretmek yeterli değildir; aynı zamanda hataları doğru bir şekilde ele almak ve performansı optimize etmek de büyük önem taşır. Yanlış girilen kodlarla veya beklenmeyen karakterlerle karşılaşıldığında lexer'ın nasıl tepki vereceği, derleyicinin kullanıcı dostu olup olmadığını belirleyen kritik bir faktördür.

Etkin Hata Yönetimi Nasıl Yapılır?

Lexical analiz sırasında karşılaşılabilecek hatalar genellikle iki ana kategoriye ayrılır:

  1. Geçersiz Karakterler: Programlama dilinin sözdiziminde tanımlanmamış, anlamsız karakterler (örneğin, bir C dilinde $ karakterinin beklenmeyen bir yerde kullanılması).
  2. Tanınmayan Deseler: Dilin kurallarına uymayan, ancak tek tek karakterleri geçerli olabilecek dizeler (örneğin, if_then yerine if-then yazılması, burada - operatör olabilir ama if-then bir anahtar kelime veya tanımlayıcı değildir).

İyi bir hata yönetimi stratejisi şunları içermelidir:

  • Hata Raporlama: Hatanın tipini, konumunu (satır ve sütun numarası) açıkça belirten mesajlar sağlamak. Bu, geliştiricilerin sorunu hızla bulup düzeltmesine yardımcı olur.
  • Hata Kurtarma: Bir hata ile karşılaşıldığında lexer'ın tamamen durmak yerine, hatayı rapor edip mümkünse analizine devam etmeye çalışması. Örneğin, geçersiz bir karakteri atlayıp sonraki geçerli tokeni araması. Bu, derleyicinin bir seferde birden fazla hata bulmasını sağlayabilir.
  • Uyarılar: Kodu derlenebilir kılmaya yetecek ancak yine de potansiyel sorunlara işaret eden durumlar için uyarılar vermek.

        // Hata raporlama örneği (sözde kod)
        function reportError(message, line, column) {
            console.error(Lexical Hata [Satır ${line}, Sütun ${column}]: ${message});
        }

        // Lexer içerisinde
        // ...
        // if (match is None):
        //     reportError("Beklenmeyen karakter", currentLine, currentColumn);
        //     self.pos += 1; // Hatalı karakteri atla ve devam et
        // ...
    

Uzman İpucu: Hata mesajlarınızı kullanıcı dostu tutun. "Syntax Error" yerine "Tanımlayıcı bekliyordum ama '123' buldum" gibi daha açıklayıcı mesajlar, geliştiricinin işini çok kolaylaştırır.

Performans Optimizasyonu Teknikleri

Derleyiciler genellikle büyük kod tabanlarını işledikleri için, lexical analiz aşamasının hızlı olması önemlidir. İşte bazı optimizasyon ipuçları:

  • Girdi Tamponlama (Input Buffering): Kaynak kodu bloklar halinde okuyup bellekte tutmak, her karakter okuması için disk I/O'sunu önleyerek performansı artırır. Bu, özellikle büyük dosyalarla çalışırken fark yaratır.
  • Etkili Regex Motorları: Kullandığınız düzenli ifade motorunun (örneğin Python'ın re modülü veya C++'ın std::regex'i) performansı önemlidir. Bazı dillerde yerleşik regex motorları yavaş olabilir; bu durumda harici kütüphaneler (örneğin Google'ın RE2 kütüphanesi) veya özel olarak yazılmış, daha performanslı DFA tabanlı tanıyıcılar tercih edilebilir.
  • Önceliklendirilmiş Desen Eşleştirme: En sık karşılaşılan token desenlerini (örneğin tanımlayıcılar, sayılar) regex listesinin en başına koyarak, lexer'ın daha hızlı eşleşme bulmasını sağlayabilirsiniz.
  • Önbellekleme: Bazı lexer'lar, sık kullanılan token desenleri için önceden derlenmiş regex nesnelerini önbelleğe alarak tekrar tekrar derleme maliyetinden kaçınır.
  • Manuel Lexer Yazımı: Çok yüksek performans gerektiren durumlarda, otomatik lexer üreteçleri yerine elle yazılmış durum makineleri (state machines) daha hızlı olabilir. Ancak bu yaklaşım daha fazla kod ve hata potansiyeli anlamına gelir.

Vaka Analizi: Büyük Bir Projede Hata Yönetimi

Büyük bir yazılım projesinde, örneğin 100.000 satırdan fazla kodu olan bir uygulama geliştirirken, lexical hataların tespiti ve yönetimi hayati önem taşır. Eğer lexer yalnızca ilk hatada durup rapor verseydi, geliştiricinin her hatayı düzelttikten sonra tekrar derleme yapması gerekirdi. Bu, geliştirme sürecini ciddi şekilde yavaşlatır. Modern derleyiciler, bu tür durumlar için genellikle hata kurtarma mekanizmalarını devreye sokar. Örneğin, geçersiz bir karakter dizisiyle karşılaşıldığında, lexer o karakter dizisini "INVALID_TOKEN" olarak işaretler, hata mesajını kaydeder ve bir sonraki potansiyel geçerli tokeni aramaya devam eder. Böylece, tek bir derleme işleminde birden fazla lexical hatanın raporlanması sağlanır, bu da geliştiricinin daha az iterasyonla sorunları çözmesine olanak tanır.

Sözcüksel Analiz Aracının Yapısal Şablonu: Kendi Lexer'ınızı Nasıl Oluşturursunuz?

Kendi lexer'ınızı oluşturmak, sözcüksel analizin temel prensiplerini pekiştirmenin en iyi yoludur. Bu bölümde, basit bir lexer'ın temel yapı taşlarını ve adım adım nasıl oluşturulacağını inceleyeceğiz. Amacımız, herhangi bir programlama dilinde uygulanabilecek genel bir şablon sunmaktır.

Lexer'ın Ana Bileşenleri

Bir lexer genellikle aşağıdaki ana bileşenleri içerir:

  1. Girdi Akışı (Input Stream): Kaynak kodun metin halindeki girdisi. Bu genellikle bir dosya içeriği veya bir string olabilir.
  2. Okuma İşaretçisi (Pointer/Cursor): Girdi akışında o an hangi karakterin işlendiğini gösteren bir konum bilgisi (genellikle bir indeks).
  3. Token Tanımları (Token Definitions): Dilin anahtar kelimeleri, operatörleri, tanımlayıcıları, sayıları vb. için düzenli ifadeler veya eşleşme kuralları.
  4. getNextToken() Metodu: Girdi akışından bir sonraki geçerli tokeni bulan, oluşturup döndüren ana metod.
  5. Hata Yönetimi (Error Handling): Tanınmayan karakterler veya geçersiz desenlerle karşılaşıldığında hata mesajları üreten mekanizma.

Şimdi, basit bir programlama dili için (sadece matematiksel ifadeler ve değişkenler) bir lexer'ın temel iskeletini adım adım oluşturalım. Bu örnekte JavaScript benzeri bir sözde kod kullanacağız.

Adım 1: Token Türlerini Tanımlama

İlk olarak, dilimizde bulunacak tüm token türlerini (TokenType) belirlememiz gerekir. Bu, lexer'ın ve daha sonraki ayrıştırma aşamalarının çalışacağı temel yapı taşlarıdır.


        // Token Türleri
        const TokenType = {
            IDENTIFIER: 'IDENTIFIER',       // Değişken isimleri (örn: 'x', 'sayac')
            NUMBER: 'NUMBER',               // Sayılar (örn: '10', '3.14')
            PLUS: 'PLUS',                   // Toplama operatörü '+'
            MINUS: 'MINUS',                 // Çıkarma operatörü '-'
            MULTIPLY: 'MULTIPLY',           // Çarpma operatörü '*'
            DIVIDE: 'DIVIDE',               // Bölme operatörü '/'
            ASSIGN: 'ASSIGN',               // Atama operatörü '='
            LPAREN: 'LPAREN',               // Sol parantez '('
            RPAREN: 'RPAREN',               // Sağ parantez ')'
            SEMICOLON: 'SEMICOLON',         // Noktalı virgül ';'
            EOF: 'EOF'                      // Dosya Sonu (End Of File)
        };

        // Token sınıfı
        class Token {
            constructor(type, value) {
                this.type = type;
                this.value = value;
            }

            toString() {
                return Token(${this.type}, ${this.value !== null ? JSON.stringify(this.value) : 'null'});
            }
        }
    

Adım 2: Lexer Sınıfını Oluşturma

Girdi metnini işleyecek ana lexer sınıfını tanımlayalım. Bu sınıf, girdi metnini tutacak, mevcut okuma pozisyonunu yönetecek ve karakter okuma işlemleri için yardımcı metotlara sahip olacaktır.


        class Lexer {
            constructor(input) {
                this.input = input;
                this.position = 0; // Girdi akışındaki mevcut karakterin indeksi
                this.line = 1;     // Mevcut satır numarası
                this.column = 0;   // Mevcut sütun numarası
            }

            // Mevcut karakteri döndürür
            peek() {
                return this.position < this.input.length ? this.input[this.position] : null;
            }

            // Mevcut karakteri döndürür ve pozisyonu bir artırır
            advance() {
                const char = this.peek();
                if (char === '\n') {
                    this.line++;
                    this.column = 0;
                } else {
                    this.column++;
                }
                this.position++;
                return char;
            }

            // Belirli bir deseni atlar (örn: boşluklar)
            skipWhitespace() {
                while (this.peek() !== null && /\s/.test(this.peek())) {
                    this.advance();
                }
            }

            // Hata raporlama
            error(message) {
                throw new Error(Lexer Hatası [Satır ${this.line}, Sütun ${this.column}]: ${message});
            }

            // ... getNextToken metodu buraya gelecek ...
        }
    

Adım 3: getNextToken() Metodunu Uygulama

Bu, lexer'ın kalbidir. Kaynak kodu karakter karakter tarayarak, tanımlanan desenlere uyan lexeme'leri bulur ve bunlara karşılık gelen tokenleri oluşturur. Burada düzenli ifadeler veya elle yazılmış durum makineleri kullanılabilir.


        class Lexer {
            // ... önceki kodlar ...

            getNextToken() {
                this.skipWhitespace(); // Her tokendan önce boşlukları atla

                if (this.position >= this.input.length) {
                    return new Token(TokenType.EOF, null);
                }

                const char = this.peek();

                // Sayıları oku
                if (/\d/.test(char)) {
                    let value = '';
                    while (this.peek() !== null && /\d/.test(this.peek())) {
                        value += this.advance();
                    }
                    // Ondalıklı sayıları da destekle
                    if (this.peek() === '.') {
                        value += this.advance();
                        while (this.peek() !== null && /\d/.test(this.peek())) {
                            value += this.advance();
                        }
                    }
                    return new Token(TokenType.NUMBER, parseFloat(value));
                }

                // Tanımlayıcıları ve Anahtar Kelimeleri oku
                if (/[a-zA-Z_]/.test(char)) {
                    let value = '';
                    while (this.peek() !== null && /[a-zA-Z0-9_]/.test(this.peek())) {
                        value += this.advance();
                    }
                    // Anahtar kelime kontrolü (basit bir dilde henüz anahtar kelime yok, ama eklenebilir)
                    // if (value === 'if') return new Token(TokenType.IF, value);
                    // ...
                    return new Token(TokenType.IDENTIFIER, value);
                }

                // Operatörler ve Noktalama işaretleri
                switch (char) {
                    case '+': this.advance(); return new Token(TokenType.PLUS, '+');
                    case '-': this.advance(); return new Token(TokenType.MINUS, '-');
                    case '*': this.advance(); return new Token(TokenType.MULTIPLY, '*');
                    case '/': this.advance(); return new Token(TokenType.DIVIDE, '/');
                    case '=': this.advance(); return new Token(TokenType.ASSIGN, '=');
                    case '(': this.advance(); return new Token(TokenType.LPAREN, '(');
                    case ')': this.advance(); return new Token(TokenType.RPAREN, ')');
                    case ';': this.advance(); return new Token(TokenType.SEMICOLON, ';');
                    default: this.error(Geçersiz karakter: '${char}');
                }
            }
        }
    

Bu temel yapı, lexer'ınız için güçlü bir başlangıç noktasıdır. Karmaşıklık arttıkça, getNextToken() metodu daha karmaşık hale gelebilir veya düzenli ifade motorları (örneğin re modülü veya üçüncü taraf bir kütüphane) kullanılarak basitleştirilebilir.

Kullanım Örneği:


        // const sourceCode = "sayac = (10 + 5) * 2;";
        // const lexer = new Lexer(sourceCode);
        // let token;
        // do {
        //     token = lexer.getNextToken();
        //     console.log(token.toString());
        // } while (token.type !== TokenType.EOF);
        // /* Çıktı:
        // Token(IDENTIFIER, "sayac")
        // Token(ASSIGN, "=")
        // Token(LPAREN, "(")
        // Token(NUMBER, 10)
        // Token(PLUS, "+")
        // Token(NUMBER, 5)
        // Token(RPAREN, ")")
        // Token(MULTIPLY, "*")
        // Token(NUMBER, 2)
        // Token(SEMICOLON, ";")
        // Token(EOF, null)
        // */
    

Bu örnekte, elle yazılmış bir lexer'ın temel mantığını görüyorsunuz. Her adımda bir karakter okuyup, bir sonraki tokeni tahmin etmeye çalışıyor. Girdi metni karmaşıklaştıkça, bu manuel yaklaşımın yerini daha çok düzenli ifade tabanlı veya otomatik lexer üreteçleri alır. Kendi dilinizin ihtiyaçlarına göre bu yapıyı genişletebilir ve geliştirebilirsiniz.

Sonuç: Derleyici Geliştirmenin İlk Adımı Tamamlandı

Derleyici geliştirme serimizin bu üçüncü bölümünde, sözcüksel analizin büyüleyici dünyasına derinlemesine bir dalış yaptık. Artık, kaynak kodun ham metin formundan anlamlı tokenler akışına nasıl dönüştürüldüğünü, bu dönüşümün arkasındaki temel kavramlar olan token, lexeme ve ayracı net bir şekilde anlıyorsunuz. Düzenli ifadelerin bu süreçteki kritik rolünü, sonlu otomatlarla olan ilişkisini ve kendi lexer'ınızı oluştururken izlemeniz gereken adımları pratik örneklerle ele aldık. Ayrıca, hata yönetimi ve performans optimizasyonu gibi önemli konulara değinerek, sağlam ve etkili bir lexer geliştirmenin ipuçlarını da paylaştık.

Sözcüksel analiz, bir derleyicinin inşaatının temelini oluşturur. Bu aşama olmadan, programın sözdizimini kontrol etmek veya anlamsal anlamını çıkarmak imkansız hale gelirdi. Şimdi, elinizde programlama dilinizin "kelimeleri" var. Bir sonraki adımda, bu kelimeleri kullanarak "cümleleri" nasıl oluşturacağımızı, yani sözdizimsel analizi (parsing) inceleyeceğiz. Bu, derleyici geliştirme yolculuğunuzda attığınız büyük bir adımdır ve sizi kendi programlama dilinizi yaratma hedefinize bir adım daha yaklaştırmaktadır. Unutmayın, her büyük yazılım projesi, böyle küçük ve temel adımlarla başlar!

Uzman İpucu: Makalenin HTML yapısının mobil cihazlarda düzgün görünmesini sağlamak için CSS media query'ler kullanabilirsiniz. Örneğin, küçük ekranlar için font boyutlarını küçültmek veya tablo düzenlerini değiştirmek gibi ayarlamalar yapılabilir. Bu, kullanıcı deneyimini önemli ölçüde artırır.

Sıkça Sorulan Sorular (SSS)

S1: Lexical analiz neden gerekli?
C1: Lexical analiz, kaynak kodun ham metin halindeki girdisini, derleyicinin sonraki aşamalarında (sözdizimsel ve anlamsal analiz) işleyebileceği anlamlı tokenlere ayırmak için gereklidir. Bu, kodun yapılandırılmış bir şekilde anlaşılmasını ve işlenmesini sağlar.
S2: Token ve lexeme arasındaki fark nedir?
C2: Lexeme, kaynak kodda bir tokeni oluşturan gerçek metin parçasıdır (örneğin, "if"). Token ise bu lexeme'e verilen anlamsal etikettir ve bir tür (TokenType) taşır (örneğin, {type: KEYWORD, value: "if"}). Lexeme "nedir", token "ne anlama gelir" sorusuna cevap verir.
S3: Düzenli ifadeler ne kadar karmaşık olabilir?
C3: Düzenli ifadeler oldukça karmaşık olabilir ve birçok modern programlama dilinin sözdizimini tanımlamak için yeterince güçlüdür. Ancak, çok karmaşık düzenli ifadeler okunabilirliği ve performansı olumsuz etkileyebilir. Genellikle karmaşık durumlar için birden fazla basit düzenli ifade kombinasyonu veya Sonlu Otomatlar kullanılır.
S4: Hata yönetimi neden önemlidir?
C4: Hata yönetimi, derleyicinin kullanıcı dostu olmasını sağlar. Doğru hata mesajları (konum bilgisiyle birlikte) geliştiricilerin kodlarındaki sorunları hızla bulup düzeltmelerine yardımcı olur. İyi bir hata yönetimi, derleyicinin tek bir derleme işleminde birden fazla hatayı raporlamasına da olanak tanır.
S5: Bir lexer yazmak için hangi araçlar kullanılabilir?
C5: Elle yazılmış lexer'lar küçük diller için yeterli olabilirken, daha karmaşık diller için Flex (C/C++), JFlex (Java), Lex/Yacc veya Python'daki PLY gibi lexer üreteçleri veya kütüphaneleri kullanılabilir. Bu araçlar, düzenli ifadelerden otomatik olarak lexer kodu oluşturarak geliştirme sürecini hızlandırır.


Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.