Derleyiciler statik olarak kontrol edildiğinde "karmaşık" ifadeler yazarken kullanılan genel prosedür nedir?


23

Not: Başlıkta "karmaşık" kelimesini kullandığımda, ifadenin çok sayıda işleci ve işleci olduğu anlamına gelir. İfadenin kendisinin karmaşık olması değil.


Son zamanlarda x86-64 montaj için basit bir derleyici üzerinde çalışıyorum. Derleyicinin ana ön ucunu (lexer ve parser) bitirdim ve şimdi programımın Soyut Sözdizimi Ağacı temsilini oluşturabiliyorum. Dilim statik olarak yazılacağı için, şimdi bir sonraki aşamayı yapıyorum: kaynak kodunu denetleyerek yazın. Ancak bir sorunla karşılaştım ve bunu kendim için makul bir şekilde çözemedim.

Aşağıdaki örneği düşünün:

Derleyicimin ayrıştırıcısı şu kod satırını okudu:

int a = 1 + 2 - 3 * 4 - 5

Ve aşağıdaki AST'ye dönüştürdü:

       =
     /   \
  a(int)  \
           -
         /   \
        -     5
      /   \
     +     *
    / \   / \
   1   2 3   4

Şimdi AST'yi kontrol etmeli. ilk tip =operatörü kontrol ederek başlar . İlk önce operatörün sol tarafını kontrol eder. Değişkenin abir tamsayı olarak bildirildiğini görür . Bu yüzden şimdi sağ taraftaki ifadenin bir tamsayı olarak değerlendirildiğini doğrulamalıdır.

İfade 1ya da gibi tek bir değerse, bunun nasıl yapılabileceğini anlıyorum 'a'. Fakat bu , yukarıdaki gibi bir çok değer ve işlenen - karmaşık bir ifade - ifadesi için nasıl yapılır ? İfadenin değerini doğru bir şekilde belirlemek için, tür denetleyicisinin gerçek ifadenin kendisini yürütmesi ve sonucu kaydetmesi gerekir gibi görünüyor . Ancak bu açıkça derleme ve yürütme aşamalarını ayırma amacını ortadan kaldırıyor gibi görünmektedir.

Bunu yapabileceğimi düşünmenin tek yolu AST'deki her bir alt ifadenin yaprağını tekrar tekrar kontrol etmek ve tüm yaprak türlerinin beklenen operatör tipine uyduğunu doğrulamaktır. =Operatörden başlayarak , tip denetleyicisi sol taraftaki AST'nin tamamını tarar ve yaprakların tüm sayıların tam olduğunu doğrular. Daha sonra alt ifadedeki her operatör için bunu tekrar eder.

"The Dragon Book" adlı kopyamdaki konuyu araştırmayı denedim , ama pek de ayrıntılı bir şekilde görünmüyor ve zaten bildiklerimi yineliyor.

Bir derleyici birçok işleç ve işleçle ifade denetleme türü olduğunda, genel yöntem nedir? Yukarıda bahsettiğim yöntemlerden herhangi biri kullanılmış mı? Eğer değilse, yöntemler nelerdir ve tam olarak nasıl çalışırlar?


8
Bir ifadenin türünü kontrol etmenin açık ve basit bir yolu var. Bize "ahlaksız" dediği şeyi söylesen iyi edersin.
gnasher729

12
Genel yöntem "ikinci yöntem" dir: derleyici, alt ifadelerinin türlerinden karmaşık ifadenin türünü etkiler. Bu, anlambilimsel anlambilimin ve bu güne kadar yaratılan tip sistemlerinin çoğunun temel noktasıydı.
Joker_vD

5
İki yaklaşım farklı davranışlar üretebilir: Yukarıdan aşağıya yaklaşım double a = 7/2 , sağ tarafı çift olarak yorumlamaya çalışır, bu nedenle pay ve paydayı çift olarak yorumlamaya ve gerekirse bunları dönüştürmeye çalışır; sonuç olarak a = 3.5. Aşağıdan yukarıya, tamsayı bölme işlemini gerçekleştirir ve yalnızca son adımda (atama) dönüştürür a = 3.0.
Hagen von Eitzen,

3
Senin AST resmi ifadesi ile uyuşmuyor o Bildirimi int a = 1 + 2 - 3 * 4 - 5bunlarlaint a = 5 - ((4*3) - (1+2))
Basile Starynkevitch

22
İfadeyi değerler yerine türlerde "çalıştırabilirsin"; örneğin int + intolur int.

Yanıtlar:


14

Özyineleme cevaptır, ancak işlemi yapmadan önce her bir alt ağacın içine inersiniz:

int a = 1 + 2 - 3 * 4 - 5

ağaç formuna:

(assign (a) (sub (sub (add (1) (2)) (mul (3) (4))) (5))

Türün çıkarımı, önce sol tarafa, sonra sağ tarafa yürüyüp, ardından işleçlerin türlerinin çıkarıldığı anda operatörü ele alarak gerçekleşir:

(assign*(a) (sub (sub (add (1) (2)) (mul (3) (4))) (5))

-> lhs'ye inmek

(assign (a*) (sub (sub (add (1) (2)) (mul (3) (4))) (5))

-> çıkarım a. aolduğu bilinmektedir int. assignŞimdi düğüme geri döndük :

(assign (int:a)*(sub (sub (add (1) (2)) (mul (3) (4))) (5))

-> ilginç bir şeye çarpıncaya kadar rhs, ardından iç operatörlerin lhs iniş

(assign (int:a) (sub*(sub (add (1) (2)) (mul (3) (4))) (5))
(assign (int:a) (sub (sub*(add (1) (2)) (mul (3) (4))) (5))
(assign (int:a) (sub (sub (add*(1) (2)) (mul (3) (4))) (5))
(assign (int:a) (sub (sub (add (1*) (2)) (mul (3) (4))) (5))

-> tipine anlaması 1olduğunu intebeveyne ve karşılığında

(assign (int:a) (sub (sub (add (int:1)*(2)) (mul (3) (4))) (5))

-> rhs gir

(assign (int:a) (sub (sub (add (int:1) (2*)) (mul (3) (4))) (5))

-> tipine anlaması 2olduğunu intebeveyne ve karşılığında

(assign (int:a) (sub (sub (add (int:1) (int:2)*) (mul (3) (4))) (5))

-> tipine anlaması add(int, int)olduğunu intebeveyne ve karşılığında

(assign (int:a) (sub (sub (int:add (int:1) (int:2))*(mul (3) (4))) (5))

-> rhs inmek

(assign (int:a) (sub (sub (int:add (int:1) (int:2)) (mul*(3) (4))) (5))

vb ile sona erene kadar

(assign (int:a) (int:sub (int:sub (int:add (int:1) (int:2)) (int:mul (int:3) (int:4))) (int:5))*

Ödevin kendisinin de türüne sahip bir ifade olup olmadığı dilinize bağlıdır.

Önemli paket: ağaçtaki herhangi bir operatör düğümünün türünü belirlemek için, yalnızca kendilerine önceden atanmış bir tür olması gereken acil çocuklarına bakmanız gerekir.


43

Bir derleyici birçok işleç ve işlenenle yapılan ifadeleri denetlerken, genellikle kullanılan yöntem nedir.

Tip sistemi ve tip çıkarımı ile birleşmeyi kullanan Hindley-Milner tipindeki sistemdeki uyarıları okuyun . Terimbilimsel anlambilim ve işlemsel anlambilim hakkında da okuyun .

Tip kontrolü aşağıdaki durumlarda daha kolay olabilir:

  • gibi tüm değişkenleriniz aaçıkça bir tür ile bildirilir. Bu, C veya Pascal veya C ++ 98 gibidir, ancak bazı tip çıkarımları olan C ++ 11 gibi değildir auto.
  • tüm edebi değerler gibi 1, 2ya 'c'doğal bir türü var: Bir int değişmezi hep türü olan int, bir karakter değişmezi hep türü vardır char, ....
  • fonksiyonlar ve operatörler aşırı yüklenmemiş, örneğin +operatör her zaman tipe sahip (int, int) -> int. C, operatörler için aşırı yüklemeye sahiptir ( +işaretli ve işaretsiz tamsayı tipleri ve çiftler için çalışır) ancak işlevlerin aşırı yüklenmesi yoktur.

Bu kısıtlamalar altında, aşağıdan yukarıya bir özyinelemeli AST tipi dekorasyon algoritması yeterli olabilir (bu sadece türlerle ilgilenir , somut değerlerle değil, derleme zamanı yaklaşımıyla da ilgilidir):

  • Her kapsam için, tüm görünür değişken türlerine (çevre adı verilen) bir tablo tutarsınız. Bir açıklamadan sonra int agirişi a: inttabloya eklersiniz .

  • Yaprakları yazmak, önemsiz özyinelemede temel bir durumdur: benzeyen değişmezlerin türü 1zaten bilinir ve açevrede değişkenlerin türü aranabilir.

  • Önceden hesaplanan (iç içe alt ifadeler) işlenenlerin türlerine göre bazı işleç ve işlenenlerle bir ifade yazmak için işlenenler üzerinde özyineleme kullanıyoruz (bu yüzden önce bu alt ifadeleri yazıyoruz) ve işleçle ilgili yazım kurallarını izliyoruz .

Öyleyse, örneğinizde 4 * 3ve 1 + 2yazılmıştır intçünkü 4& 3ve 1& 2daha önce yazılmıştır intve yazım kurallarınız iki- intöğelerin toplamının veya ürününün bir intvb. Olduğunu söyler (4 * 3) - (1 + 2).

Sonra Pierce'in Çeşitlerini ve Programlama Dilleri kitabını okuyun . Küçük bir miktar Ocaml ve λ-calculus öğrenmenizi tavsiye ederim.

Daha dinamik olarak yazılmış diller için (Lisp benzeri), Queinnec'in Lisp In Küçük Parçalar bölümünü de okuyun.

Ayrıca Scott'ın Programlama Dilleri Pragmatik kitabını okuyun.

BTW, bir dil agnostik yazım koduna sahip olamazsınız, çünkü tür sistemi dilin anlambiliminin önemli bir parçasıdır .


2
C ++ 11'ler nasıl autodaha basit değildir? Bu olmadan, sağ taraftaki yazıyı bulmak zorundasınız, sonra sol taraftaki yazıyla bir eşleşme veya dönüşüm olup olmadığını görmelisiniz. İle autosize sadece sağ tarafının türünü anlamaya ve bitirdiniz.
nwp

3
@nwp C ++ auto, C # varve Go :=değişken tanımlarının genel fikri çok basittir: tanımın sağ tarafını kontrol edin. Sonuçta ortaya çıkan tür sol taraftaki değişkenin türüdür. Fakat şeytan ayrıntıda gizlidir. Örneğin, C ++ tanımları kendi kendine referans olabilir, böylece rhs'de bildirilen değişkene başvurabilirsiniz, örn int i = f(&i). iTürünün çıkarımı varsa yukarıdaki algoritma başarısız olur: türünün içıkarımı türünü bilmeniz gerekir i. Bunun yerine, tür değişkenleriyle tam HM tarzı tür çıkarımı yapmanız gerekir.
amon

13

C'de (ve açıkça C'ye dayanan en statik şekilde yazılmış diller), her operatör bir işlev çağrısı için sözdizimsel şeker olarak görülebilir.

Yani ifadeniz şu şekilde yeniden yazılabilir:

int a{operator-(operator-(operator+(1,2),operator*(3,4)),5)};

Ardından aşırı yük çözünürlüğü devreye girer ve her işlevin (int, int)ya da (const int&, const int&)türünde olduğuna karar verir .

Bu yol, tip çözünürlüğünün anlaşılmasını ve takip edilmesini ve (daha da önemlisi) uygulanmasını kolaylaştırır. Tipler hakkındaki bilgiler sadece 1 şekilde akar (iç ifadelerden dışa doğru).

İşte nedeni budur double x = 1/2;sonuçlanacaktır x == 0çünkü 1/2bir int ifadesi olarak değerlendirilir.


6
+İşlev çağrıları gibi doubleint
işlenmeyen

2
@BasileStarynkevitch: Sunucu aşırı fonksiyonları bir dizi gibi uygulanacağı: operator+(int,int), operator+(double,double), operator+(char*,size_t)vb ayrıştırıcı sadece takip etmek zorundadır biri seçilir.
Mooing Duck

3
@aschepler Kimse kaynak olmayan ve spec düzeyinde, C almayı öneriyorum aslında fonksiyonlarını veya motor fonksiyonlarını aşırı yüklenmiş
kedi

1
Tabii ki değil. Sadece bir C ayrıştırıcısı durumunda, bir "işlev çağrısı" nın burada açıklandığı gibi "işlev çağrıları olarak işleçler" ile pek ortak bir yanı bulunmadığını belirtmeniz gerekir. Aslında, C'de, türünü f(a,b)bulmak, türünü bulmaktan biraz daha kolaydır a+b.
aschepler

2
Herhangi bir makul C derleyicisinin birden fazla fazı vardır. Ön tarafta (önişlemciden sonra) AST oluşturan ayrıştırıcıyı bulursunuz. İşte operatörlerin fonksiyon çağrıları olmadığı oldukça açık. Ancak, kod oluşturmada, artık hangi dil yapısının bir AST düğümü oluşturduğuyla ilgilenmiyorsunuz. Düğümün özellikleri, düğümün nasıl işlendiğini belirler. Özellikle, + çok iyi bir işlev çağrısı olabilir - bu genellikle taklit kayan nokta matematiğine sahip platformlarda olur. Öykünülmüş FP matematiğini kullanma kararı kod oluşturmada gerçekleşir; Daha önce AST farkı gerekli değil.
MSalters

6

Algoritmanıza odaklanarak aşağıdan yukarıya değiştirmeyi deneyin. Pf değişkenlerini ve sabitlerini bilirsiniz; operatörü taşıyan düğümü sonuç türüyle etiketleyin. Yaprağın operatörün türünü ve aynı zamanda fikrinizin tersini belirleyebilmesine izin verin .


6

+Tek bir kavramdan ziyade çeşitli fonksiyonlar olarak düşündüğünüz sürece aslında oldukça kolaydır .

    int operator=(int)
     /   \
  a(int)  \
        int operator-(int,int)
         /                  \
    int operator-(int,int)    5
         /              \
int operator+(int,int) int operator*(int,int)
    / \                      / \
   1   2                    3   4

Sağ tarafın ayrıştırma aşaması sırasında, ayrıştırıcı alır 1, bunu bilir int, daha sonra ayrıştırır +ve bunu "çözülmemiş bir işlev adı" olarak saklar, sonra bunu ayrıştırır 2, onun bir olduğunu bilir intve sonra yığına geri döndürür. +Fonksiyon düğüm artık hem parametre türlerini bilir, bu yüzden çözebilirsiniz +INTO int operator+(int, int), şimdi bu alt ifadenin türünü bilir ve ayrıştırıcı 's neşeli yolda devam eder.

Gördüğünüz gibi, ağaç tamamen kurulduktan sonra, fonksiyon çağrıları da dahil olmak üzere her bir düğüm kendi türünü bilir. Bu anahtardır çünkü parametrelerinden farklı tipler döndüren fonksiyonlara izin verir.

char* ptr = itoa(3);

İşte, ağaç:

    char* itoa(int)
     /           \
  ptr(char*)      3

4

Tip kontrolünün temeli derleyicinin yaptığı değil, dilin tanımladığı şeydir.

C dilinde, her işlenenin bir türü vardır. "abc" "const char dizisi" türündedir. 1, "int" türündedir. 1L "long" türündedir. Eğer x ve y ifadeler ise, o zaman x + y tipi vb. İçin kurallar vardır. Bu yüzden derleyici açıkça dilin kurallarına uymak zorunda.

Swift gibi modern dillerde, kurallar çok daha karmaşık. Bazı durumlar, C'deki gibi basittir. Diğer durumlarda, derleyiciye bir ifade görür, önceden ifadenin ne tür olması gerektiği söylenir ve buna dayalı alt ifade türlerini belirler. X ve y, farklı türlerdeki değişkenler ise ve aynı ifade atanmışsa, bu ifade farklı bir şekilde değerlendirilebilir. Örneğin, 12 * (2/3) atamak, bir Double'ye 8.0 ve bir Int'e 0 atayacaktır. Ve derleyicinin iki türün ilişkili olduğunu bildiği ve hangi türden buna dayandığını çözdüğünüz durumlar var.

Hızlı örnek:

var x: Double
var y: Int

x = 12 * (2 / 3)
y = 12 * (2 / 3)

print (x, y)

"8.0, 0" yazdırır.

Atamada x = 12 * (2/3): Sol tarafın bilinen bir Double tipi vardır, bu nedenle sağ tarafın Double tipi olması gerekir. "*" İşleci için Double döndüren yalnızca bir aşırı yük var ve bu da Double * Double -> Double. Bu nedenle 12, Double tipinin yanı sıra 2/3 tipinde olmalıdır. 12 "IntegerLiteralConvertible" protokolünü destekler. Double, "IntegerLiteralConvertible" türünde bir argüman alan bir başlatıcıya sahiptir, bu nedenle 12, Double'ye dönüştürülür. 2/3, Double türünde olmalıdır. "/" Operatörü için Double döndüren yalnızca bir aşırı yük var ve bu Double / Double -> Double. 2 ve 3, Çift'e dönüştürülür. 2/3 sonucu 0.6666666'dır. 12 * (2/3) sonucu 8.0. 8.0 x'e atanmıştır.

Atama y = 12 * (2/3), sol taraftaki y, Int türüne sahiptir, bu nedenle sağ tarafın Int türüne sahip olması gerekir, bu nedenle 12, 2, 3, Int ile dönüştürülür, sonuç 2/3 = 0, 12 * (2/3) = 0.

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.