Bir dilbilgisine dayalı bir lexer yazarken izlenen prosedür nedir?


13

Dilbilgisi, Lexers ve Parsers hakkında açıklama konusundaki bir cevabı okurken , cevap şunları söyledi:

[...] bir BNF dilbilgisi sözlüksel analiz ve ayrıştırma için ihtiyacınız olan tüm kuralları içerir.

Bu benim için biraz garip geldi, çünkü şimdiye kadar, bir lexer'ın bir gramer üzerine hiç dayanmadığını düşünürken, bir ayrıştırıcı büyük ölçüde bir gramer üzerine dayanıyordu. Bu sonuca, lexers yazma hakkında çok sayıda blog yazısı okuduktan sonra geldim ve hiç biri tasarım için bir temel olarak 1 EBNF / BNF kullanmıyordu .

Lexers ve ayrıştırıcılar EBNF / BNF dilbilgisine dayanıyorsa, bu yöntemi kullanarak bir lexer oluşturmaya ne dersiniz? Yani, belirli bir EBNF / BNF dilbilgisi kullanarak nasıl bir sözlük oluşturabilirim?

EBNF / BNF'yi bir rehber veya bir plan olarak kullanarak bir ayrıştırıcı yazmayla ilgili çok, çok sayıda yayın gördüm , ancak şu ana kadar lexer tasarımı ile eşdeğer olan hiçbiriyle karşılaşmadım.

Örneğin, aşağıdaki dilbilgisini alın:

input = digit| string ;
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"', { all characters - '"' }, '"' ;
all characters = ? all visible characters ? ;

Dilbilgisine dayanan bir sözlük nasıl oluşturulur? Bir ayrıştırıcının böyle bir gramerden nasıl yazılacağını hayal edebildim, ama aynı şeyi bir lexer ile yapma kavramını kavrayamıyorum.

Ayrıştırıcı yazarken olduğu gibi böyle bir görevi yerine getirmek için kullanılan belirli kurallar veya mantık var mı? Açıkçası, lexer tasarımlarının tek başına bir EBNF / BNF dilbilgisi kullanıp kullanmadığını merak etmeye başlıyorum.


1 Genişletilmiş Backus – Naur formu ve Backus – Naur formu

Yanıtlar:


18

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: digitve stringkuralları 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 " = " RESTGerisi 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.


Çok güzel cevap @ amon, Teşekkürler. Tamamen sindirmek için biraz zaman ayırmam gerekecek. Ancak cevabınız hakkında birkaç şey merak ediyordum. Sekizinci paragrafın çevresinde, örnek EBNF dilbilgimi bir ayrıştırıcı için kurallar olarak nasıl değiştirebileceğimi gösteriyorsunuz. Gösterdiğiniz dilbilgisi ayrıştırıcı tarafından da kullanılacak mı? Yoksa ayrıştırıcı için hala ayrı bir dilbilgisi var mı?
Christian Dean

@Mühendisim Birkaç düzenleme yaptım. EBNF'niz doğrudan bir ayrıştırıcı tarafından kullanılabilir. Ancak benim örneğim, dilbilgisinin hangi bölümlerinin ayrı bir sözlük tarafından ele alınabileceğini göstermektedir. Diğer kurallar yine de ana ayrıştırıcı tarafından ele alınacaktır, ancak örneğinizde bu sadece input = digit | string.
amon

4
Tarayıcısız ayrıştırıcıların en büyük avantajı, oluşturmanın çok daha kolay olmasıdır; Bunun uç örnek hiçbir şey yapmıyorsunuz ayrıştırıcı combinator kütüphaneleri vardır ama oluştur ayrıştırıcıları. Ayrıştırıcılar oluşturmak, şablonda bir dil ile SQL ile serpiştirilmiş HTML içinde gömülü ECMAScript gömülü-HTML içine gömülü veya Markdown'da gömülü Ruby örnekleri gibi durumlar için ilginçtir. gömülü Ruby-belgeleme-yorumlar ya da bunun gibi bir şey.
Jörg W Mittag

Son mermi noktası çok önemli, ancak yazdığınız yolun yanıltıcı olduğunu hissediyorum. Lexers'ın girinti tabanlı bir sözdizimi ile kolayca kullanılamayacağı doğrudur, ancak bu durumda tarayıcısız ayrıştırma daha da zordur. Yani bu tür bir diliniz varsa, ilgili durumla güçlendirerek bir lexer kullanmak istersiniz .
user541686

@Mehrdad Python tarzı lexer güdümlü girinti / gizli belirteçler yalnızca girintiye duyarlı çok basit diller için mümkündür ve genellikle uygulanamaz. Daha genel bir alternatif nitelik gramerleridir, ancak standart araçlarda destekleri yoktur. Fikir, her AST parçasına girintisi ile açıklama eklememiz ve tüm kurallara kısıtlamalar eklememizdir. Özelliklerin birleştirici ayrıştırma ile eklenmesi kolaydır, bu da tarayıcısız ayrıştırma işlemini kolaylaştırır.
amon
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.