Neden bir lexer'i 2d dizisi ve dev bir anahtar olarak uygular?


24

Derecemi bitirmek için yavaşça çalışıyorum ve bu dönem Derleyiciler 101. Ejderha Kitabını kullanıyoruz . Derse kısa bir süre sonra, sözcüksel analiz ve deterministik sonlu otomatalar (bundan sonra DFA) ile nasıl uygulanabileceğinden söz ediyoruz. Çeşitli lexer durumlarınızı ayarlayın, aralarındaki geçişleri tanımlayın, vb.

Ancak hem profesör hem de kitap, onları dev bir 2d dizisine (çeşitli boyutlarda olmayan terminal durumları ve bir boyut gibi muhtemel giriş sembolleri) ve diğer tüm terminalleri idare edebilmek için olası giriş sembollerine denk gelen geçiş tabloları aracılığıyla uygulamayı önermektedir. terminal olmayan bir durumda ise geçiş tablolarına gönderilir.

Teori gayet iyi ve iyidir, ancak onlarca yıldır kod yazmış biri olarak, uygulama aşağılıktır. Test edilebilir değildir, bakımı yapılamaz, okunaklı değildir ve bu hata ayıklamak için bir acı ve bir buçuk. Daha da kötüsü, dilin UTF özelliğine sahip olması durumunda nasıl uzaktan uygulanabileceğini göremiyorum. Terminal olmayan her devlet için bir milyondan fazla geçiş tablosu girişi olması aceleyle ağırlaşıyor.

Peki anlaşma nedir? Konuyla ilgili kesin kitap neden bu şekilde yapmayı söylüyor?

Fonksiyon ek yükü gerçekten bu kadar mı çağırıyor? Bu iyi çalışan bir şey mi yoksa dilbilgisi vaktinden önce bilinmediğinde gerekli mi (normal ifadeler?)? Veya daha spesifik çözümler daha spesifik gramerler için daha iyi çalışsa bile, belki de tüm vakaları ele alan bir şey?

( not: olası yinelenen " Neden dev bir anahtar ifadesi yerine bir OO yaklaşımı kullanmalıyım? " yakın, ancak OO'yu umursamıyorum. İşlevsel bir yaklaşım veya hatta bağımsız işlevlere sahip olan zorunlu bir yaklaşım yaklaşımı iyi olabilir.)

Ve örnek olarak, sadece tanımlayıcıları olan ve bu tanımlayıcıları olan bir dili düşünün [a-zA-Z]+. DFA uygulamasında, şöyle bir şey elde edersiniz:

private enum State
{
    Error = -1,
    Start = 0,
    IdentifierInProgress = 1,
    IdentifierDone = 2
}

private static State[][] transition = new State[][]{
    ///* Start */                  new State[]{ State.Error, State.Error (repeat until 'A'), State.IdentifierInProgress, ...
    ///* IdentifierInProgress */   new State[]{ State.IdentifierDone, State.IdentifierDone (repeat until 'A'), State.IdentifierInProgress, ...
    ///* etc. */
};

public static string NextToken(string input, int startIndex)
{
    State currentState = State.Start;
    int currentIndex = startIndex;
    while (currentIndex < input.Length)
    {
        switch (currentState)
        {
            case State.Error:
                // Whatever, example
                throw new NotImplementedException();
            case State.IdentifierDone:
                return input.Substring(startIndex, currentIndex - startIndex);
            default:
                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;
        }
    }

    return String.Empty;
}

(yine de dosyanın sonunu doğru halledecek bir şey)

Beklediğim ile karşılaştırıldığında:

public static string NextToken(string input, int startIndex)
{
    int currentIndex = startIndex;
    while (currentIndex < startIndex && IsLetter(input[currentIndex]))
    {
        currentIndex++;
    }

    return input.Substring(startIndex, currentIndex - startIndex);
}

public static bool IsLetter(char c)
{
    return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
}

NextTokenDFA'nın başından itibaren birden fazla varış noktanız olduğunda , kod tekrar kendi işlevine eklendiğinde.


5
Bir antik (1977) mirası Derleyici Tasarımı İlkeleri ? 40 yıl önce, kodlama tarzı çok farklıydı
gnat

7
DFA devletlerinin geçişlerini nasıl uygularsınız? Ve bu terminaller ve terminal olmayanlar hakkında ne, "terminal olmayanlar" genellikle dilbilgisindeki sözcüksel analizden sonra gelecek olan üretim kurallarını ifade eder .

10
Bu tabloların insanlara okunabilir olması, derleyici tarafından kullanılabilir olması ve çok hızlı bir şekilde yapılması amaçlanmıştır. Girişte ileriye bakarken bir masa etrafında atlamak kolaydır (örneğin, soldaki özyinelemeyi yakalamak için, pratikte çoğu durumda bunu önlemek için yapılmıştır).

5
Eğer tahrişinizin bir kısmı daha iyi bir iş yapmayı bilmekten ve tercih edeceğiniz bir yaklaşım için herhangi bir geri bildirim veya takdir alma kabiliyetinden yoksunsa - sanayide onlarca yıl geribildirimi beklemek ve zaman zaman takdir almak - Daha iyi bir uygulama yazmanız ve kendi iç huzurunuz için bir kısmını elde etmek için CodeReview.SE'ye göndermelisiniz.
Jimmy Hoffa

7
Basit cevap şudur, lexer genellikle sonlu durumlu bir makine olarak uygulanır ve otomatik olarak gramerden üretilir - ve durum tablosu, şaşırtıcı olmayan bir şekilde, en kolay ve kompakt bir tablo olarak gösterilir. Nesne kodunda olduğu gibi, insanların birlikte çalışmasının kolay olmadığı gerçeği de önemsiz çünkü insanlar onunla çalışmıyor; kaynağı değiştirir ve yeni bir örnek oluştururlar.
keshlam

Yanıtlar:


16

Uygulamada bu tablolar dilin belirteçlerini tanımlayan düzenli ifadelerden üretilir:

number := [digit][digit|underscore]+
reserved_word := 'if' | 'then' | 'else' | 'for' | 'while' | ...
identifier := [letter][letter|digit|underscore]*
assignment_operator := '=' | '+=' | '-=' | '*=' | '/=' 
addition_operator := '+' | '-' 
multiplication_operator := '*' | '/' | '%'
...

Lex'in yazıldığı 1975'ten beri sözcüksel analizör üretme imkanlarımız oldu.

Temel olarak düzenli ifadeleri işlemsel kodla değiştirmenizi öneriyorsunuz. Bu, normal ifadedeki birkaç karakteri birkaç kod satırına genişletir. Orta derecede ilginç olan herhangi bir dilin sözcüksel analizi için el yazısı prosedür kodu, hem verimsiz hem de bakımı zor olma eğilimindedir.


4
Toptan satış yapmayı önerdiğimden emin değilim. Düzenli ifadeler isteğe bağlı (normal) dillerle ilgilenir. Belirli dillerle çalışırken daha iyi yaklaşımlar yok mu? Kitap, öngörücü yaklaşımlara değiniyor ancak daha sonra bunları örneklerde görmezden geliyor. Ayrıca, C # yıl önce saf bir analizör yaptırdıktan sonra bakımını yapmakta zorlandım. Yetersiz? Tabii ki, ama o kadar da değil, o zamanki yeteneğimi verdim.
Telastyn

1
@Telastyn: Tablo güdümlü DFA'dan daha hızlı gitmek neredeyse imkansız: bir sonraki karaktere geç, geçiş tablosunda bir sonraki duruma bak, durumu değiştir. Yeni durum terminal ise bir belirteç yayar. C # veya Java'da, geçici dizeler oluşturmayı içeren herhangi bir yaklaşım daha yavaş olacaktır.
kevin cline

@kevincline - tabi, ama benim örneğimde geçici dizeler yok. C'de bile, sadece bir indeks veya dizginin içinden geçen bir işaretçi olacaktır.
Telastyn

6
@JimmyHoffa: evet, performans kesinlikle derleyicilerle ilgilidir. Derleyiciler hızlıdır çünkü cehenneme ve geriye doğru optimize edilmiştir. Mikro-optimizasyonlar değil, sadece gereksiz geçici cisimler oluşturma ve atma gibi gereksiz işler yapmazlar. Tecrübelerime göre çoğu ticari metin işlem kodu, modern bir derleyicinin onda birini oluşturuyor ve bunu yapmak için on kat sürüyor. Bir gigabayt metnin işlenmesinde performans çok fazladır.
kevin cline

1
@Telastyn, aklınızdaki "daha iyi yaklaşım" ın ne olduğunu ve hangi şekilde "daha iyi" olmasını beklersiniz? Zaten iyi test edilmiş lexing araçlarına sahip olduğumuz ve çok hızlı ayrıştırıcılar ürettikleri göz önüne alındığında (diğerlerinin söylediği gibi, masa başında çalışan DFA'lar çok hızlı), onları kullanmanın anlamı var. Lex dilbilgisi yazarken neden belirli bir dil için yeni bir özel yaklaşım icat etmek isteyelim? Lex gramerinin bakımı daha kolaydır ve elde edilen çözümleyici doğru olma olasılığı daha yüksektir (lex ve benzeri araçların ne kadar iyi test edildiği göz önüne alındığında).
DW

7

Belirli bir algoritma için motivasyon büyük ölçüde bir öğrenme alıştırması olduğundan, bir DFA fikrine yakın olmaya ve durumları ve geçişleri kodda çok açık tutmaya çalışır. Kural olarak, hiç kimse bu kodun hiçbirini elle zaten yazmaz - bir gramerden kod üretmek için bir araç kullanırsınız. Ve bu araç kodun okunabilirliğini önemsemez çünkü kaynak kod değildir, dilbilgisi tanımına dayalı bir çıktıdır.

Kodunuz, elle yazılmış bir DFA tutan biri için daha temiz, ancak öğretilen kavramlardan biraz daha uzaklaştı.


7

İç döngü:

                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;

performans avantajları çoktur. Bunda hiç dal yok, çünkü her giriş karakteri için tamamen aynı şeyi yapıyorsunuz. Derleyicinin performansı, sözcü tarafından girilebilir (girişin her karakterinin bir ölçeğinde çalışması gerekir). Dragon Kitabı yazıldığı zaman bu daha da doğruydu.

Uygulamada, sözcükleri okuyan CS öğrencileri dışında, hiç kimse bu iç döngüyü uygulamak zorunda değildir (çünkü hata ayıklamak zorunda değildir) çünkü transitiontabloyu oluşturan araçla birlikte gelen kazan plakasının bir parçasıdır .


5

Bellekten, - kitabı okuduğumdan bu yana çok zaman geçti ve en son basımı okumadığımdan eminim, kesinlikle Java gibi görünen bir şey hatırlamıyorum - o kısımla yazılmış Kod bir şablon olarak tasarlandı, tablo bir lexer generator gibi bir lex ile dolduruldu. Yine de bellekten, tablo sıkıştırması ile ilgili bir bölüm vardı (yine bellekten, tablo yönlendirmeli ayrıştırıcılara da uygulanabilir, böylece belki de kitapta henüz gördüğünüzden daha ileride). Benzer şekilde, hatırladığım kitapta 8 bitlik bir karakter seti olduğunu varsaymıştım, daha sonraki basımlarda daha büyük karakter kümelerinin ele alınması üzerine, muhtemelen tablo sıkıştırmasının bir parçası olarak beklerdim. Bunu SO sorusuna cevap olarak ele almanın alternatif bir yolunu verdim .

Modern mimaride yönlendirilmiş sıkı bir döngü verisine sahip olmanın kesin bir performans avantajı vardır: oldukça önbellek dostu (eğer tabloları sıkıştırdıysanız) ve atlama tahmini mümkün olduğu kadar mükemmeldir (Lexem sonunda bir tanesi, belki biri sembolüne bağlı olan koda gönderilen anahtarın özledim; masaya dekompresyonunuzun tahmin edilebilir atlamalarla yapılabileceği varsayılır). Bu durum makinesini saf koda taşımak, atlama tahmini performansını düşürür ve belki de önbellek basıncını arttırır.


2

Daha önce Ejderha Kitabı üzerinde çalışmış, masa tahrikli kolları ve ayrıştırıcıları kullanmanın temel nedeni, ayrıştırıcıyı oluşturmak için lexer ve BNF'yi oluşturmak için düzenli ifadeleri kullanabilmenizdir. Kitap aynı zamanda lex ve yacc gibi araçların nasıl çalıştığını ve bu araçların nasıl çalıştığını bilmeniz için kapsar. Ayrıca, bazı pratik örnekler üzerinde çalışmanız önemlidir.

Yorumların çoğuna rağmen, 40'ların, 50'lerin, 60'ların içine yazılan kod tarzıyla hiçbir ilgisi yok ... aletlerin sizin için ne yaptığını ve ne yaptığınızı pratik bir şekilde anlamakla ilgili. onları çalışması için yapmak için. Derleyicilerin hem teorik hem de pratik açıdan nasıl çalıştığını temel bir anlayışla yapacak her şeyi vardır.

Umarım, eğitmeniniz ayrıca lex ve yacc kullanmanıza izin verecektir (eğer lisansüstü bir sınıf değilse ve lex ve yacc yazabiliyorsanız).


0

Partiye geç :-) Jetonlar düzenli ifadelerle eşleştirilir. Bunların çoğu olduğu için, sırayla dev DFA olan çoklu regex motoruna sahipsiniz.

"Daha da kötüsü, dilin UTF yeteneğine sahip olması durumunda nasıl uzaktan uygulanabileceğini göremiyorum."

Alakasız (veya şeffaf). UTF'nin hoş mülkiyeti yanında, varlıkları kısmen bile örtüşmemektedir. Örneğin, "A" karakterini temsil eden bayt (ASCII-7 tablosundan), başka bir UTF karakteri için tekrar kullanılmaz.

Yani, tüm lexer için tek bir DFA (çoklu regex) var. 2d dizisinden daha iyi yazmak ne kadar iyi?

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.