Derleyiciyi kendi dilinde yazma


204

Sezgisel olarak, bir dil derleyicisinin Fookendisi Foo'da yazılamayacağı anlaşılıyor . Daha spesifik olarak, dil için ilk derleyici FooFoo'da yazılamaz, ancak sonraki herhangi bir derleyici için yazılabilir Foo.

Ama bu gerçekten doğru mu? İlk derleyicisi "kendisinde" yazan bir dil hakkında okuma konusunda çok belirsiz bir anım var. Bu mümkün mü ve eğer mümkünse nasıl?



Bu çok eski bir soru, ama diyelim ki Java'da Foo dili için bir tercüman yazdım. Sonra dil foo ile kendi tercümanı yazdım. Foo hala JRE'ye ihtiyaç duyar mı?
George Xavier

Yanıtlar:


231

Buna "önyükleme" denir. Öncelikle diliniz için başka bir dilde (genellikle Java veya C) bir derleyici (veya yorumlayıcı) oluşturmanız gerekir. Bu yapıldıktan sonra, derleyicinin yeni bir sürümünü Foo dilinde yazabilirsiniz. Derleyiciyi derlemek için ilk önyükleme derleyicisini kullanın ve daha sonra bu derlenmiş derleyiciyi diğer her şeyi (gelecekteki sürümleri de dahil olmak üzere) derlemek için kullanın.

Çoğu dil, bu tarzda, kısmen dil tasarımcıları oluşturdukları dili kullanmayı sevdikleri ve önemsiz olmayan bir derleyici genellikle dilin ne kadar "tam" olabileceğine dair yararlı bir ölçüt oluşturduğu için oluşturulur.

Buna bir örnek Scala olabilir. İlk derleyicisi Martin Odersky'nin deney dili Pizza'da oluşturuldu. Sürüm 2.0 itibariyle, derleyici tamamen Scala'da yeniden yazıldı. Bu noktadan sonra, yeni Pizza derleyicisi gelecekteki yinelemeler için kendini derlemek için kullanılabileceği için tamamen atılabilir.


Belki aptalca bir soru: Derleyicinizi başka bir mikroişlemci mimarisine taşımak istiyorsanız, önyükleme işlemi o mimari için çalışan bir derleyiciden yeniden başlatılmalıdır. Bu doğru mu? Bu doğruysa, bu, derleyicinizi diğer mimarilere taşımak yararlı olabileceğinden (özellikle C gibi bir 'evrensel dilde yazılmışsa) ilk derleyiciyi tutmanın daha iyi olduğu anlamına mı gelir?
piertoni

2
@piertoni derleyici arka ucunu yeni mikroişlemciye yeniden hedeflemek genellikle daha kolay olurdu.
bstpierre


76

Dick Gabriel'in orijinal LISP yorumlayıcısını LISP'de kağıda çıplak kemikli bir versiyon yazarak ve el ile makine koduna monte ederek anlattığı bir Yazılım Mühendisliği Radyo podcastini dinlediğimi hatırlıyorum . O andan itibaren, LISP özelliklerinin geri kalanı hem LISP'de yazıldı hem de yorumlandı.


Her şey bir sürü el ile bir genesis transistöründen bootstrapped

47

Önceki cevaplara merak eklemek.

İşte Linux From Scratch kılavuzundan, GCC derleyicisini kaynağından oluşturmaya başladığı adımda bir alıntı. (Linux From Scratch, bir dağıtımın kurulumundan kökten farklı olan Linux'u kurmanın bir yoludur , çünkü hedef sistemin her bir ikili dosyasını derlemeniz gerekir .)

make bootstrap

'Bootstrap' hedefi sadece GCC'yi değil aynı zamanda birkaç kez de derler. İlk turda derlenen programları ikinci kez, sonra tekrar üçüncü kez derlemek için kullanır. Daha sonra bu ikinci ve üçüncü derlemeleri, kendini kusursuz bir şekilde üretebildiğinden emin olmak için karşılaştırır. Bu aynı zamanda doğru derlendiği anlamına gelir.

'Bootstrap' hedefinin kullanılması, hedef sistemin takım zincirini oluşturmak için kullanılan derleyicinin hedef derleyicinin aynı sürümüne sahip olmaması gerçeğiyle motive edilir. Bu şekilde ilerlemek, hedef sistemde kendini derleyebilen bir derleyici elde edeceğinden emin olur.


12
"Hedef sistemin her bir ikili dosyasını derlemelisiniz" ve yine de bir yerden aldığınız bir gcc ikili dosyasıyla başlamalısınız, çünkü kaynak kendini derleyemez. Acaba her ardışık gcc'yi yeniden derlemek için kullanılan her gcc ikilisinin soyunu takip edip etmediğinizi, K & R'nin orijinal C derleyicisine kadar geri dönecek misiniz?
robru

43

C için ilk derleyicinizi yazdığınızda, başka bir dilde yazarsınız. Şimdi, montajcıda C için bir derleyiciniz var. Sonunda, dizeleri ayrıştırmak, özellikle dizilerden kaçmak zorunda olduğunuz yere geleceksiniz. \nOndalık kod 10 (ve \r13, vb.) İle karakter dönüştürmek için kod yazacaksınız .

Bu derleyici hazır olduktan sonra, C'de yeniden uygulamaya başlayacaksınız. Bu işleme " önyükleme " adı verilir .

Dize ayrıştırma kodu şu hale gelir:

...
if (c == 92) { // backslash
    c = getc();
    if (c == 110) { // n
        return 10;
    } else if (c == 92) { // another backslash
        return 92;
    } else {
        ...
    }
}
...

Bu derlendiğinde, '\ n' yi anlayan bir ikili dosyaya sahip olursunuz. Bu, kaynak kodunu değiştirebileceğiniz anlamına gelir:

...
if (c == '\\') {
    c = getc();
    if (c == 'n') {
        return '\n';
    } else if (c == '\\') {
        return '\\';
    } else {
        ...
    }
}
...

Peki '\ n' 13 kodu nedir? İkilide! DNA gibi: C kaynak kodunu bu ikili ile derlemek bu bilgiyi miras alacaktır. Derleyici kendini derlerse, bu bilgiyi yavrularına aktaracaktır. Bu noktadan itibaren, derleyicinin ne yapacağını yalnızca kaynaktan görmenin bir yolu yoktur.

Bir virüsü bazı programların kaynağında gizlemek istiyorsanız, bunu şu şekilde yapabilirsiniz: Bir derleyicinin kaynağını alın, işlevleri derleyen işlevi bulun ve onunla değiştirin:

void compileFunction(char * name, char * filename, char * code) {
    if (strcmp("compileFunction", name) == 0 && strcmp("compile.c", filename) == 0) {
        code = A;
    } else if (strcmp("xxx", name) == 0 && strcmp("yyy.c", filename) == 0) {
        code = B;
    }

    ... code to compile the function body from the string in "code" ...
}

İlginç parçalar A ve B'dir. A, compileFunctionvirüsü dahil etmek için kaynak koddur , muhtemelen bir şekilde şifrelenmiştir, bu nedenle sonuçta elde edilen ikili dosyayı aramaktan belli değildir. Bu, derleyiciye derlemenin virüs enjeksiyon kodunu koruyabilmesini sağlar.

B, virüsümüzle değiştirmek istediğimiz işlev için aynıdır. Örneğin, muhtemelen Linux çekirdeğinden olan "login.c" kaynak dosyasındaki "login" işlevi olabilir. Bunu, normal hesaba ek olarak kök hesap için "joshua" şifresini kabul edecek bir sürümle değiştirebiliriz.

Bunu derleyip bir ikili olarak yayarsanız, virüse kaynağa bakarak bulmanın bir yolu olmayacaktır.

Fikrin orijinal kaynağı: https://web.archive.org/web/20070714062657/http://www.acm.org/classics/sep95/


1
İkinci yarının virüs bulaşmış derleyicileri yazmanın anlamı nedir? :)
mhvelplund

3
@mhvelplund Sadece önyüklemenin sizi nasıl öldürebileceği bilgisini yaymak.
Aaron Digulla

19

Kendi başına bir derleyici yazamazsınız çünkü başlangıç ​​kaynak kodunuzu derleyecek hiçbir şeyiniz yoktur. Bunu çözmek için iki yaklaşım vardır.

En az tercih edilen şudur. Montajcıya (yuck) minimal bir dil kümesi için minimal bir derleyici yazarsınız ve daha sonra bu derleyiciyi dilin ekstra özelliklerini uygulamak için kullanırsınız. Tüm dil özelliklerini içeren bir derleyiciye sahip oluncaya kadar yolunuzu oluşturun. Genellikle sadece başka seçeneğiniz olmadığında yapılan acı verici bir süreç.

Tercih edilen yaklaşım bir çapraz derleyici kullanmaktır. Hedef makinede çalışan çıktı oluşturmak için farklı bir makinede var olan bir derleyicinin arka ucunu değiştirirsiniz. Sonra güzel bir tam derleyici var ve hedef makine üzerinde çalışıyor. Bunun için en popüler olanı C dilidir, çünkü takas edilebilir takılabilir arka uçlara sahip birçok mevcut derleyici vardır.

Az bilinen bir gerçek, GNU C ++ derleyicisinin sadece C alt kümesini kullanan bir uygulaması olmasıdır. Bunun nedeni, daha sonra tam GNU C ++ derleyicisini oluşturmanıza izin veren yeni bir hedef makine için bir C derleyicisi bulmak genellikle kolaydır. Artık hedef makinede bir C ++ derleyicisine sahip önyükleme yaptınız.


14

Genel olarak, önce çalışan derleyicinin çalışma (ilkel ise) bir kesimine sahip olmanız gerekir - o zaman kendi kendine barındırma yapmayı düşünmeye başlayabilirsiniz. Bu aslında bazı dillerde önemli bir kilometre taşı olarak kabul edilmektedir.

"Mono" dan hatırladığım kadarıyla, muhtemelen çalışmasını sağlamak için yansımaya birkaç şey eklemeleri gerekecek: mono ekibi bazı şeylerin mümkün olmadığını gösteriyor Reflection.Emit; elbette, MS ekibi onların yanlış olduğunu kanıtlayabilir.

Bunun birkaç gerçek avantajı vardır: yeni başlayanlar için oldukça iyi bir birim testtir! Ve endişelenmeniz gereken tek bir diliniz var (yani bir C # uzmanı çok fazla C ++ bilmiyor olabilir; ama şimdi sizin C # derleyicisini düzeltebilir). Ama merak ediyorum burada iş başında bir miktar profesyonel gurur yok: sadece kendi kendine barındırma olmasını istiyorlar .

Oldukça derleyici değil, ama son zamanlarda kendi kendini barındıran bir sistem üzerinde çalışıyorum; kod üreteci kod üreteci oluşturmak için kullanılır ... böylece şema değişirse ben sadece kendi üzerinde çalıştırın: yeni sürüm. Bir hata varsa, sadece önceki bir sürüme geri dönüp tekrar deneyin. Çok kullanışlı ve bakımı çok kolay.


Güncelleme 1

Anders'in bu videosunu PDC'de izledim ve (yaklaşık bir saat içinde) çok daha geçerli nedenler veriyor - derleyici hakkında bir hizmet olarak. Sadece kayıt için.


4

İşte bir dökümü (aslında aranması zor bir konu):

Bu aynı zamanda PyPy ve Rubinius'un fikri :

(Bence bu Forth için de geçerli olabilir , ancak Forth hakkında hiçbir şey bilmiyorum.)


Smalltalk ile ilgili bir makalenin ilk bağlantısı, şu anda görünür yararlı ve anında bilgi içermeyen bir sayfaya işaret ediyor.
nbro

1

GNU Ada derleyicisi olan GNAT, tam olarak bir Ada derleyicisinin oluşturulmasını gerektirir. Bu, hali hazırda GNAT ikili olmayan bir platforma taşınırken bir acı olabilir.


1
Nedenini anlamıyorum? Birden fazla önyükleme yapmanız gerekmeyen bir kural yoktur (her yeni platformda olduğu gibi), mevcut bir platformla da çapraz derleme yapabilirsiniz.
Marco van de Voort

1

Aslında, derleyicilerin çoğu yukarıda belirtilen nedenlerle derledikleri dilde yazılır.

İlk önyükleme derleyicisi genellikle C, C ++ veya Assembly'de yazılır.


1

Mono projesi C # derleyicisi uzun zamandır "kendi kendine barındırılıyor", bunun anlamı C # 'da yazılmış olması.

Ne biliyorum derleyici saf C kodu olarak başlatıldı, ama ECMA "temel" özellikleri uygulandıktan sonra onlar derleyici C # yeniden yazmaya başladı.

Derleyiciyi aynı dilde yazmanın avantajlarının farkında değilim, ancak en azından dilin sunabileceği özelliklerle ilgili olması gerektiğinden emin değilim (örneğin, nesne odaklı programlamayı desteklemiyor) .

Daha fazla bilgiyi burada bulabilirsiniz .


1

Kendi içinde SLIC (Derleyicileri Uygulamak için Diller Sistemi) yazdım. Sonra el derleme içine derledi. Beş alt dilin tek bir derleyicisi olduğu için SLIC'de çok şey var:

  • SYNTAX Ayrıştırıcı Programlama Dili PPL
  • JENERATÖR LISP 2 tabanlı ağaç tarama PSEUDO kod oluşturma dili
  • ISO Sırası, PSEUDO kodu, Optimizasyon dili
  • PSEUDO Macro gibi Assembly kodu üreten dil.
  • MACHOP Dili tanımlayan montaj makinesi talimatı.

SLIC, CWIC'den (Derleyiciler Yazma ve Uygulama Derleyicisi) esinlenmiştir. Çoğu derleyici geliştirme paketinin aksine, SLIC ve CWIC uzman, alana özgü dillerle kod oluşturma konusunu ele aldı. SLIC, hedef makine özelliklerini ağaç tarama üreteci dilinden ayıran ISO, PSEUDO ve MACHOP alt dillerini ekleyerek CWIC kod üretimini genişletir.

LISP 2 ağaçları ve listeleri

LISP 2 tabanlı jeneratör dilinin dinamik bellek yönetim sistemi önemli bir bileşendir. Listeler köşeli parantez içine alınmış dilde ifade edilir, bileşenleri virgülle ayrılır, yani üç elemanlı [a, b, c] liste.

Ağaçlar:

     ADD
    /   \
  MPY     3
 /   \
5     x

ilk girişi bir düğüm nesnesi olan listelerle temsil edilir:

[ADD,[MPY,5,x],3]

Ağaçlar genellikle dallardan önce gelen düğüm ayrı olarak görüntülenir:

ADD[MPY[5,x],3]

LISP 2 tabanlı jeneratör fonksiyonları ile ayıklama

Bir üreteç işlevi adlandırılmış (ayrıştır) => eylem> çiftler kümesidir ...

<NAME>(<unparse>)=><action>;
      (<unparse>)=><action>;
            ...
      (<unparse>)=><action>;

Unparse ifadeleri, ağaç desenlerini ve / veya nesne türlerini birbirinden ayıran ve bu parçaları yordamsal eylemiyle işlenmek üzere yerel değişkene atayan testlerdir. Farklı argüman türlerini alan aşırı yüklenmiş bir işlev gibi. () => ... hariç kodlar sırasıyla test edilir. Karşılık gelen eylemi gerçekleştiren ilk başarılı çözüm. Eşitsiz ifadeler testleri söküyor. ADD [x, y], dallarını x ve y yerel değişkenlerine atayan iki dallı ADD ağacıyla eşleşir. Eylem basit bir ifade veya bir .BEGIN ... .END sınırlı kod bloğu olabilir. Bugün c tarzı {...} blokları kullanardım. Ağaç eşleştirme, [], unparse kuralları, döndürülen sonuçları eyleme geçiren jeneratörleri çağırabilir:

expr_gen(ADD[expr_gen(x),expr_gen(y)])=> x+y;

Özellikle yukarıdaki expr_gen unparse, iki dallı bir ADD ağacıyla eşleşir. Test modeli içinde, bir ağaç dalına yerleştirilen tek bir argüman üreteci bu dal ile çağrılır. Ancak argüman listesi, döndürülen nesnelerle atanan yerel değişkenlerdir. Ayrıştırmanın üstünde, iki dalın ADD ağacının sökülmesini belirtir, her dalın expr_gen öğesine yinelemeli olarak bastırılması. Sol dal dönüşü yerel değişkenlere x yerleştirilir. Benzer şekilde sağ dal, dönüş nesnesiyle birlikte expr_gen öğesine geçti. Yukarıdakiler sayısal ifade değerlendiricisinin bir parçası olabilir. Düğüm dizesi yerine yukarıda vektörler olarak adlandırılan kısayol özellikleri vardı, karşılık gelen eylemlerin bir vektörü ile bir düğüm vektörü kullanılabilir:

expr_gen(#node[expr_gen(x),expr_gen(y)])=> #action;

  node:   ADD, SUB, MPY, DIV;
  action: x+y, x-y, x*y, x/y;

        (NUMBER(x))=> x;
        (SYMBOL(x))=> val:(x);

Yukarıdaki daha eksiksiz ifade değerlendirici expr_gen sol daldan x'e ve sağ daldan y'ye dönüş atar. X ve y'de gerçekleştirilen ilgili hareket vektörü geri döndü. Son unparse => eylem çiftleri, sayısal ve sembol nesnelerle eşleşir.

Sembol ve sembol özellikleri

Semboller adlandırılmış niteliklere sahip olabilir. val: (x) x içinde bulunan sembol nesnesinin val özelliğine erişir. Genelleştirilmiş bir sembol tablosu yığını SLIC'in bir parçasıdır. SYMBOL tablosu, işlevler için yerel semboller sağlayarak itilebilir ve patlatılabilir. Yeni oluşturulan sembol üst sembol tablosunda kataloglanır. Sembol araması, üstteki tablodan ilk önce yığını geriye doğru sembol tablosu yığınını arar.

Makineden bağımsız kod oluşturma

SLIC'in jeneratör dili PSEUDO komut nesnelerini üretir ve bunları bir bölüm kod listesine ekler. Bir .FLUSH, PSEUDO kod listesinin çalışmasını ve her PSEUDO komutunu listeden kaldırmasını ve çağırmasını sağlar. Yürütmeden sonra bir PSEUDO nesnesi belleği serbest bırakılır. PSEUDO'ların ve JENERATÖR eylemlerinin prosedür organları, çıktıları dışında temel olarak aynı dildir. PSEUDO, makineden bağımsız kod sıralaması sağlayan montaj makroları olarak işlev görür. Belirli hedef makinenin ağaç tarama üreteci dilinden ayrılmasını sağlarlar. PSEUDO'lar makine kodunu çıkarmak için MACHOP işlevlerini çağırır. MACHOP'lar, montajlı sahte operasyonları (dc, sabit vb. Tanımlamak) ve makine talimatını veya vektörlü girişi kullanarak benzer formlu talimatlar ailesini tanımlamak için kullanılır. Parametreleri, talimatı oluşturan bir bit alan dizisine dönüştürürler. MACHOP çağrıları, montaj gibi görünmeli ve derleme listesinde montajın gösterileceği alanların yazdırma formatını sağlamalıdır. Örnek kodda kolayca eklenebilir, ancak orijinal dillerde olmayan c tarzı yorum kullanıyorum. MACHOP'lar, adreslenebilir bir bit belleğe kod üretmektedir. SLIC bağlayıcı derleyicinin çıktısını işler. Vektörlü girişi kullanan DEC-10 kullanıcı modu talimatları için bir MACHOP: MACHOP'lar, adreslenebilir bir bit belleğe kod üretmektedir. SLIC bağlayıcı derleyicinin çıktısını işler. Vektörlü girişi kullanan DEC-10 kullanıcı modu talimatları için bir MACHOP: MACHOP'lar, adreslenebilir bir bit belleğe kod üretmektedir. SLIC bağlayıcı derleyicinin çıktısını işler. Vektörlü girişi kullanan DEC-10 kullanıcı modu talimatları için bir MACHOP:

.MACHOP #opnm register,@indirect offset (index): // Instruction's parameters.
.MORG 36, O(18): $/36; // Align to 36 bit boundary print format: 18 bit octal $/36
O(9):  #opcd;          // Op code 9 bit octal print out
 (4):  register;       // 4 bit register field appended print
 (1):  indirect;       // 1 bit appended print
 (4):  index;          // 4 bit index register appended print
O(18): if (#opcd&&3==1) offset // immediate mode use value else
       else offset/36;         // memory address divide by 36
                               // to get word address.
// Vectored entry opcode table:
#opnm := MOVE, MOVEI, MOVEM, MOVES, MOVS, MOVSI, MOVSM, MOVSS,
         MOVN, MOVNI, MOVNM, MOVNS, MOVM, MOVMI, MOVMM, MOVMS,
         IMUL, IMULI, IMULM, IMULB, MUL,  MULI,  MULM,  MULB,
                           ...
         TDO,  TSO,   TDOE,  TSOE,  TDOA, TSOA,  TDON,  TSON;
// corresponding opcode value:
#opcd := 0O200, 0O201, 0O202, 0O203, 0O204, 0O205, 0O206, 0O207,
         0O210, 0O211, 0O212, 0O213, 0O214, 0O215, 0O216, 0O217,
         0O220, 0O221, 0O222, 0O223, 0O224, 0O225, 0O226, 0O227,
                           ...
         0O670, 0O671, 0O672, 0O673, 0O674, 0O675, 0O676, 0O677;

MORG 36, O (18): $ / 36; konumu sekiz bitlik 18 bitlik $ / 36 sözcük adresini yazdırarak 36 bitlik bir sınıra hizalar. 9 bit opcd, 4 bit yazmaç, dolaylı bit ve 4 bitlik dizin yazmaç birleştirilir ve tek bir 18 bitlik alan gibi yazdırılır. 18 bit adres / 36 veya anlık değer çıkarılır ve sekizlik olarak yazdırılır. MOVEI örneği r1 = 1 ve r2 = 2 ile yazdırılıyor:

400020 201082 000005            MOVEI r1,5(r2)

Derleyici montaj seçeneği ile oluşturulan derleme kodunu derleme listesine alırsınız.

Birlikte bağlayın

SLIC bağlayıcı, bağlantı ve sembol çözünürlüklerini işleyen bir kütüphane olarak sağlanır. Hedef çıktı çıktı dosya biçimlendirmesi hedef makineler için yazılmalı ve bağlayıcı kütüphane kitaplığına bağlanmalıdır.

Jeneratör dili, ağaçlara bir dosya yazabilir ve bunları okuyarak çok yollu bir derleyicinin uygulanmasına izin verir.

Kısa yazlık kod üretimi ve kökenleri

Ben SLIC gerçek bir derleyici derleyici olduğu anlaşılır olduğundan emin olmak için ilk kod üretimi üzerine gitti. SLIC, 1960'ların sonunda Systems Development Corporation'da geliştirilen CWIC'den (Derleyiciler Yazma ve Uygulama Derleyicisi) esinlenmiştir. CWIC, yalnızca JENERATÖR dilinden sayısal bayt kodu üreten SYNTAX ve GENERATOR dillerine sahipti. Bayt kodu, adlandırılmış bölümlerle ilişkili bellek tamponlarına yerleştirildi veya ekildi (CWICs belgelerinde kullanılan terim) .FLUSH ifadesi ile yazıldı. CWIC ile ilgili bir ACM belgesi ACM arşivlerinden edinilebilir.

Büyük bir programlama dilini başarıyla uygulamak

1970'lerin sonunda bir COBOL çapraz derleyicisi yazmak için SLIC kullanıldı. Yaklaşık 3 ay içinde çoğunlukla tek bir programcı tarafından tamamlandı. Gerektiğinde programcı ile biraz çalıştım. Başka bir programcı hedef TI-990 mini BİLGİSAYAR için çalışma zamanı kitaplığını ve MACHOP'ları yazdı. Bu COBOL derleyicisi, derlemede yazılan DEC-10 yerel COBOL derleyicisinden saniyede önemli ölçüde daha fazla satır derledi.

Derleyici hakkında daha fazla bilgi genellikle

Derleyiciyi sıfırdan yazmanın büyük bir kısmı çalışma zamanı kütüphanesidir. Bir sembol tablosuna ihtiyacınız var. Giriş ve çıkışa ihtiyacınız var. Dinamik bellek yönetimi vb. Bir derleyici için çalışma zamanı kitaplığını yazmaktan sonra derleyiciyi yazmak daha kolay olabilir. Ancak SLIC ile bu çalışma zamanı kütüphanesi, SLIC'de geliştirilen tüm derleyiciler için ortaktır. İki çalışma zamanı kitaplığı olduğunu unutmayın. Dilin (örneğin COBOL) hedef makinesi için bir tane. Diğeri de derleyici derleyicileri çalışma zamanı kütüphanesidir.

Sanırım bunların ayrıştırıcı jeneratörler olmadığını belirledim. Şimdi arka ucun biraz anlaşılmasıyla ayrıştırıcı programlama dilini açıklayabilirim.

Ayrıştırıcı programlama dili

Ayrıştırıcı basit denklemler şeklinde yazılmış formül kullanılarak yazılmıştır.

<name> <formula type operator> <expression> ;

En düşük seviyedeki dil elemanı karakterdir. Jetonlar, dilin karakterlerinin bir alt kümesinden oluşur. Karakter sınıfları, bu karakter alt kümelerini adlandırmak ve tanımlamak için kullanılır. İşleç tanımlayan karakter sınıfı iki nokta üst üste (:) karakteridir. Sınıfın üyesi olan karakterler tanımın sağ tarafında kodlanır. Yazdırılabilir karakterler, tekli 'dizeler içine alınır. Basılamayan ve özel karakterler sayısal sıralarıyla temsil edilebilir. Sınıf üyeleri bir alternatif ile ayrılır | Şebeke. Sınıf formülü noktalı virgülle biter. Karakter sınıfları daha önce tanımlanmış sınıfları içerebilir:

/*  Character Class Formula                                    class_mask */
bin: '0'|'1';                                                // 0b00000010
oct: bin|'2'|'3'|'4'|'5'|'6'|'7';                            // 0b00000110
dgt: oct|'8'|'9';                                            // 0b00001110
hex: dgt|'A'|'B'|'C'|'D'|'E'|'F'|'a'|'b'|'c'|'d'|'e'|'f';    // 0b00011110
upr:  'A'|'B'|'C'|'D'|'E'|'F'|'G'|'H'|'I'|'J'|'K'|'L'|'M'|
      'N'|'O'|'P'|'Q'|'R'|'S'|'T'|'U'|'V'|'W'|'X'|'Y'|'Z';   // 0b00100000
lwr:  'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i'|'j'|'k'|'l'|'m'|
      'n'|'o'|'p'|'q'|'r'|'s'|'t'|'u'|'v'|'w'|'x'|'y'|'z';   // 0b01000000
alpha:  upr|lwr;                                             // 0b01100000
alphanum: alpha|dgt;                                         // 0b01101110

Skip_class 0b00000001 önceden tanımlanmıştır ancak bir skip_class tanımlıyor olabilir.

Özetle: Bir karakter sınıfı, yalnızca bir karakter sabiti, bir karakterin sırası veya daha önce tanımlanmış bir karakter sınıfı olabilen bir alternatif listesidir. Karakter sınıflarını uygularken: Sınıf formülüne bir sınıf bit maskesi atanır. (Yukarıdaki yorumlarda gösterilmiştir) Herhangi bir gerçek veya sıralı karaktere sahip herhangi bir sınıf formülü, bir sınıf bitinin tahsis edilmesine neden olur. Bir maske, dahil edilen sınıf (lar) ın sınıf maskesini / maskelerini ayrılan bit (varsa) ile oring yaparak yapılır. Karakter sınıflarından bir sınıf tablosu oluşturulur. Bir karakterin sıralamasıyla dizine eklenen bir giriş, karakterin sınıf üyeliklerini gösteren bitler içerir. Sınıf testi yerinde yapılır. Karakter sıralamasında eax'ta bulunan bir IA-86 kod örneği sınıf testini gösterir:

test    byte ptr [eax+_classmap],dgt

Ardından a:

jne      <success>

veya

je       <failure>

IA-86 talimat kodu örnekleri kullanılıyor çünkü bence IA-86 talimatları bugün daha yaygın olarak biliniyor. Sınıf maskesini değerlendiren sınıf adı, ordinal (eax cinsinden) karakterlerle dizinlenmiş sınıf tablosu ile tahribatsız AND'tir. Sıfırdan farklı bir sonuç sınıf üyeliğini gösterir. (EAX karakteri içeren al (EAX'in en düşük 8 biti) dışında sıfırlanır).

Jetonlar bu eski derleyicilerde biraz farklıydı. Anahtar kelimeler jeton olarak açıklanmadı. Bunlar, ayrıştırıcı dilinde alıntılanan dize sabitleri ile eşleştirildi. Alıntılanan dizeler normalde tutulmaz. Değiştiriciler kullanılabilir. A + dizeyi eşleştirir. (ie + '-' başarılı olduğunda karakteri koruyan bir - karakteriyle eşleşir), işlemi (yani 'E') dizgiyi jetona ekler. Beyaz boşluk, ilk eşleşme yapılana kadar önde gelen SKIP_CLASS karakterlerini atlayan jeton formülüyle işlenir. Açık bir skip_class karakter eşleşmesinin, jetonun skip_class karakteriyle başlamasına izin vererek atlamayı durduracağını unutmayın. Dize jetonu formülü, tek tırnaklı bir çift karakterle veya çift tırnaklı bir dize ile eşleşen skip_class karakterlerini atlar. İlgilenilen bir "dize içinde" karakteri eşleşen bir dizedir:

string .. (''' .ANY ''' | '"' $(-"""" .ANY | """""","""") '"') MAKSTR[];

İlk alternatif, alıntı yapılan herhangi bir tek karakterle eşleşir. Doğru alternatif, tek bir karakteri temsil etmek için iki "karakteri bir arada kullanan çift tırnak işareti karakterleri içerebilen çift tırnaklı bir alıntı dizesiyle eşleşir. Bu formül kendi tanımında kullanılan dizeleri tanımlar. Sağ iç alternatif '"' $ (-" "" ".ANY |" "" "" "", "" "") '"', çift tırnaklı bir dize ile eşleşir. Bir çift tırnak işareti karakteriyle eşleştirmek için tek bir tırnak karakteri kullanabiliriz. Ancak "tırnak işareti" kullanmak istiyorsak, bir "karakter kullanmak istiyorsak, iki karakter kullanmalıyız." Örneğin, iç sol alternatifte, tırnak işareti dışında herhangi bir karakterle eşleşen:

-"""" .ANY

Negatif bir göz atma - "" "" başarılı olduğunda (bir "karakteri eşleşmediğinde) .ANY karakteriyle eşleşir (" bir karakter olamaz çünkü - "" "" bu olasılığı ortadan kaldırır). Doğru alternatif - "" "" ile "karakteri eşleştirerek başarısız olmak doğru alternatifti:

"""""",""""

iki "karakteri tek bir çift ile değiştirmeyi dener," "" "thw single" karakterini eklemeye çalışır. Kapatma dizesi alıntı karakterini geçemeyen her iki iç seçenek de eşleştirilir ve MAKSTR [] bir dize nesnesi oluşturmaya çağırılır. dizi, döngü başarılı olduğunda, operatör bir diziyi eşleştirmek için kullanılır.Tozör formülü önde gelen atlama sınıfı karakterlerini atla (beyaz boşluk) İlk eşleme yapıldıktan sonra skip_class atlama devre dışı bırakılır. [] kullanarak diğer dillerde programlanmış işlevleri çağırabiliriz. [], MAKBIN [], MAKOCT [], MAKHEX [], MAKFLOAT [] ve MAKINT [], eşleşen bir belirteç dizesini yazılan bir nesneye dönüştüren kitaplık işlevidir: Aşağıdaki sayı formülü oldukça karmaşık bir belirteç tanımayı gösterir:

number .. "0B" bin $bin MAKBIN[]        // binary integer
         |"0O" oct $oct MAKOCT[]        // octal integer
         |("0H"|"0X") hex $hex MAKHEX[] // hexadecimal integer
// look for decimal number determining if integer or floating point.
         | ('+'|+'-'|--)                // only - matters
           dgt $dgt                     // integer part
           ( +'.' $dgt                  // fractional part?
              ((+'E'|'e','E')           // exponent  part
               ('+'|+'-'|--)            // Only negative matters
               dgt(dgt(dgt|--)|--)|--)  // 1 2 or 3 digit exponent
             MAKFLOAT[] )               // floating point
           MAKINT[];                    // decimal integer

Yukarıdaki sayı belirteci formülü tamsayı ve kayan nokta sayılarını tanır. - alternatifleri her zaman başarılıdır. Hesaplamalarda sayısal nesneler kullanılabilir. Belirteç nesneleri, formülün başarısı üzerine ayrıştırma yığınına itilir. (+ 'E' | 'e', ​​'E') üssündeki kurşun ilginçtir. MAKEFLOAT [] için her zaman büyük E harfine sahip olmak istiyoruz. Ama biz küçük bir 'e' harfini 'E' kullanarak değiştirmeye izin veriyoruz.

Karakter sınıfı ve simge formülünün tutarlılıklarını fark etmiş olabilirsiniz. Ayrıştırma formülü, geri izleme alternatifleri ve ağaç inşaat operatörleri eklemeye devam ediyor. Geri izleme ve geri izleme olmayan alternatif işleçler, bir ifade düzeyi içinde karıştırılamaz. Geri izlemesiz karıştırma (a | b \ c) yapamayabilirsiniz | \ backtracking alternatifi ile. (a \ b \ c), (a | b | c) ve ((a | b) \ c) geçerlidir. \ Backtracking alternatifi, sol alternatifini denemeden önce ayrıştırma durumunu kaydeder ve hata durumunda doğru alternatifi denemeden önce ayrıştırma durumunu geri yükler. Bir dizi alternatifte ilk başarılı alternatif grubu tatmin eder. Başka alternatifler denenmez. Faktoring ve gruplama, sürekli ilerleyen bir ayrıştırma sağlar. Geri izleme alternatifi, sol alternatifini denemeden önce ayrıştırmanın kaydedilmiş bir durumunu oluşturur. Ayrıştırma kısmi bir eşleşme yapabilir ve sonra başarısız olursa geri izleme gereklidir:

(a b | c d)\ e

Yukarıda bir hata döndürürse alternatif cd denenir. Eğer o zaman c başarısızlık verirse, backtrack alternatifi denenecektir. Başarılı ve b başarısız olursa, ayrıştırma geri izlenir ve e denenir. Aynı şekilde başarısız bir c başarılı ve b başarısız olursa ayrıştırma geri izlenir ve alternatif e alınır. Geri izleme bir formül içinde sınırlı değildir. Herhangi bir ayrıştırma formülü herhangi bir zamanda kısmi bir eşleşme yapar ve başarısız olursa, ayrıştırma üst geri izlemeye sıfırlanır ve alternatifi alınır. Kod çıkarılırsa, derleme hatası oluştuğunda bir derleme hatası oluşabilir. Derlemeye başlamadan önce bir geri iz bırakılır. Geri dönüş hatası veya geri izleme derleyici hatasıdır. Geri izler istiflenir. Olumsuz ve pozitif kullanabilir miyiz? ayrıştırmayı ilerletmeden test etmek için operatörlere göz atın / bakın. dize testi olmak, yalnızca giriş durumunun kaydedilmesi ve sıfırlanması gereken bir adım öndedir. İleriye bakma, başarısız olmadan önce kısmi eşleşme yapan bir ayrıştırma ifadesi olacaktır. Geri izleme kullanılarak ileriye bir bakış uygulanır.

Ayrıştırıcı dili LL veya LR ayrıştırıcısı değildir. Ancak, ağaç yapısını programladığınız özyinelemeli düzgün bir ayrıştırıcı yazmak için bir programlama dili:

:<node name> creates a node object and pushes it onto the node stack.
..           Token formula create token objects and push them onto 
             the parse stack.
!<number>    pops the top node object and top <number> of parstack 
             entries into a list representation of the tree. The 
             tree then pushed onto the parse stack.
+[ ... ]+    creates a list of the parse stack entries created 
             between them:
              '(' +[argument $(',' argument]+ ')'
             could parse an argument list. into a list.

Yaygın olarak kullanılan bir ayrıştırma örneği aritmetik bir ifadedir:

Exp = Term $(('+':ADD|'-':SUB) Term!2); 
Term = Factor $(('*':MPY|'/':DIV) Factor!2);
Factor = ( number
         | id  ( '(' +[Exp $(',' Exp)]+ ')' :FUN!2
               | --)
         | '(' Exp ')" )
         (^' Factor:XPO!2 |--);

Exp ve Term bir döngü kullanarak solak bir ağaç oluşturur. Sağ özyineleme kullanan faktör sağ elini kullanan bir ağaç oluşturur:

d^(x+5)^3-a+b*c => ADD[SUB[EXP[EXP[d,ADD[x,5]],3],a],MPY[b,c]]

              ADD
             /   \
          SUB     MPY
         /   \   /   \
      EXP     a b     c
     /   \
    d     EXP     
         /   \
      ADD     3
     /   \
    x     5

İşte cc derleyicisinin bir kısmı, c stili yorumlarla SLIC'in güncellenmiş bir sürümü. İşlev türleri (dilbilgisi, belirteç, karakter sınıfı, jeneratör, PSEUDO veya MACHOP, kimliklerini izleyen ilk sözdizimleriyle belirlenir. Bu yukarıdan aşağıya ayrıştırıcılarla formül tanımlayan bir programla başlarsınız:

program = $((declaration            // A program is a sequence of
                                    // declarations terminated by
            |.EOF .STOP)            // End Of File finish & stop compile
           \                        // Backtrack: .EOF failed or
                                    // declaration long-failed.
             (ERRORX["?Error?"]     // report unknown error
                                    // flagging furthest parse point.
              $(-';' (.ANY          // find a ';'. skiping .ANY
                     | .STOP))      // character: .ANY fails on end of file
                                    // so .STOP ends the compile.
                                    // (-';') failing breaks loop.
              ';'));                // Match ';' and continue

declaration =  "#" directive                // Compiler directive.
             | comment                      // skips comment text
             | global        DECLAR[*1]     // Global linkage
             |(id                           // functions starting with an id:
                ( formula    PARSER[*1]     // Parsing formula
                | sequencer  GENERATOR[*1]  // Code generator
                | optimizer  ISO[*1]        // Optimizer
                | pseudo_op  PRODUCTION[*1] // Pseudo instruction
                | emitor_op  MACHOP[*1]     // Machine instruction
                )        // All the above start with an identifier
              \ (ERRORX["Syntax error."]
                 garbol);                    // skip over error.

// Ağacı oluştururken kimliğin nasıl kapatıldığını ve daha sonra nasıl birleştirildiğini not edin.

formula =   ("==" syntax  :BCKTRAK   // backtrack grammar formula
            |'='  syntax  :SYNTAX    // grammar formula.
            |':'  chclass :CLASS     // character class define
            |".." token   :TOKEN     // token formula
              )';' !2                // Combine node name with id 
                                     // parsed in calling declaration 
                                     // formula and tree produced
                                     // by the called syntax, token
                                     // or character class formula.
                $(-(.NL |"/*") (.ANY|.STOP)); Comment ; to line separator?

chclass = +[ letter $('|' letter) ]+;// a simple list of character codes
                                     // except 
letter  = char | number | id;        // when including another class

syntax  = seq ('|' alt1|'\' alt2 |--);

alt1    = seq:ALT!2 ('|' alt1|--);  Non-backtrack alternative sequence.

alt2    = seq:BKTK!2 ('\' alt2|--); backtrack alternative sequence

seq     = +[oper $oper]+;

oper    = test | action | '(' syntax ')' | comment; 

test    = string | id ('[' (arg_list| ,NILL) ']':GENCALL!2|.EMPTY);

action  = ':' id:NODE!1
        | '!' number:MAKTREE!1
        | "+["  seq "]+" :MAKLST!1;

//     C style comments
comment  = "//" $(-.NL .ANY)
         | "/*" $(-"*/" .ANY) "*/";

Ayrıştırıcı dilinin yorumlama ve hata gidermeyi nasıl ele aldığı dikkat çekicidir.

Sanırım soruyu cevapladım. SLIC'lerin halefinin büyük bir bölümünü burada yazmış olan cc dili. Henüz bunun için bir derleyici yok. Ama el montaj kodu, çıplak asm c veya c ++ fonksiyonları derlemek olabilir.


0

Evet, o dilde bir dil için derleyici yazabilirsiniz. Hayır, o dilin önyükleme yapması için ilk derleyiciye ihtiyacınız yok.

Bootstrap için ihtiyacınız olan şey dilin bir uygulamasıdır. Bu bir derleyici veya tercüman olabilir.

Tarihsel olarak, diller genellikle ya yorumlanmış diller ya da derlenmiş diller olarak düşünülüyordu. Tercümanlar sadece birincisi için yazılmıştır ve derleyiciler sadece ikincisi için yazılmıştır. Bu nedenle, genellikle bir derleyici bir dil için yazılırsa, ilk derleyici onu önyüklemek için başka bir dilde yazılır, daha sonra isteğe bağlı olarak, derleyici konu dili için yeniden yazılır. Ancak bunun yerine başka bir dilde tercüman yazmak bir seçenektir.

Bu sadece teorik değil. Şu anda bunu kendim yapıyorum. Kendimi geliştirdiğim bir dil olan bir derleyici olan Somon üzerinde çalışıyorum. İlk önce C'de bir Salmon derleyici oluşturdum ve şimdi derleyiciyi Salmon'da yazıyorum, böylece Salmon derleyicisini başka bir dilde yazılmış olan Salmon derleyicisine sahip olmadan çalıştırabilirim.


-1

Belki BNF'yi tanımlayan bir BNF yazabilirsiniz .


4
Gerçekten de (o kadar da zor değil), ancak tek pratik uygulaması bir ayrıştırıcı jeneratörde olacaktır.
Daniel Spiewak

Gerçekten de LIME ayrıştırıcı üretecini üretmek için bu yöntemi kullandım. Metagramın sınırlı, basitleştirilmiş, tablo şeklinde gösterimi basit bir özyinelemeli iniş ayrıştırıcısından geçer. Daha sonra LIME, dilbilgisi dili için bir ayrıştırıcı oluşturur ve daha sonra bu ayrıştırıcıyı, birisinin gerçekte ayrıştırıcı oluşturmakla ilgilenen dilbilgisini okumak için kullanır. Bu, yeni yazdıklarımı nasıl yazacağımı bilmeme gerek olmadığı anlamına geliyor. Sihir gibi geliyor.
Ian

Aslında yapamazsınız, çünkü BNF kendini tarif edemez. Terminal dışı sembollerin alıntılanmadığı yacc'de kullanılan gibi bir varyanta ihtiyacınız vardır.
Lorne Marquis

1
Bnf'yi <> tanınmadığından, bnf'yi tanımlamak için kullanamazsınız. EBNF, dilin sabit dize belirteçlerini alıntılayarak bunu düzeltti.
GK
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.