Lexers, ana ayrıştırıcı için performans optimizasyonu olarak kullanılan basit ayrıştırıcılardır. Bir lexer'ımız varsa, lexer ve ayrıştırıcı tüm dili tanımlamak için birlikte çalışır. Ayrı bir lexing aşaması olmayan ayrıştırıcılara bazen "tarayıcısız" denir.
Lexers olmasaydı, ayrıştırıcı karakter karakter bazında çalışmak zorunda kalacaktı. Ayrıştırıcı, her girdi öğesi hakkında meta veriler depolaması ve her girdi öğesi durumu için tabloları önceden hesaplaması gerekebileceğinden, bu, büyük girdi boyutları için kabul edilemez bellek tüketimine neden olur. Özellikle, soyut sözdizimi ağacında karakter başına ayrı bir düğüme ihtiyacımız yok.
Karakter karakter bazında metin oldukça belirsiz olduğu için, bu da ele alınması can sıkıcı bir durumdur. Bir kural düşünün R → identifier | "for " identifier
. burada tanımlayıcı ASCII harflerinden oluşur. Belirsizlikten kaçınmak istiyorsam, hangi alternatifin seçilmesi gerektiğini belirlemek için şimdi 4 karakterli bir ileriye ihtiyacım var. Bir lexer ile, ayrıştırıcı sadece bir IDENTIFIER veya FOR jetonu olup olmadığını kontrol etmelidir - 1 jetonlu bir ileri okuma.
İki seviyeli dilbilgisi.
Lexers, giriş alfabesini daha uygun bir alfabeye çevirerek çalışır.
Tarayıcısız bir ayrıştırıcı, bir dilbilgisi (N, Σ, P, S) tanımlar; burada terminal olmayan N, dilbilgisindeki kuralların sol taraflarıdır, Σ alfabesi örneğin ASCII karakteridir, P yapımları dilbilgisindeki kurallardır ve başlangıç sembolü S, ayrıştırıcının en üst düzey kuralıdır.
Lexer şimdi a, b, c,… belirteçlerinin bir alfabesini tanımlar. Bu, ana ayrıştırıcının şu belirteçleri alfabe olarak kullanmasına izin verir: Σ = {a, b, c,…}. Lexer için bu belirteçler terminal olmayanlardır ve başlangıç kuralı S L S L → ε | a S | b S | c S | …, Yani: herhangi bir token dizisi. Sözlük dilbilgisindeki kurallar, bu simgeleri üretmek için gerekli kurallardır.
Performans avantajı, lexer kurallarını normal bir dil olarak ifade etmekten gelir . Bunlar, bağlamdan bağımsız dillerden çok daha verimli bir şekilde ayrıştırılabilir. Özellikle, normal diller O (n) uzayda ve O (n) zamanda tanınabilir. Pratikte, bir kod üreticisi böyle bir lexer'ı yüksek verimli atlama tablolarına dönüştürebilir.
Dilbilginizden token çıkarma.
Örneğinize dokunmak için: digit
ve string
kuralları karakter karakter düzeyinde ifade edilir. Bunları jeton olarak kullanabiliriz. Dilbilgisinin geri kalanı bozulmadan kalır. Düzenli olduğunu açıkça belirtmek için sağ doğrusal bir dilbilgisi olarak yazılan lexer dilbilgisi:
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"' , string-rest ;
string-rest = '"' | STRING-CHAR, string-rest ;
STRING-CHAR = ? all visible characters ? - '"' ;
Ancak düzenli olduğu için, jeton sözdizimini ifade etmek için genellikle düzenli ifadeler kullanırız. Aşağıda, .NET karakter sınıfı hariç tutma sözdizimi ve POSIX çizelgeleri kullanılarak yazılan regexes olarak jeton tanımları verilmiştir:
digit ~ [0-9]
string ~ "[[:print:]-["]]*"
Ana ayrıştırıcı dilbilgisi daha sonra lexer tarafından ele alınmayan kalan kuralları içerir. Sizin durumunuzda, bu sadece:
input = digit | string ;
Lexers kolayca kullanılamadığında.
Bir dil tasarlarken, dilbilgisinin bir lexer düzeyine ve ayrıştırıcı düzeyine temiz bir şekilde ayrılabilmesine ve lexer düzeyinin normal bir dil tanımlamasına özen gösteririz. Bu her zaman mümkün değil.
Dilleri yerleştirirken. Bazı diller, dizeleri içine kodunu interpole sağlar: "name={expression}"
. İfade sözdizimi, bağlamdan bağımsız dilbilgisinin bir parçasıdır ve bu nedenle normal bir ifade ile belirtilemez. Bunu çözmek için ayrıştırıcıyı lexer ile yeniden birleştiririz veya gibi ek jetonlar sunarız STRING-CONTENT, INTERPOLATE-START, INTERPOLATE-END
. Bir dize için dilbilgisi kuralı sonra gibi görünebilir: String → STRING-START STRING-CONTENTS { INTERPOLATE-START Expression INTERPOLATE-END STRING-CONTENTS } STRING-END
. Tabii ki İfade bizi bir sonraki soruna götüren başka dizeler içerebilir.
Jetonlar birbirlerini ne zaman içerebilir? C benzeri dillerde, anahtar kelimeler tanımlayıcılardan ayırt edilemez. Bu, sözlükte anahtar kelimelerin tanımlayıcılara göre önceliklendirilmesiyle çözülür. Böyle bir strateji her zaman mümkün değildir. Line → IDENTIFIER " = " REST
Gerisi bir tanımlayıcı gibi görünse bile, kalanın satır sonuna kadar herhangi bir karakter olduğu bir yapılandırma dosyası düşünün . Örnek bir çizgi olabilir a = b c
. Lexer gerçekten aptal ve belirteçlerin hangi sırada olabileceğini bilmiyor. Dolayısıyla, RESTOR yerine IDENTIFIER'a öncelik verirsek, lexer bize verirdi IDENT(a), " = ", IDENT(b), REST( c)
. REST (TANITICI) yerine öncelik verirsek, lexer bize verirdi REST(a = b c)
.
Bunu çözmek için lexer'ı ayrıştırıcıyla yeniden birleştirmeliyiz. Ayrılma, lexer tembel hale getirilerek bir şekilde korunabilir: ayrıştırıcının bir sonraki jetona her ihtiyacı olduğunda, lexer'dan ister ve lexer'a kabul edilebilir jeton kümesini söyler. Etkili bir şekilde, her pozisyon için lexer dilbilgisi için yeni bir üst düzey kural oluşturuyoruz. Burada, aramalarla sonuçlanır nextToken(IDENT), nextToken(" = "), nextToken(REST)
ve her şey yolunda gider. Bu, her yerde LR gibi aşağıdan yukarıya ayrıştırıcı anlamına gelen kabul edilebilir belirteçlerin tamamını bilen bir ayrıştırıcı gerektirir.
Lexer devleti korumak zorunda olduğunda. Python dili kod bloklarını kıvırcık parantezlerle değil, girinti ile sınırlar. Bir dilbilgisi içinde düzene duyarlı sözdizimini ele almanın yolları vardır, ancak bu teknikler Python için aşırı doldurulur. Bunun yerine, lexer her satırın girintisini kontrol eder ve yeni bir girintili blok bulunursa INDENT belirteçleri ve blok bitmişse DEDENT belirteçleri yayar. Bu, ana dilbilgisini basitleştirir çünkü artık bu tokenlerin kıvırcık parantez gibi olduğunu iddia edebilir. Ancak lexer şu anki durumu korumalıdır: mevcut girinti. Bu, lexer'ın teknik olarak artık normal bir dili değil, aslında bağlama duyarlı bir dili tanımladığı anlamına gelir. Neyse ki bu fark pratikte geçerli değildir ve Python'un lexer hala O (n) zamanında çalışabilir.