Yanıtlar:
Gerçekten üç seçenek var, üçü de farklı durumlarda tercih ediliyor.
Diyelim, ŞİMDİ bazı eski veri formatları için bir ayrıştırıcı oluşturmanız isteniyor. Ya da ayrıştırıcınızın hızlı olması gerekir. Veya ayrıştırıcınızın kolayca bakımı yapılabilir olması gerekir.
Bu gibi durumlarda, muhtemelen bir ayrıştırıcı jeneratörü kullanmaktan en iyisini alırsınız. Ayrıntılarla uğraşmak zorunda değilsiniz, doğru çalışması için çok sayıda karmaşık kod almak zorunda değilsiniz, sadece girişin uyması gereken gramer bilgisini yazıyorsunuz, biraz kullanım kodu ve presto: anında ayrıştırıcı yazıyorsunuz.
Avantajları açıktır:
Ayrıştırma jeneratörleriyle ilgili dikkat etmeniz gereken bir şey var: bazen dilbilginizi reddedebilir. Farklı ayrıştırıcı türlerine ve sizi nasıl ısırmalarına genel bir bakış için buradan başlamak isteyebilirsiniz . Burada , birçok uygulamaya ve kabul ettikleri gramer türlerine genel bir bakış bulabilirsiniz.
Ayrıştırıcı jeneratörler güzel, ancak çok kullanıcı (son kullanıcı, siz değil) dostu değiller. Genelde iyi hata mesajları veremezsiniz ya da hata kurtarma sağlayamazsınız. Belki de diliniz çok gariptir ve ayrıştırıcılar dilbilginizi reddeder veya jeneratörün size verdiğinden daha fazla kontrole ihtiyacınız var.
Bu durumlarda, elle yazılmış özyinelemeli iniş çözümleyici kullanmak muhtemelen en iyisidir. Doğru yaparken karmaşık olabilir, ayrıştırıcınız üzerinde tam kontrol sahibi olursunuz, böylece hata mesajları ve hatta hata kurtarma gibi ayrıştırıcı üreticilerle yapamayacağınız her türlü güzel şeyi yapabilirsiniz (tüm noktalı virgülleri C # dosyasından kaldırmayı deneyin). : C # derleyicisi şikayet eder, ancak noktalı virgüllerin varlığına bakmaksızın bu hataların çoğunu yine de tespit eder).
Elle yazılmış ayrıştırıcılar, ayrıştırıcının kalitesinin yeterince yüksek olduğu varsayılarak, genellikle oluşturulanlardan daha iyi performans gösterir. Öte yandan, iyi bir çözümleyici yazmayı başaramazsanız - genellikle deneyim (bilgi eksikliği) veya tasarım eksikliği nedeniyle - o zaman performans genellikle yavaştır. Lexers için ise tam tersi doğrudur: genellikle üretilen lexers tablo aramalarını kullanır ve bunları (çoğu) elle yazılmışlardan daha hızlı yapar.
Eğitim-bilge, kendi çözümleyicinizi yazmak size bir jeneratör kullanmaktan daha fazlasını öğretecektir. Sonuçta daha karmaşık kodlar yazmanız gerekir, ayrıca bir dili nasıl ayrıştırdığınızı tam olarak anlamanız gerekir. Öte yandan, kendi dilinizi nasıl yaratacağınızı öğrenmek istiyorsanız (bu nedenle, dil tasarımında deneyim edinin), seçenek 1 veya seçenek 3 tercih edilir: eğer bir dil geliştiriyorsanız, muhtemelen çok şey değişecek, ve seçenek 1 ve 3 size bununla daha kolay zaman verir.
Şu anda yürüdüğüm yol bu: kendi ayrıştırıcı jeneratörünüzü yazıyorsunuz . Çok önemsiz olsa da, bunu yapmak size muhtemelen en iyisini öğretecektir.
Bunun gibi bir projeyi yapmanın neyi içerdiği hakkında bir fikir vermek için size kendi gelişimimden bahsedeceğim.
Lexer üreteci
Önce kendi lexer jeneratörümü yarattım. Genellikle kodun nasıl kullanılacağından başlayarak yazılımlar tasarlarım, bu yüzden kodumu nasıl kullanmak istediğimi düşündüm ve bu kod parçasını yazdım (C # dilinde):
Lexer<CalculatorToken> calculatorLexer = new Lexer<CalculatorToken>(
new List<StringTokenPair>()
{ // This is just like a lex specification:
// regex token
new StringTokenPair("\\+", CalculatorToken.Plus),
new StringTokenPair("\\*", CalculatorToken.Times),
new StringTokenPair("(", CalculatorToken.LeftParenthesis),
new StringTokenPair(")", CalculatorToken.RightParenthesis),
new StringTokenPair("\\d+", CalculatorToken.Number),
});
foreach (CalculatorToken token in
calculatorLexer.GetLexer(new StringReader("15+4*10")))
{ // This will iterate over all tokens in the string.
Console.WriteLine(token.Value);
}
// Prints:
// 15
// +
// 4
// *
// 10
Girdi dizgisi-belirteç çiftleri, bir aritmetik yığının fikirlerini kullanarak temsil ettikleri normal ifadeleri tarif eden karşılık gelen özyinelemeli bir yapıya dönüştürülür. Bu daha sonra bir NFA'ya (nondeterministik sonlu otomaton) dönüştürülür ve bu sırada bir DFA'ya (deterministik sonlu otomaton) dönüştürülür. Daha sonra dizeleri DFA ile eşleştirebilirsiniz.
Bu şekilde, lexers'ın tam olarak nasıl çalıştığı hakkında iyi bir fikir edinebilirsiniz. Ek olarak, doğru şekilde yaparsanız, lexer jeneratörünüzün sonuçları kabaca profesyonel uygulamalar kadar hızlı olabilir. Ayrıca, seçenek 2 ile karşılaştırıldığında hiçbir ifade kaybettiniz ve seçenek 1 ile karşılaştırıldığında hiçbir ifade kaybettiniz.
Lexer jeneratörümü 1600'den fazla kod satırında uyguladım. Bu kod yukarıdakileri çalıştırır, ancak programı her başlattığınızda anında lexer'ı oluşturur: Bir noktada diske yazmak için kod ekleyeceğim.
Kendi lexer yazmayı bilmek istiyorsanız, bu başlamak için iyi bir yerdir.
Ayrıştırıcı jeneratör
Ardından ayrıştırma jeneratörünüzü yazın. Farklı ayrıştırıcı türlerine genel bir bakış için buraya tekrar atıfta bulunuyorum - kural olarak, ne kadar çok ayrıştırırlarsa, yavaşlarlar.
Hız benim için bir sorun değil, bir Earley ayrıştırıcı uygulamayı seçtim. Bir Earley ayrıştırıcısının gelişmiş uygulamalarının diğer ayrıştırıcı türlerinden iki kat daha yavaş olduğu gösterilmiştir .
Bu hız çarpması karşılığında, herhangi bir gramer, hatta belirsiz olanları bile ayrıştırma olanağına sahip olursunuz . Bu, ayrıştırıcınızın içinde herhangi bir özyinelemeye sahip olup olmadığı ya da vardiya azaltıcı bir çatışmanın ne olduğu konusunda endişelenmenize gerek olmadığı anlamına gelir. Sonuç olarak hangi ayrıştırma ağacının bir önemi yoksa, 1 + 2 + 3'ü (1 + 2) +3 olarak ya da 1 olarak ayrıştırmanızın önemi olmadığı gibi, belirsiz gramerleri kullanarak gramerleri daha kolay tanımlayabilirsiniz. + (2 + 3).
Ayrıştırma jeneratörümü kullanan bir kod parçası şöyle görünebilir:
Lexer<CalculatorToken> calculatorLexer = new Lexer<CalculatorToken>(
new List<StringTokenPair>()
{
new StringTokenPair("\\+", CalculatorToken.Plus),
new StringTokenPair("\\*", CalculatorToken.Times),
new StringTokenPair("(", CalculatorToken.LeftParenthesis),
new StringTokenPair(")", CalculatorToken.RightParenthesis),
new StringTokenPair("\\d+", CalculatorToken.Number),
});
Grammar<IntWrapper, CalculatorToken> calculator
= new Grammar<IntWrapper, CalculatorToken>(calculatorLexer);
// Declaring the nonterminals.
INonTerminal<IntWrapper> expr = calculator.AddNonTerminal<IntWrapper>();
INonTerminal<IntWrapper> term = calculator.AddNonTerminal<IntWrapper>();
INonTerminal<IntWrapper> factor = calculator.AddNonTerminal<IntWrapper>();
// expr will be our head nonterminal.
calculator.SetAsMainNonTerminal(expr);
// expr: term | expr Plus term;
calculator.AddProduction(expr, term.GetDefault());
calculator.AddProduction(expr,
expr.GetDefault(),
CalculatorToken.Plus.GetDefault(),
term.AddCode(
(x, r) => { x.Result.Value += r.Value; return x; }
));
// term: factor | term Times factor;
calculator.AddProduction(term, factor.GetDefault());
calculator.AddProduction(term,
term.GetDefault(),
CalculatorToken.Times.GetDefault(),
factor.AddCode
(
(x, r) => { x.Result.Value *= r.Value; return x; }
));
// factor: LeftParenthesis expr RightParenthesis
// | Number;
calculator.AddProduction(factor,
CalculatorToken.LeftParenthesis.GetDefault(),
expr.GetDefault(),
CalculatorToken.RightParenthesis.GetDefault());
calculator.AddProduction(factor,
CalculatorToken.Number.AddCode
(
(x, s) => { x.Result = new IntWrapper(int.Parse(s));
return x; }
));
IntWrapper result = calculator.Parse("15+4*10");
// result == 55
(IntWrapper'ın yalnızca bir Int32 olduğunu unutmayın; C #, bunun bir sınıf olmasını gerektirir, bu yüzden bir sarmalayıcı sınıfı tanıtmak zorunda kaldım)
Umarım yukarıdaki kodun çok güçlü olduğunu görürsünüz: bulabileceğiniz herhangi bir gramer ayrıştırılabilir. Dilbilgisine çok sayıda görevi yerine getirebilecek isteğe bağlı kod parçaları ekleyebilirsiniz. Tüm bunları halletmeyi başarırsanız, sonuç kodunu birçok işi yapmak için yeniden kullanabilirsiniz: bu kod parçasını kullanarak bir komut satırı yorumlayıcısı oluşturmayı hayal edin.
Asla, hiç ayrıştırıcı yazmadıysan, yapmanı tavsiye ederim. Bu eğlenceli olur ve işlerin nasıl öğrenmek ve ayrıştırıcı ve lexer jeneratörleri yapmaktan kurtaracak o çabayı takdir öğrenmek yanındaki bir ayrıştırıcı gereken zamanı.
Ayrıca, http://compilers.iecc.com/crenshaw/ sayfasını okumayı denemenizi tavsiye ederim .
Kendi özyinelemeli iniş ayrıştırıcınızı yazmanın avantajı , sözdizimi hatalarında yüksek kaliteli hata mesajları üretebilmenizdir . Ayrıştırma jeneratörlerini kullanarak, hata üretimleri yapabilir ve belirli noktalarda özel hata mesajları ekleyebilirsiniz, ancak ayrıştırma jeneratörleri ayrıştırma üzerinde tam denetime sahip olma gücüyle eşleşmez.
Kendinizinkini yazmanın bir başka avantajı, gramerinize bire bir yazışma yapmayan daha basit bir gösterime ayrılmanın daha kolay olmasıdır.
Dilbilginiz sabitse ve hata mesajları önemliyse, kendi dilinizi yuvarlayın ya da en azından ihtiyacınız olan hata mesajlarını veren bir ayrıştırıcı oluşturucu kullanın. Dilbilginiz sürekli değişiyorsa, bunun yerine ayrıştırıcı jeneratörleri kullanmayı düşünmelisiniz.
Bjarne Stroustrup, C ++ 'ın ilk uygulaması için YACC'yi nasıl kullandığı hakkında konuşuyor (bkz. C ++' ın Tasarımı ve Evrimi ). Bu ilk durumda, bunun yerine kendi özyinelemeli iniş ayrıştırıcısını yazmasını diledi!
Seçenek 3: Hiçbiri (Kendi ayrıştırıcı jeneratörünüzü döndürmeyin)
Kullanmamak için bir neden var diye antlr , bizon , Coco / R , Grammatica , JavaCC , Limon , yarım kaynatılmış , SableCC , Quex , vb - o anında kendi ayrıştırıcı + lexer rulo gerektiği anlamına gelmez.
Tanımlayın neden neden onlar size hedefe ulaşmak izin vermeyin - tüm bu araçlar yeterince iyi değil?
Dilbilgisi ile uğraştığınız tuhaflıkların benzersiz olduğundan emin değilseniz, sadece bunun için tek bir özel çözümleyici + lexer oluşturmamalısınız. Bunun yerine, istediğiniz şeyi yaratacak, ancak gelecekteki ihtiyaçları karşılamak için de kullanılabilecek bir araç oluşturun, sonra diğer insanların da sizinle aynı sorunu yaşamasını önlemek için Özgür Yazılım olarak yayınlayın.
Kendi ayrıştırıcınızı kullanmak, sizi doğrudan dilinizin karmaşıklığı hakkında düşünmeye zorlar. Dilin ayrıştırılması zorsa, muhtemelen anlaşılması zor olacaktır.
İlk günlerde ayrıştırma üreticilerine çok fazla ilgi vardı, çok karmaşık (bazıları "işkence" diyebilirdi) dil sözdizimi tarafından motive edildi. JOVIAL özellikle kötü bir örnekti: diğer her şeyin en fazla bir sembol gerektirdiği bir zamanda iki sembol bakış açısı gerektiriyordu. Bu, bir JOVIAL derleyici için ayrıştırıcıyı beklenenden daha zor hale getirdi (General Dynamics / Fort Worth Division, F-16 programı için JOVIAL derleyicileri tedarik ederken zor yoldan öğrendiği için).
Bugün, özyinelemeli iniş evrensel olarak tercih edilen yöntemdir, çünkü derleyici yazarları için daha kolaydır. Özyinelemeli alçalış derleyiciler basit, temiz bir dil tasarımını kuvvetli bir şekilde ödüllendirir; bu nedenle, özyinelemeli, dağınık olandan daha basit, temiz bir dil için özyinelemeli bir çözümleyici yazmak çok daha kolaydır.
Sonunda: Dilinizi LISP'ye yerleştirmeyi ve LISP tercümanının sizin için ağır yük kaldırmasına izin vermeyi düşündünüz mü? AutoCAD bunu yaptı ve hayatlarını çok daha kolay hale getirdi. Dışarıda oldukça hafif hafif LISP tercümanları var, bazıları gömülebilir.
Bir kez ticari uygulama için bir ayrıştırıcı yazdım ve yacc kullandım . Bir geliştiricinin her şeyi C ++ 'ta elle yazdığı ve yaklaşık beş kat daha yavaş çalıştığı rakip bir prototip vardı.
Bu ayrıştırıcının lexer gelince, tamamen elle yazdım. Sürdü - üzgünüm, neredeyse 10 yıl önceydi, bu yüzden tam olarak hatırlamıyorum - C'de yaklaşık 1000 satır .
Sözcüğü elle yazmamın nedeni, ayrıştırıcının giriş dilbilgisi idi. Bu bir zorunluluktu, ayrıştırıcı uygulamamın tasarladığım şeyin aksine uyması gereken bir şeydi. (Tabii ki farklı şekilde tasarlardım. Ve daha iyisi!) Dilbilgisi, içeriğe bağımlıydı ve hatta bazı yerlerde anlambilime bağlıydı. Örneğin, bir noktalı virgül bir yerdeki bir belirtecin parçası olabilir, ancak daha önce ayrıştırılan bazı öğelerin anlamsal yorumuna dayanarak farklı bir yerdeki bir ayırıcı olabilir. Bu yüzden, anlamsal bağımlılıkları elle yazılmış bir Lexer'a "gömdüm" ve bu beni yacc içinde uygulanması kolay olan oldukça basit bir BNF ile bıraktı .
EKLENDİ cevaben MacNeil'in : yacc programcı sağlayan çok güçlü bir soyutlama sağlayan böyle terminalleri, sigara terminalleri, yapımları ve malzeme açısından düşünüyorum. Ayrıca, yylex()
işlevi uygularken , geçerli belirteci döndürmeye odaklanmamda bana yardımcı oldu ve ondan önce veya sonra ne olduğu konusunda endişelenmeyin. C ++ programcısı karakter düzeyinde, bu soyutlamanın yararı olmadan çalıştı ve daha karmaşık ve daha az verimli bir algoritma oluşturdu. Yavaş hızın C ++ 'ın kendisi veya herhangi bir kütüphaneyle bir ilgisi olmadığı sonucuna vardık. Hafızaya yüklenen dosyalar ile saf ayrıştırma hızını ölçtük; Eğer bir dosya tamponlama problemimiz olsaydı, yacc bunu çözmek için bizim seçim aracımız olmazdı.
Üstelik EKLEME İSTİYOR : Bu, genel olarak ortakları yazmak için bir reçete değil, sadece belirli bir durumda nasıl çalıştığına bir örnek.
Bu tamamen ayrıştırmanız gerekenlere bağlıdır. Kendinizi bir lexer'ın öğrenme eğrisine varacak kadar hızlı yuvarlayabilir misiniz? Ayrıştırılacak malzeme kararını daha sonra pişman olmayacak kadar statik mi? Mevcut uygulamaları aşırı karmaşık buluyor musunuz? Eğer öyleyse, eğlenerek kendi eğlencelerinizi yapın, ancak sadece bir öğrenme eğrisi kullanmıyorsanız.
Son zamanlarda, şimdiye kadar kullandığım en basit ve en kolay olan limon ayrıştırıcısını gerçekten sevdim . İşleri kolaylaştırmak uğruna, sadece çoğu ihtiyaç için kullanıyorum. SQLite, diğer bazı kayda değer projeleri olduğu gibi kullanır.
Ancak, ben hiç bir şekilde kullanmaya ihtiyacım olduğunda (bu nedenle, limon), yolumuza girmeden, lexers ile hiç ilgilenmiyorum. Olabilirsin ve öyleyse neden bir tane yapmıyorsun? Var olanı kullanmaya geri döneceğinize dair bir fikrim var, ama gerekiyorsa kaşınıyorsunuz :)
Amacın ne olduğuna bağlı.
Ayrıştırıcıların / derleyicilerin nasıl çalıştığını öğrenmeye mi çalışıyorsunuz? O zaman sıfırdan kendin yaz. Yaptıklarının tüm içeriğini ve çıkışlarını takdir etmeyi gerçekten öğrenmenin tek yolu bu. Son bir kaç aydır bir tane yazıyorum ve ilginç ve değerli bir deneyim oldu, özellikle de 'ah, bu yüzden dil X bunu neden yapıyor?'
Son başvuru tarihine bir başvuru için hızlı bir şekilde bir araya getirmeniz mi gerekiyor? O zaman belki bir çözümleyici aracı kullanın.
Önümüzdeki 10, 20, belki 30 yıl boyunca genişletmek isteyeceğiniz bir şeye mi ihtiyacınız var? Kendinizinkini yazın ve zaman ayırın. Buna değecek.
Martin Fowlers'ın dil tezgahı yaklaşımını düşündün mü ? Makaleden alıntı
Bir dil tezgahının denklemde yaptığı en belirgin değişiklik, harici DSL'ler yaratma kolaylığıdır. Artık bir çözümleyici yazmak zorunda değilsin. Soyut sözdizimini tanımlamanız gerekir - fakat bu aslında oldukça basit bir veri modelleme adımıdır. Ek olarak DSL'iniz güçlü bir IDE alır - bu editörü tanımlamak için biraz zaman harcamanıza rağmen. Jeneratör hala yapmanız gereken bir şey ve benim fikrim, hiç olmadığı kadar kolay olmadığı. Ancak, iyi ve basit bir DSL için bir jeneratör oluşturmak, egzersizin en kolay kısımlarından biridir.
Bunu okumak, kendi ayrıştırıcınızı yazma günlerinin bittiğini ve mevcut kütüphanelerden birini kullanmanın daha iyi olduğunu söyleyebilirim. Kütüphaneye hakim olduktan sonra, gelecekte oluşturacağınız tüm DSL'ler bu bilgiden yararlanır. Ayrıca, diğerleri ayrıştırma yaklaşımınızı öğrenmek zorunda değildir.
Yorumu kapsayacak şekilde düzenleyin (ve soruyu yeniden düzenlendi)
Kendinizinkini yuvarlamanın avantajları
Yani kısacası, ustalaşmak için güçlü bir motivasyona sahip olduğunuz, gerçekten zor bir problemin bağırsaklarına derinlemesine dalmak istediğinizde kendinize yuvarlanmalısınız.
Başkasının kütüphanesini kullanmanın avantajları
Bu nedenle, hızlı sonuç almak istiyorsanız başka birinin kütüphanesini kullanın.
Genel olarak, bu, soruna ve dolayısıyla çözüme ne kadar sahip olmak istediğinize bağlı. Eğer hepsini istiyorsan kendin yap.
Kendi yazınızı yazmanın en büyük avantajı kendi yazınızı nasıl yapacağınızı bilmenizdir. Yacc gibi bir alet kullanmanın en büyük avantajı, aleti nasıl kullanacağınızı bilmenizdir. İlk keşif için treetop hayranıyım .
Neden açık kaynaklı bir ayrıştırıcı jeneratöre dokunup kendin yapmıyorsun? Ayrıştırma jeneratörleri kullanmazsanız, kodunuzu korumak çok zor olacaktır, eğer büyük değişiklikler yaparsanız, dilinizin sözdizimini değiştirirsiniz.
Parserlerimde, kodları okunaklı kılmak için bazı ifadeler kullanmak için düzenli ifadeler kullandım (Perl tarzı). Ancak, ayrıştırıcı tarafından üretilen kod daha hızlı devlet tablolar ve uzun yaparak olabilir switch
- case
Eğer sürece kaynak kod boyutunu artırabilir s, .gitignore
onlarla.
Özel olarak yazılan ayrıştırıcılarımın iki örneği:
https://github.com/SHiNKiROU/DesignScript - TEMEL bir lehçe, çünkü dizi gösterimine bakmak için tembel olduğum için hata mesajı feda ettim, https://github.com/SHiNKiROU/ExprParser - Bir formül hesaplayıcısı. Garip metaprogramlama numaralarına dikkat edin
"Bu denenmiş ve test edilmiş" tekerleği "mi kullanmalıyım veya yeniden icat etmeli miyim?"