Bir lexer için jetonlarla geliyor


14

Oluşturduğum bir biçimlendirme dili için bir ayrıştırıcı yazıyorum (python ile yazma, ancak bu gerçekten bu soru ile ilgili değil - aslında bu kötü bir fikir gibi görünüyorsa, daha iyi bir yol için bir öneri isterim) .

Burada ayrıştırıcılar hakkında okuyorum: http://www.ferg.org/parsing/index.html ve doğru anlarsam içeriği jetonlara bölen lexer'ı yazmaya çalışıyorum. Anlamakta zorlandığım şey, hangi token türlerini kullanmam gerektiği veya bunları nasıl oluşturacağım. Örneğin, bağlandığım örnekteki belirteç türleri şunlardır:

  • STRING
  • IDENTIFIER
  • NUMARA
  • BEYAZ BOŞLUK
  • YORUM YAP
  • EOF
  • {Ve (gibi birçok simge kendi simge türleri olarak sayılır)

Yaşadığım sorun, daha genel token türlerinin benim için biraz keyfi görünmesi. Örneğin, IDENTIFIER'a karşı neden kendi ayrı token türünü STRING yapıyor? Bir dize STRING_START + (IDENTIFIER | WHITESPACE) + STRING_START olarak temsil edilebilir.

Bu, dilimin zorluklarıyla da ilgili olabilir. Örneğin, değişken bildirimleri olarak yazılır {var-name var value}ve ile konuşlandırılır {var-name}. Kendi jetonları gibi görünüyor '{'ve '}'olmalı, ancak VAR_NAME ve VAR_VALUE uygun jeton türleri mi yoksa ikisi de IDENTIFIER kapsamında mı olacak? Dahası, VAR_VALUE aslında boşluk içerebilir. Sonraki boşluk var-name, bildirideki değerin başlangıcını belirtmek için kullanılır. Diğer tüm boşluklar, değerin bir parçasıdır. Bu boşluk kendi jetonu mu oluyor? Boşluk yalnızca bu bağlamda bu anlama sahiptir. Dahası, {değişken bir bildirimin başlangıcı olmayabilir .. bağlama bağlıdır (yine bu kelime var!). {:bir isim bildirimi başlatır ve{ bazı değerlerin bir parçası olarak bile kullanılabilir.

Benim dilim Python'a benzer, çünkü bloklar girintiyle oluşturulmuştur. Python'un INDENT ve DEDENT jetonlarını oluşturmak için lexer'ı nasıl kullandığı hakkında okuyordum (bu, daha fazla veya daha az başka dilde ne yapacağını {ve }ne yapacağını). Python bağlamdan bağımsız olduğunu iddia ediyor, bu da bana en azından lexer'ın jeton oluştururken akışta nerede olduğunu umursamaması gerektiği anlamına geliyor. Python'un lexer'ı, önceki karakterleri bilmeden belirli bir uzunlukta bir INDENT jetonu oluşturduğunu nasıl biliyor (örn. Önceki satırın yeni satır olduğunu, bu nedenle INDENT için boşluklar oluşturmaya başlayın)? Soruyorum çünkü bunu da bilmem gerekiyor.

Son sorum en aptal olanı: bir lexer neden gerekli? Bana öyle geliyor ki, ayrıştırıcı karakter karaktere gidebilir ve nerede olduğunu ve ne beklediğini anlayabilir. Lexer basitliğin faydasını ekliyor mu?


2
Devam edin ve tarayıcısız bir ayrıştırıcı yazmayı deneyin. Eğer hiç işe yaramazsa (sonucun bazı ayrıştırma algoritmaları için çok belirsiz olabileceğini düşünürsem), "boşluk da burada izin verilir" ve "bekle, tanımlayıcı veya bir sayı? " Deneyimden konuşuyorum.

Neden özel bir tekerleği yeniden icat ettiniz? Özel olarak oluşturulmuş bir lexer gerektiren bir dil tasarlamak yerine, zaten LISP veya hatta FORTH gibi yerleşik bir lexer ile gelen mevcut bir dili kullanmayı düşündünüz mü?
John R. Strohm

2
@ JohnR.Strohm akademik amaçlı. Dilin kendisi muhtemelen pratik olarak yararlı olmayacaktır.
Patlama Hapları

Yanıtlar:


11

Sorunuz (son paragrafınızın ipucu verdiği gibi) gerçekten lexer ile ilgili değil, lexer ve ayrıştırıcı arasındaki arayüzün doğru tasarımı ile ilgilidir. Tahmin edebileceğiniz gibi, sözlüklerin ve ayrıştırıcıların tasarımı hakkında birçok kitap var. Ben severim Dick Grune tarafından ayrıştırıcı kitabı , ama iyi bir tanıtım kitabı olmayabilir. Ben Appel tarafından C-tabanlı kitap yoğun sevmiyorum , çünkü kod yararlı olarak kendi derleyici genişletilebilir değildir (çünkü C gibi davranma kararının doğasında bulunan bellek yönetimi sorunları ML gibidir). Kendi tanıtımım PJ Brown'un kitabıydı , ama iyi bir genel tanıtım değil (özellikle tercümanlar için oldukça iyi olsa da). Ama sorunuza geri dönelim.

Bu sorunun cevabı, ileri veya geri görünümlü kısıtlamaları kullanmaya gerek kalmadan lexer'da yapabildiğiniz kadar çok şey yapın.

Bu, (elbette dilin ayrıntılarına bağlı olarak) bir dizeyi "karakter ve ardından bir not dizisi ve sonra başka bir" karakter olarak tanımanız gerektiği anlamına gelir. Bunu ayrıştırıcıya tek bir birim olarak döndürün. bunun nedenleri, ama önemli olanlar

  1. Bu, ayrıştırıcının sürdürmesi gereken durum miktarını azaltır ve bellek tüketimini sınırlar.
  2. Bu, lexer uygulamasının temel yapı taşlarını tanımaya odaklanmasını sağlar ve ayrıştırıcıyı, tek tek sözdizimsel öğelerin bir program oluşturmak için nasıl kullanıldığını tanımlamak için serbest bırakır.

Ayrıştırıcılar genellikle sözlükçiden bir token almak için hemen harekete geçebilir. Örneğin, IDENTIFIER alınır alınmaz ayrıştırıcı, sembolün zaten bilinip bilinmediğini öğrenmek için bir sembol tablosu araması yapabilir. Ayrıştırıcınız dize sabitlerini QUOTE (IDENTIFIER SPACES) * QUOTE olarak da ayrıştırırsa, çok sayıda alakasız sembol tablosu araması gerçekleştirirsiniz veya sembol tablosu aramalarını ayrıştırıcının sözdizimi öğeleri ağacının yukarısında yukarı kaldırırsınız, çünkü yalnızca bir dizeye bakmadığınızdan emin olabilirsiniz.

Söylemeye çalıştığım şeyi ifade etmek için, ama farklı bir şekilde, lexer şeylerin yazımıyla ve ayrıştırıcının şeylerin yapısıyla ilgilenmelidir.

Bir dizenin neye benzediğine ilişkin açıklamamın normal ifadeye çok benzediğini fark edebilirsiniz. Bu bir tesadüf değil. Sözlüksel analizörler genellikle düzenli ifadeler kullanan küçük dillerde ( Jon Bentley'in mükemmel Programlama İncileri kitabı anlamında ) uygulanır. Metni tanıdığımda normal ifadeler açısından düşünmeye alışkınım.

Boşluk ile ilgili sorunuzla ilgili olarak, söz konusu kelimeyi sözlükte tanıyın. Diliniz oldukça serbest biçimli olması amaçlanıyorsa, WHITESPACE jetonlarını ayrıştırıcıya iade etmeyin, çünkü bunları atmak zorunda kalacak, bu yüzden parserinizin üretim kuralları esasen gürültü ile spam olacak - sadece atmak için tanınacak şeyler onları uzaklara.

Sözdizimsel olarak anlamlı olduğunda boşlukları nasıl ele almanız gerektiği hakkında ne anlama gelirseniz, sizin için diliniz hakkında daha fazla bilgi sahibi olmadan gerçekten iyi çalışacak bir karar verebileceğimden emin değilim. Kararım, boşlukun bazen önemli ve bazen önemsiz olduğu durumlardan kaçınmak ve bir tür sınırlayıcı (tırnak işaretleri gibi) kullanmaktır. Ancak, dili istediğiniz şekilde tasarlayamıyorsanız, bu seçenek sizin için uygun olmayabilir.

Dil ayrıştırma sistemleri tasarlamanın başka yolları da vardır. Kesinlikle birleşik bir lexer ve ayrıştırıcı sistemi belirtmek için izin derleyici inşaat sistemleri vardır (sanırım ANTLR Java sürümü bunu yapar) ama ben hiç kullanmadım.

Son tarihi bir not. On yıllar önce, ayrıştırıcıya teslim etmeden önce lexer için mümkün olduğunca çok şey yapmak önemliydi, çünkü iki program aynı anda belleğe sığmayacaktı. Lexer'da daha fazlasını yapmak, ayrıştırıcıyı akıllı yapmak için daha fazla bellek bıraktı. Whitesmiths C Derleyicisini birkaç yıl boyunca kullanıyordum ve doğru anlarsam , sadece 64KB RAM'de çalışacaktı (küçük model MS-DOS programı) ve hatta C'nin bir varyantını tercüme etti. ANSI C'ye çok yakındı.


Bellek boyutu ile ilgili iyi bir tarihsel not, işi en başta lexers ve ayrıştırıcılara bölmenin bir nedeni.
stevegt

3

Son sorunuza bakacağım, aslında aptalca değil. Ayrıştırıcılar, karakter karakter bazında karmaşık yapılar oluşturabilir ve oluşturabilir. Hatırlıyorsam, Harbison ve Steele'deki dilbilgisi ("C - Bir referans el kitabı") terminal olarak tek karakter kullanan ve tek karakterlerden terminal olmayan tanımlayıcılar, dizeler, sayılar vb.

Biçimsel diller açısından, normal ifade tabanlı bir lexer'ın "dize değişmezi", "tanımlayıcı", "sayı", "anahtar kelime" vb. Olarak tanıyabileceği ve sınıflandırabileceği her şey, bir LL (1) ayrıştırıcısının bile tanıyabildiğini gösterir. Yani her şeyi tanımak için bir ayrıştırıcı jeneratör kullanmakla ilgili teorik bir problem yok.

Algoritmik bir bakış açısından, normal bir ifade tanıyıcı herhangi bir ayrıştırıcıdan çok daha hızlı çalışabilir. Bilişsel bir bakış açısından, bir programcının normal ifade-lexer ve ayrıştırıcı oluşturucu yazılı bir ayrıştırıcı arasındaki işi parçalaması muhtemelen daha kolaydır.

Pratik düşüncelerin insanların ayrı sözlük ve ayrıştırıcılara sahip olma kararını vermesine neden olduğunu söyleyebilirim.


Evet - ve C standardının kendisi de aynı şeyi yapıyor, sanki doğru hatırlıyorum, Kernighan ve Ritchie'nin her iki basımı da yaptı.
James Youngman

3

Gerçekten gramerleri anlamadan bir lexer / ayrıştırıcı yazmaya çalıştığınız anlaşılıyor. Tipik olarak, insanlar bir lexer ve ayrıştırıcı yazarken, bunları bazı gramerlere uymak için yazarlar. Ayrıştırıcı kuralları / terminal olmayanları eşleştirmek için bu belirteçleri kullanırken lexer dil bilgisini dilbilgisinde döndürmelidir . Girdinizi bayt bayt olacak şekilde kolayca ayrıştırabiliyorsanız, bir lexer ve ayrıştırıcı gereğinden fazla olabilir.

Lexers işleri kolaylaştırır.

Dilbilgisine genel bakış : Dilbilgisi, bazı sözdiziminin veya girdinin nasıl görünmesi gerektiğine ilişkin bir dizi kuraldır. Örneğin, burada bir oyuncak dilbilgisi (simple_command başlangıç ​​sembolüdür):

simple_command:
 WORD DIGIT AND_SYMBOL
simple_command:
     addition_expression

addition_expression:
    NUM '+' NUM

Bu dilbilgisi şu anlama gelir:
Bir simple_command,
A) WORD, ardından DIGIT ve ardından AND_SYMBOL'dan oluşur (bunlar benim tanımladığım "jetonlardır")
B) "add_expression " (bu bir kural veya "terminal dışı")

Add_expression, şunlardan oluşur:
NUM ve ardından bir '+' ve ardından bir NUM (NUM benim tanımladığım bir "belirteç", '+' değişmez bir artı işaretidir).

Bu nedenle, simple_command "başlat sembolü" (başladığım yer) olduğundan, bir jeton aldığımda bunun simple_command'a uyup uymadığını kontrol ederim. Girişteki ilk simge bir WORD ve sonraki simge bir DIGIT ve sonraki simge bir AND_SYMBOL ise, o zaman bazı simple_command ile eşleştirdim ve bazı işlemler yapabilirim. Aksi takdirde, add_expression olan simple_command'ın diğer kuralıyla eşleştirmeye çalışacağım. Bu nedenle, ilk belirteç bir NUM ve ardından bir '+' ve ardından bir NUM ise, bir simple_command ile eşleştim ve bazı eylemler gerçekleştirdim. Bunlardan hiçbiri değilse, bir sözdizimi hatası var.

Bu gramerlere çok ama çok temel bir giriş. Daha kapsamlı bir anlayış için, bu wiki makalesine göz atın ve bağlamda gramer dersleri için web'de arama yapın.

Bir lexer / ayrıştırıcı düzenlemesi kullanarak, ayrıştırıcının nasıl görünebileceğine bir örnek:

bool simple_command(){
   if (peek_next_token() == WORD){
       get_next_token();
       if (get_next_token() == DIGIT){
           if (get_next_token() == AND_SYMBOL){
               return true;
           } 
       }
   }
   else if (addition_expression()){
       return true;
   }

   return false;
}

bool addition_expression(){
    if (get_next_token() == NUM){
        if (get_next_token() == '+'){
             if (get_next_token() == NUM){
                  return true;
             }
        }
    }
    return false;
}

Tamam, böylece kod çirkin tür ve asla iç içe if ifadeleri tavsiye ederim. Ama asıl mesele, hoş modüler "get_next_token" ve "peek_next_token" işlevlerinizi kullanmak yerine karakter üzerinde yukarıdaki şeyi yapmaya çalıştığınızı düşünün . Cidden, bir şans ver. Sonucu sevmeyeceksiniz. Şimdi yukarıdaki dilbilgisinin neredeyse tüm yararlı dilbilgilerinden yaklaşık 30 kat daha az karmaşık olduğunu unutmayın. Bir lexer kullanmanın faydasını görüyor musunuz?

Dürüst olmak gerekirse, lexers ve ayrıştırıcılar dünyadaki en temel konular değildir. Önce gramerleri okumayı ve anlamayı, sonra da lexers / parsers hakkında biraz okumayı, sonra dalamanızı tavsiye ederim.


Dilbilgisi hakkında bilgi edinmek için önerileriniz var mı?
Patlama Hapları

Cevabımı dilbilgilerine çok temel bir giriş ve daha fazla öğrenme için bazı öneriler içerecek şekilde düzenledim. Dilbilgisi bilgisayar biliminde çok önemli bir konudur, bu yüzden öğrenmeye değer.
Casey Patton

1

Son sorum en aptal olanı: bir lexer neden gerekli? Bana öyle geliyor ki, ayrıştırıcı karakter karaktere gidebilir ve nerede olduğunu ve ne beklediğini anlayabilir.

Bu aptalca değil, sadece gerçek.

Ancak uygulanabilirlik bir şekilde araçlarınıza ve hedeflerinize bağlıdır. Örneğin, lexc olmadan yacc kullanırsanız ve tanımlayıcılarda unicode harflere izin vermek istiyorsanız, açıklığın tüm geçerli karakterleri numaralandırdığı büyük ve çirkin bir kural yazmanız gerekir. Bir sözlükte, bir karakterin harf kategorisinin bir üyesi olup olmadığını belki de bir kütüphane rutini isteyebilirsiniz.

Bir lexer kullanmak veya kullanmamak, diliniz ile karakter seviyesi arasında bir soyutlama seviyesine sahip olmakla ilgilidir. Karakter seviyesinin, günümüzde, bit seviyesinin üzerinde başka bir soyutlama olduğuna dikkat edin, bu da bit seviyesinin üzerinde bir soyutlamadır.

Son olarak, bit düzeyinde bile ayrıştırabilirsiniz.


0
STRING_START + (IDENTIFIER | WHITESPACE) + STRING_START.

Hayır, olamaz. Ne olmuş "("? Size göre, bu geçerli bir dize değil. Ve kaçar?

Genel olarak, boşlukları tedavi etmenin en iyi yolu, jetonları sınırlamanın ötesinde, onu görmezden gelmektir. Birçok insan çok farklı boşlukları tercih eder ve boşluk kurallarını uygulamak en iyi ihtimalle tartışmalıdır.

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.