Bu ayrı teknolojilerin neden var olduğunu ve güçlü ve zayıf yönlerinin neler olduğunu daha iyi anlamak için, son birkaç gündür çok araştırma yaptım.
Halihazırda varolan cevapların bazıları farklılıklarının bir kısmını ima ediyordu, ancak tam resmi vermediler ve bir nevi fikirde görünüyorlardı, bu yüzden bu cevap yazıldı.
Bu açıklama uzun, ama önemli. Benimle ayı (eğer sabırsız iseniz Veya, bir akış şeması görmek için sonuna gidin).
Ayrıştırıcı Kombinatorler ve Ayrıştırıcı Jeneratörler arasındaki farkları anlamak için öncelikle var olan çeşitli ayrıştırma türleri arasındaki farkı anlamak gerekir.
ayrıştırma
Ayrıştırma, bir resmi dilbilgisine göre bir dizi sembolün analiz edilmesi işlemidir. (Bilişim Bilimi,), ayrıştırma, bir bilgisayarın bir dilde yazılmış metni anlamalarına izin vermek için kullanılır, genellikle yazılı metni temsil eden bir ayrıştırma ağacı oluşturur , farklı yazılı bölümlerin anlamlarını ağacın her düğümünde saklar. Bu ayrıştırma ağacı, daha sonra başka bir dile (birçok derleyicide kullanılan) çevirme, yazılı talimatları doğrudan bir şekilde yorumlama (SQL, HTML), Linters gibi araçların işlerini
yapmalarına izin verme gibi çeşitli amaçlar için kullanılabilir. , vb. Bazen bir ayrıştırma ağacı açıkça değildir.Oluşturulduktan sonra ağaçtaki her düğüm türünde yapılması gereken eylem doğrudan yürütülür. Bu, verimliliği artırıyor, ancak sualtında hala gizli bir ayrıştırma ağacı var.
Ayrıştırma işlemsel olarak zor bir sorundur. Bu konuda elli yılı aşkın bir süredir araştırma yapıldı, ancak hala öğrenilmesi gereken çok şey var.
Kabaca konuşursak, bir bilgisayarın girişi ayrıştırmasına izin veren dört genel algoritma vardır:
- LL ayrıştırma. (Bağlamsız, yukarıdan aşağıya ayrıştırma.)
- LR ayrıştırma. (Bağlamsız, aşağıdan yukarıya ayrıştırma.)
- PEG + Packrat ayrıştırma.
- Earley Ayrıştırma.
Bu ayrıştırma türlerinin çok genel ve teorik tanımlamalar olduğunu unutmayın. Bu algoritmaların her birini farklı işlemlerle fiziksel makinelere uygulamak için birçok yol vardır.
LL ve LR sadece Bağlamsız gramerlere bakabilir (yani; yazılan belirteçlerin etrafındaki içerik nasıl kullanıldığını anlamak için önemli değildir).
PEG / Packrat ayrıştırma ve Earley ayrıştırma çok daha az kullanılır: Earley ayrıştırma çok daha fazla gramer işleyebileceği için güzeldir (zorunlu olarak ücretsiz olmayanlar dahil) ancak daha az verimlidir (ejderha tarafından iddia edildiği gibi) kitap (bölüm 4.1.1); bu iddiaların hala doğru olup olmadığından emin değilim).
Ayrıştırma İfadesi Dilbilgisi + Paket-ayrıştırma , göreceli olarak verimli olan ve hem LL hem de LR'den daha fazla gramer işleyebilen bir yöntemdir, ancak aşağıda hızlı bir şekilde değinildiği gibi belirsizlikleri gizler.
LL (Soldan sağa, Soldan türetme)
Bu muhtemelen ayrıştırmayı düşünmenin en doğal yoludur. Buradaki düşünce, giriş dizgisindeki bir sonraki belirtecine bakmak ve daha sonra, bir ağaç yapısını oluşturmak için muhtemel çoklu tekrarlamalı çağrıların hangisinin yapılması gerektiğine karar vermektir.
Bu ağaç 'yukarıdan aşağıya' inşa edilmiştir, bu da ağacın kökünden başlayacağımız anlamına gelir ve dilbilgisi kurallarını giriş dizgisi boyunca hareket ettiğimiz şekilde hareket ettiririz. Ayrıca okunmakta olan 'infix' token akışı için bir 'postfix' eşdeğerinin oluşturulması olarak görülebilir.
LL tarzı ayrıştırma gerçekleştiren ayrıştırıcılar, belirtilen orijinal dilbilgisine çok benzemek için yazılabilir. Bu onları anlamak, hata ayıklamak ve geliştirmek nispeten kolay hale getirir. Klasik Ayrıştırıcı Birleştiriciler, LL tarzı bir ayrıştırıcı oluşturmak için bir araya getirilebilecek olan “lego parçalarından” başka bir şey değildir.
LR (Soldan sağa, en sağdan türetme)
LR ayrıştırması diğer yoldan aşağıya doğru hareket eder: Her adımda, yığındaki üst eleman (lar) gramerde
daha üst bir seviyeye indirgenip düşürülmeyeceklerini görmek için dilbilgisi listesiyle karşılaştırılır . Değilse, girdi akışından sonraki belirteç kaydırılır ve yığının üstüne yerleştirilir.
Sonunda, gramerimizdeki başlangıç kuralını temsil eden istif üzerinde tek bir düğümle sonuçlanırsak, bir program doğrudur.
lookahead
Bu iki sistemden birinde, bazen hangi seçimi yapacağınıza karar vermeden önce girdilerden daha fazla simge atmanız gerekir. Bu (0)
, (1)
, (k)
veya (*)
böyle bu iki genel algoritmalar, isimlerinin sonra göreceği -syntax LR(1)
veya LL(k)
. k
genellikle 'dilbilgisi ihtiyaç duyduğunuz kadar' *
anlamına gelirken, genellikle 'bu ayrıştırıcı geri izleme işlemini gerçekleştirir' anlamına gelir; bu uygulamanın uygulanması daha güçlü / kolaydır, ancak yalnızca ayrıştırmaya devam edebilen bir ayrıştırıcıdan çok daha yüksek bellek ve zaman kullanımına sahiptir doğrusal.
LR stili ayrıştırıcıların, 'ileriye bakmaya' karar verebilecekleri durumda, yığında zaten çok sayıda belirteç bulunduğunu, bu nedenle gönderilecek daha fazla bilgiye sahip olduklarını unutmayın. Bu, aynı dilbilgisi için bir LL tarzı ayrıştırıcıdan daha az 'bakıma' ihtiyaç duymaları anlamına gelir.
LL - LR: Belirsizlik
Yukarıdaki iki tanımı okurken, LL tarzı ayrıştırmanın çok daha doğal göründüğü için LR tarzı ayrıştırmanın neden var olduğunu merak edebilirsiniz.
Bununla birlikte, LL stili ayrıştırmada bir sorun var: Left Recursion .
Böyle bir gramer yazmak çok doğaldır:
expr ::= expr '+' expr | term
term ::= integer | float
Ancak, LL tarzı bir çözümleyici bu dilbilgisini ayrıştırırken sonsuz bir özyinelemeli döngüde sıkışıp kalacak: expr
Kuralın en soldaki olasılığını denerken, herhangi bir girdi kullanmadan tekrar bu kurala geri çekilir.
Bu sorunu çözmenin yolları var. En basit olanı, gramerinizi yeniden yazmaktır; böylece bu tür bir özyineleme artık gerçekleşmez:
expr ::= term expr_rest
expr_rest ::= '+' expr | ϵ
term ::= integer | float
(Burada ϵ 'boş dize' anlamına gelir)
Bu dilbilgisi şimdi doğru özyinelemeli. Hemen okumak daha zor olduğunu unutmayın.
Uygulamada, sol özyineleme, aradaki birçok adımla dolaylı olarak gerçekleşebilir . Bu dikkat edilmesi zor bir problemdir. Ancak bunu çözmeye çalışmak, gramerinizi okumayı zorlaştırır.
Dragon Kitabın 2.5 Bölümünde belirtildiği gibi:
Bir anlaşmazlık var gibi görünüyor: bir yandan çeviriyi kolaylaştıran dilbilgisine ihtiyacımız var, diğer taraftan ayrıştırmayı kolaylaştıran önemli ölçüde farklı bir dilbilgisine ihtiyacımız var. Çözüm, kolay çeviri için gramer ile başlamak ve ayrıştırmayı kolaylaştırmak için dikkatlice dönüştürmektir. Sol özyinelemeyi elimine ederek, öngörülü özyinelemeli bir tercümanda kullanıma uygun bir gramer elde edebiliriz.
LR stili ayrıştırıcılar, ağacı aşağıdan yukarıya doğru inşa ederken bu sol-özyinelemeyle ilgili bir problemi yoktur.
Ancak , (genellikle bir şekilde uygulanan bir LR tarzı ayrıştırıcı yukarıdaki gibi bir gramer zihinsel çeviri Sonlu-Devlet Automaton )
+ (ve hataya açık) sıklıkta devletlerin yüzlerce veya binlerce vardır yapmak çok zordur durum geçişlerini dikkate almak. Bu nedenle LR stili ayrıştırıcılar genellikle 'derleyici derleyici' olarak da bilinen bir Ayrıştırıcı Jeneratör tarafından oluşturulur .
Belirsizlikler nasıl çözülür?
Yukarıdaki Sol yinelemeyle ilgili belirsizlikleri çözmek için iki yöntem gördük: 1) sözdizimini yeniden yazın 2) bir LR ayrıştırıcısı kullanın.
Ancak çözülmesi zor olan başka tür belirsizlikler var: Aynı anda iki farklı kural aynı şekilde uygulanabilirse ne olur?
Bazı yaygın örnekler:
Hem LL stili hem de LR stili ayrıştırıcılarında bunlarla ilgili sorunlar var. Aritmetik ifadelerin ayrıştırılmasıyla ilgili problemler, operatör önceliği belirtilerek çözülebilir. Benzer bir şekilde, Dangling Else gibi diğer sorunlar, bir öncelikli davranış seçerek ve buna bağlı kalarak çözülebilir. (C / C ++ 'da, örneğin, sarkan başka her zaman' if 'en yakınına aittir).
Buna başka bir 'çözüm', Ayrıştırıcı İfade Dilbilgisi (PEG) kullanmaktır: Bu, yukarıda kullanılan BNF dilbilgisine benzer, ancak belirsizlik durumunda, her zaman 'ilkini seç'. Elbette, bu sorunu gerçekten 'çözmez', ancak belirsizliğin gerçekte var olduğunu gizleyin: Son kullanıcılar ayrıştırıcının hangi seçimi yaptığını bilemeyebilir ve bu beklenmedik sonuçlara yol açabilir.
Dilbilginizin herhangi bir belirsizliğe sahip olup olmadığını bilmek neden genel olarak imkansızdır da dahil olmak üzere, bu yazıdan çok daha derinlemesine olan daha fazla bilgi: Bu bağlamda, LL ve LR bağlamındaki harika blog makalesidir : Neden ayrıştırma? araçlar zor . Bunu gerçekten tavsiye ederim; Şu anda bahsettiğim her şeyi anlamamda bana çok yardımcı oldu.
50 yıllık araştırma
Ama hayat devam ediyor. Sonlu durum otomatları olarak uygulanan 'normal' LR stili ayrıştırıcıların çoğu zaman program boyutunda sorun olan binlerce duruma + geçişe ihtiyaç duyduğu ortaya çıktı. Böylece, Basit LR (SLR) ve LALR (Önden Görünümlü LR) gibi değişkenler , otomatiği küçültmek için diğer teknikleri birleştiren, ayrıştırıcı programların disk ve bellek alanını azaltan yazılmıştır.
Ayrıca, yukarıda belirtilen belirsizlikleri çözmenin bir başka yolu, belirsizlik durumunda, her iki seçeneğin de tutulduğu ve ayrıştırıldığı genelleştirilmiş tekniklerin kullanılmasıdır : 'doğru' olanı), her ikisinin de doğru olması durumunda ikisini (ve bu şekilde belirsizlik olduğunu göstererek) geri döndürmenin yanı sıra.
İlginç bir şekilde, Genelleştirilmiş LR algoritması tanımlandıktan sonra, benzer şekilde hızlı olan belirsiz gramerler için $ O (n ^ 3) $ zaman karmaşıklığı, $ O (n) olan Genelleştirilmiş LL ayrıştırıcılarını uygulamak için benzer bir yaklaşımın kullanılabileceği ortaya çıktı. Basit (LA) bir LR ayrıştırıcısından daha fazla defter tutma olmasına rağmen, daha net bir gramer faktörü anlamına gelse de, tamamen net olmayan dilbilgileri yazmak ve hata ayıklamak için.
Ayrıştırıcı Kombinatorler, Ayrıştırıcı Jeneratörler
Yani, bu uzun açıklama ile, şimdi sorunun özüne varıyoruz:
Ayrıştırıcı Birleştiricileri ve Ayrıştırıcı Jeneratörleri arasındaki fark nedir ve biri diğerinde ne zaman kullanılmalıdır?
Onlar gerçekten farklı türde canavarlar:
Ayrıştırıcı Birleştiriciler oluşturuldu, çünkü insanlar yukarıdan aşağıya ayrıştırıcı yazıyorlardı ve bunların çoğunun ortak olduğunu biliyordu .
Ayrıştırma Jeneratörleri , insanlar LL stili ayrıştırıcıların (yani LR tarzı ayrıştırıcılar) el ile yapması çok zor olan problemleri olmayan ayrıştırıcılar oluşturmak istedikleri için yaratıldı. Yaygın olanlar arasında (LA) LR) uygulayan Yacc / Bison bulunur.
İlginçtir, günümüzde manzara biraz karışmıştır:
GLL algoritmasıyla çalışan Ayrıştırıcı Kombinatorlerini yazmak , klasik LL tarzı ayrıştırıcıların sahip olduğu belirsizlik sorunlarını çözerken, her türlü yukarıdan aşağıya ayrıştırma kadar okunabilir / anlaşılabilir olmak mümkündür.
Ayrıştırma Jeneratörleri LL stili ayrıştırıcılar için de yazılabilir. ANTLR , tam olarak bunu yapar ve klasik LL tarzı ayrıştırıcıların sahip olduğu belirsizlikleri çözmek için diğer sezgisel taramaları (Adaptif LL (*)) kullanır.
Genel olarak, bir LR ayrıştırıcı üreteci oluşturmak ve ve grameriniz üzerinde çalışan bir (LA) LR tarzı ayrıştırıcı üreteci hatalarını ayıklamak, orijinal dilbilginizin 'iç-dış' LR biçimine çevrilmesi nedeniyle zordur. Öte yandan, Yacc / Bison gibi araçlar optimizasyonlar uzun yıllar vardı ve birçok kişi artık olarak kabul edebileceğimiz anlamına vahşi, kullanım çok gördük ayrıştırma yapmak için yolda ve yeni yaklaşımlara doğru şüpheciler.
Hangisini kullanmanız gerektiği, gramerinizin ne kadar zor olduğuna ve ayrıştırıcının ne kadar hızlı olması gerektiğine bağlıdır. Dilbilgisine bağlı olarak, bu tekniklerden biri (/ farklı tekniklerin uygulamaları) daha hızlı olabilir, daha küçük bir bellek ayak izine sahip olabilir, daha küçük bir disk ayak izine sahip olabilir veya diğerlerinden daha fazla genişletilebilir veya daha kolay hata ayıklanabilir. Kilometreniz değişebilir .
Yan not: Sözlüksel Analiz konusunda.
Sözlüksel Analiz hem Ayrıştırıcı Kombinatorler hem de Ayrıştırıcı Jeneratörleri için kullanılabilir. Buradaki fikir, kaynak kodunuz üzerinden ilk geçişi gerçekleştiren, örneğin beyaz boşlukları, yorumları vb. dilinizi oluşturan farklı unsurlar kaba şekilde.
Temel avantaj, bu ilk adımın gerçek ayrıştırıcıyı çok daha basit hale getirmesidir (ve bu nedenle daha hızlı olması nedeniyle). Başlıca dezavantajı, ayrı bir çeviri adımına sahip olmanız ve örneğin satır ve sütun numaralarıyla hata bildirimi, beyaz boşluğun kaldırılması nedeniyle zorlaşıyor olmasıdır.
Sonunda bir lexer 'sadece' başka bir ayrıştırıcıdır ve yukarıdaki tekniklerden herhangi biri kullanılarak uygulanabilir. Sadeliği nedeniyle, genellikle ana ayrıştırıcıdan başka teknikler kullanılır ve örneğin ekstra 'lexer jeneratörler' vardır.
Tl; Dr:
İşte çoğu durumda geçerli olan bir akış şeması:
javac
, Scala) için tercih edilen uygulama şeklidir . Size iyi hata mesajları üretme konusunda yardımcı olan iç ayrıştırıcı durumu üzerinde en fazla kontrolü verir (ki bu son yıllarda…