Ders 17 / 18
Go Komut Ailesi
Dilin kendi komut ailesi, yazılmayan iki kuralı program içinden çağrılarak uyguluyor. Bir kaynak biçimlendiriciden geçirildiğinde bayt bayt karşılaştırılıyor ve 18 satırlık kaynağın 7 satırının değiştiği sayılıyor; inceleyici ise derleyicinin reddetmediği bir kalıbı (bir yazdırma çağrısının biçim damgasıyla verilen değerin tipinin uyuşmaması) yakalıyor. Sınırlayıcı ölçüm, biçimlendirilmiş ve biçimlendirilmemiş iki kaynağın aynı çalıştırıldığında aynı çıktıyı verdiğini gösterir — biçim değişiyor, anlam değişmiyor.
İçindekiler
Önceki derste bir modülün sürümünü kimin seçtiği ölçüldü: seçimi isteklerin kendisi belirliyordu, ayrı bir karar mekanizması yoktu. Bu derste soru bir düzey daha somutlaşıyor: bir kaynağın biçimi ve uyduğu kalıp için yazılmayan kuralları kim uyguluyor? Yanıt bu kez bir kişi ya da bir sözleşme belgesi değil — dilin kendi dağıtımıyla gelen komut ailesi. Kursun ilk dersinden beri derleyici bu ailenin bir üyesi olarak program içinden çağrılıyordu; bu ders aynı aileden iki üyeyi daha, biçimlendirmeyi ve incelemeyi, aynı yöntemle çağırıyor.
Kursun ilk üç konusu boyunca bu komut ailesinden yalnız bir üye çağrıldı: derleyici. Onun işi dardı — kaynağın çalıştırılabilir bir programa dönüşüp dönüşmeyeceğine karar vermek. Ama bir kaynak metnin çevresinde derlenip derlenmemesinin ötesinde iki soru daha duruyor: bu kaynak nasıl görünüyor, ve bu kaynak hangi kalıplara uyuyor? Derleyici ikisine de kayıtsız — bir kaynağın girintisi, boşlukları, satır sonları neresi olursa olsun aynı programı üretiyor, ve derleyicinin sözdizim kuralları bir kalıbın “iyi” ya da “kötü” olduğuna hiç karar vermiyor, yalnız kalıbın geçerli olup olmadığına bakıyor. Bu iki sorunun yanıtı bu yüzden derleyicinin dışında, aynı komut ailesinin iki ayrı üyesinde duruyor.
Bir Kaynağı Biçimlendiriciden Geçirmek
PK12. Bir kaynağın biçimi — girinti, boşluk, hizalama — dilin sözdizim kuralları tarafından zorunlu kılınmaz; aynı anlama gelen bir kaynak birçok farklı biçimde yazılabilir. PK13. Buna rağmen biçimlendirici, kendisine verilen herhangi bir geçerli kaynağı, tek bir kanonik biçime dönüştürür ve bu dönüşüm kaynağın anlamını hiç değiştirmez.
Aşağıdaki yordamlar, önceki derslerde kurulan derleyici çağırma deseninin aynısını
kullanıyor: kaynak geçici bir modüle yazılıyor, komut çağrılıyor, sonuç okunuyor. Fark
şurada — bu kez çağrılan komut kaynağı derlemiyor, kaynağın kendisini değiştiriyor.
Bu yüzden ölçüm de farklı bir şey okuyor: önceki derslerde tek bir soru vardı
(derlendi mi, derlenmedi mi); burada iki kaynak — biçimlendirmeden önceki ve sonraki —
yan yana tutulup karşılaştırılıyor. Her ölçüm yine kendi tamamen boş geçici modülünde
çalışıyor ve GOTOOLCHAIN=local ortam değişkeniyle çağrılıyor, tıpkı önceki iki derste
olduğu gibi — çağrılan komut ne bir ağ isteği göndersin ne de önceki bir denemenin
bıraktığı bir dosyayı görsün diye.
// araclar/araclar.go — go komut ailesini program icinden cagiran ortak yordamlar
package araclar
import (
"os"
"os/exec"
"path/filepath"
"strings"
)
func yeniModul(dizin string) {
os.MkdirAll(dizin, 0o755)
os.WriteFile(filepath.Join(dizin, "go.mod"), []byte("module sinama\n\ngo 1.24\n"), 0o644)
}
func ortam() []string {
return append(os.Environ(), "GOTOOLCHAIN=local")
}
// Bicimlendir kaynagi gecici bir modulde bicimlendiricinin uzerinden gecirir; bicimlendirme
// oncesi ve sonrasi kaynagi, ayrica kac satirin degistigini dondurur.
func Bicimlendir(kaynak string) (oncesi, sonrasi string, degisenSatir int) {
dizin, _ := os.MkdirTemp("", "sinama")
defer os.RemoveAll(dizin)
yeniModul(dizin)
yol := filepath.Join(dizin, "main.go")
os.WriteFile(yol, []byte(kaynak), 0o644)
cmd := exec.Command("go", "fmt", "./...")
cmd.Dir = dizin
cmd.Env = ortam()
cmd.Run()
bayt, _ := os.ReadFile(yol)
oncesi = kaynak
sonrasi = string(bayt)
oncekiSatirlar := strings.Split(strings.TrimRight(oncesi, "\n"), "\n")
sonrakiSatirlar := strings.Split(strings.TrimRight(sonrasi, "\n"), "\n")
if len(oncekiSatirlar) == len(sonrakiSatirlar) {
for i := range oncekiSatirlar {
if oncekiSatirlar[i] != sonrakiSatirlar[i] {
degisenSatir++
}
}
}
return oncesi, sonrasi, degisenSatir
}
// CalistirVeCikisAl kaynagi gecici bir modulde calistirip standart ciktisini dondurur.
func CalistirVeCikisAl(kaynak string) string {
dizin, _ := os.MkdirTemp("", "sinama")
defer os.RemoveAll(dizin)
yeniModul(dizin)
os.WriteFile(filepath.Join(dizin, "main.go"), []byte(kaynak), 0o644)
cmd := exec.Command("go", "run", ".")
cmd.Dir = dizin
cmd.Env = ortam()
cikti, _ := cmd.Output()
return strings.TrimRight(string(cikti), "\n")
}
// InceleyiciYakaliyorMu kaynagi gecici bir modulde derleyip inceleyiciden gecirir;
// derleyicinin gectigi ama inceleyicinin reddettigi kaliplari yakalamak icin kullanilir.
func InceleyiciYakaliyorMu(kaynak string) (derlendiMi, inceleyiciYakaladiMi bool) {
dizin, _ := os.MkdirTemp("", "sinama")
defer os.RemoveAll(dizin)
yeniModul(dizin)
os.WriteFile(filepath.Join(dizin, "main.go"), []byte(kaynak), 0o644)
derle := exec.Command("go", "build", "-o", os.DevNull, "./...")
derle.Dir = dizin
derle.Env = ortam()
derlendiMi = derle.Run() == nil
incele := exec.Command("go", "vet", "./...")
incele.Dir = dizin
incele.Env = ortam()
inceleyiciYakaladiMi = incele.Run() != nil
return derlendiMi, inceleyiciYakaladiMi
}
Bicimlendir, kaynağı bir dosyaya yazıp biçimlendiriciyi çağırıyor, sonra aynı dosyayı
tekrar okuyor — biçimlendirme kaynağı yerinde değiştirdiği için öncesi ile sonrası iki
ayrı okuma. Satır sayıları eşitse (biçimlendirici satır eklemediyse ya da silmediyse)
karşılık gelen satırları teker teker karşılaştırıp kaçının değiştiğini sayıyor.
InceleyiciYakaliyorMu ise iki ayrı komutu art arda çağırıyor: önce derleyici, sonra
inceleyici — ikisinin de yalnız başarı/başarısızlık durumu okunuyor, hiçbir çıktı metni
saklanmıyor ya da yazdırılmıyor.
Bir Kaynak, Bir Kalıp
Aşağıdaki ölçüm iki ayrı soruyu tek bir koşumda topluyor: aynı düzensiz kaynak biçimlendiriciden geçirildiğinde ne kadarı değişiyor, ve ayrı bir kaynak inceleyiciden geçirildiğinde derleyicinin gözden kaçırdığı kalıp yakalanıyor mu? İki ölçüm birbirinden bağımsız — biri bir kaynağın görünüşünü, öbürü bir başka kaynağın geçerliliğini sınıyor — ama ikisi de aynı yordamla, komutu program içinden çağırıp yalnız sonucu okuyarak yürütülüyor.
// main.go — bicimlendirici kaynagi degistiriyor, inceleyici derleyicinin reddetmedigi bir kalibi yakaliyor
package main
import (
"fmt"
"strings"
"ders/araclar"
)
func main() {
kaynak := "package main\n\n" +
"import \"fmt\"\n\n" +
"type Nokta struct {\n" +
" X int\n" +
" Y int\n" +
" Etiket string\n" +
"}\n\n" +
"func Topla(a int,b int) int {\n" +
" return a+b\n" +
"}\n\n" +
"func main() {\n" +
" n := Nokta{X: 1, Y: 2, Etiket: \"a\"}\n" +
" fmt.Println(n,Topla(2,3))\n" +
"}\n"
oncesi, sonrasi, degisenSatir := araclar.Bicimlendir(kaynak)
toplamSatir := strings.Count(strings.TrimRight(oncesi, "\n"), "\n") + 1
fmt.Println("-- bicimlendirici kaynagi degistiriyor mu --")
fmt.Printf("bayt bayt ayni mi: %v\n", oncesi == sonrasi)
fmt.Printf("toplam satir: %d, degisen satir: %d\n", toplamSatir, degisenSatir)
fmt.Println()
fmt.Println("-- sinirlayici olcum: bicim degisiyor, anlam degismiyor --")
ciktiOncesi := araclar.CalistirVeCikisAl(oncesi)
ciktiSonrasi := araclar.CalistirVeCikisAl(sonrasi)
fmt.Printf("bicimlendirme oncesi kaynagin ciktisi: %s\n", ciktiOncesi)
fmt.Printf("bicimlendirme sonrasi kaynagin ciktisi: %s\n", ciktiSonrasi)
fmt.Printf("iki cikti ayni mi: %v\n", ciktiOncesi == ciktiSonrasi)
fmt.Println()
fmt.Println("-- inceleyici, derleyicinin reddetmedigi bir kalibi yakaliyor --")
kalipliKaynak := "package main\n\n" +
"import \"fmt\"\n\n" +
"func main() {\n" +
"\tfmt.Printf(\"%d\\n\", \"metin\")\n" +
"}\n"
derlendiMi, yakalandiMi := araclar.InceleyiciYakaliyorMu(kalipliKaynak)
fmt.Printf("derlendi mi: %v\n", derlendiMi)
fmt.Printf("inceleyici yakaladi mi: %v\n", yakalandiMi)
}
-- bicimlendirici kaynagi degistiriyor mu --
bayt bayt ayni mi: false
toplam satir: 18, degisen satir: 7
-- sinirlayici olcum: bicim degisiyor, anlam degismiyor --
bicimlendirme oncesi kaynagin ciktisi: {1 2 a} 5
bicimlendirme sonrasi kaynagin ciktisi: {1 2 a} 5
iki cikti ayni mi: true
-- inceleyici, derleyicinin reddetmedigi bir kalibi yakaliyor --
derlendi mi: true
inceleyici yakaladi mi: true
PK14. 18 satırlık kaynağın 7 satırı biçimlendiriciden geçtikten sonra değişiyor; kalan 11 satır zaten kanonik biçimdeydi ve hiç dokunulmuyor. PK15. Değişen yedi satırın altısında girinti karakteri boşluktan sekmeye dönüşüyor; bunlardan ikisinde yapı alanlarının tipleri aynı sütunda hizalansın diye ayrıca boşluk ekleniyor, ikisinde bir işlev çağrısındaki virgülden sonraki boşluk tamamlanıyor, birinde de bir toplama işlecinin iki yanına boşluk giriyor — bu kategoriler bazı satırlarda üst üste biniyor (bir girinti düzeltmesiyle bir hizalama aynı satırda birlikte geçebiliyor), ama hiçbiri yeni bir satır eklemiyor ya da bir satırı siliyor.
Yazılmayan şey burada bir tercih: kaynağı yazan kişi girinti için boşluk mu sekme mi kullanacağını, bir yapının alanlarını hizalayıp hizalamayacağını, virgülden sonra boşluk bırakıp bırakmayacağını yazmadı. Biçimlendirici bu boşlukları kendi kanonik kararıyla dolduruyor — ve bu, kursun sıfır değer bahsindeki “tanımlı” sınıfının bir başka görünümü: yazılmayan bir şey, derleyicinin reddiyle değil, ayrı bir aracın kendi kuralıyla dolduruluyor.
Bu kararın tek bir kanonik biçimde durması, birden çok kişinin aynı kaynak üzerinde çalıştığı durumlarda ayrı bir sonuç doğuruyor. Girinti ve hizalama tercihe bağlı kaldığında, aynı anlama gelen iki kaynak yalnız görünüş farkıyla birbirinden ayrışabiliyor — ve bu fark, kaynağın önceki hâliyle karşılaştırıldığında (satır satır ne değiştiğine bakıldığında) gürültü olarak görünüyor: hangi satırın anlamca değiştiği, hangisinin yalnız biçimce değiştiği birbirine karışıyor. Biçimlendiricinin kanonik kararı bu gürültüyü kaynağında kesiyor — yazan kişi hangi girintiyi seçerse seçsin, biçimlendiriciden geçtikten sonra kaynak her zaman aynı biçime dönüşüyor, ve bu yüzden iki kaynak arasındaki gerçek fark yalnız biçimlendirilmiş iki hâlin karşılaştırılmasıyla görülebiliyor.
İnceleyici, Derleyicinin Geçtiği Bir Kalıbı Yakalıyor
PK16. fmt.Printf("%d\n", "metin") çağrısı derleniyor — Printf’in değişken sayıda
argümanı interface{} tipinde kabul ettiği için, bir dizginin bir %d damgasının karşılığı
olarak geçirilmesi derleyicinin gözünde sözdizimsel olarak geçerli. PK17. Aynı kaynak
inceleyiciden geçirildiğinde reddediliyor: %d bir tam sayı beklerken argümanın bir dizgi
olması, biçim damgası ile değerin tipi arasındaki bir uyumsuzluk, ve bu uyumsuzluk
derleyicinin denetlediği bir kural değil.
Bu ayrım kursun “reddedildi” sınıfını genişletiyor. Önceki konularda reddeden taraf hep
derleyiciydi; burada reddeden taraf inceleyici — ayrı bir komut, ayrı bir geçiş, ayrı
bir kural kümesi. Derleyici yalnız kaynağın sanal makinenin çalıştırabileceği bir programa
dönüşüp dönüşemeyeceğine bakıyor; Printf’in biçim dizgisi ile argümanları arasındaki
uyum bu sorunun dışında kalıyor, çünkü biçim dizgisi derleyicinin gözünde sıradan bir
dizgi değeri, özel bir anlamı yok. İnceleyici bu dizgiyi ayrıştırıp her damgayı
karşılık gelen argümanla eşleştiriyor ve eşleşmeyeni yakalıyor — derleyicinin hiç bakmadığı
bir katmanda çalışıyor.
Bu yakalamanın değeri, hatanın nerede ortaya çıkacağını değiştirmesinde. %d yerine bir
dizgi geçirilmiş bir çağrı derleniyor ve çalışıyor — ama çalıştığında beklenen sayı yerine
biçim damgasının kendisini ve bir hata işareti basıyor, ve bu yalnız o çağrı satırı
gerçekten çalıştırıldığında görünüyor. Bir kaynak tabanında böyle bir çağrı seyrek
çalışan bir dala düşmüşse, kusur uzun süre fark edilmeyebilir; derleme hiçbir uyarı
vermediği için de kaynağı okuyan biri bunu kaynağın kendisinden çıkaramaz. İnceleyici bu
çağrıyı hiç çalıştırmadan, yalnız kaynağı okuyarak yakalıyor — bu, kursun “açıkta”
sınıfına düşecek bir kusuru, çalışma zamanına hiç bırakmadan “reddedildi” sınıfına
taşıyan bir araç. Derleyicinin kendisi bu taşımayı yapmıyor, çünkü derleyicinin görevi
sözdizimini ve tip uyumunu denetlemekle sınırlı; biçim dizgisinin içeriği derleyicinin
tip sisteminin dışında kalan, yalnız çalışma zamanında yorumlanan bir dizgi değeri.
Sınırlayıcı Ölçüm: Biçim Değişiyor, Anlam Değişmiyor
PK18. Aynı kaynağın biçimlendirme öncesi ve sonrası hâli, ikisi de ayrı ayrı
çalıştırıldığında birebir aynı çıktıyı veriyor: {1 2 a} 5. Bayt bayt karşılaştırma
false derken, çalıştırma çıktısı karşılaştırması true diyor — bu iki sonuç birbirini
çürütmüyor, tam tersine dersin tezini kuruyor: biçimlendirici kaynağın görünüşünü
değiştiriyor, davranışını değiştirmiyor.
Bunun nedeni biçimlendiricinin ne yaptığında yatıyor: girinti, boşluk ve hizalama —
biçimlendiricinin dokunduğu her şey — dilin sözdiziminde anlamsız boşluk. Bir int
değişkeninin adı, bir işlevin gövdesindeki ifadelerin sırası, bir yapının alan adları —
bunların hiçbiri biçimlendirme sırasında değişmiyor, çünkü biçimlendirici bunlara hiç
dokunmuyor. Sınır burada net: biçimlendirici bir ayrıştırma ağacını okuyup aynı ağacı
kanonik boşluklarla yeniden yazıyor, ağacın kendisini hiç değiştirmiyor. Bu yüzden
biçimlendirilmiş bir kaynağın davranışı hakkında hiçbir yeni varsayım gerekmiyor — ölçülen
programın çalıştırıldığında ne yaptığı, biçimlendirmeden önce ne yapıyorsa biçimlendirmeden
sonra da onu yapıyor.
Bu sınır, inceleyicinin sınırından farklı bir yerde duruyor. Biçimlendirici kaynağı hiç reddetmiyor — kendisine verilen her geçerli kaynağı kabul edip yeniden yazıyor, ve girdinin “iyi” ya da “kötü” olduğuna hiç karar vermiyor. İnceleyici ise tam tersini yapıyor: kaynağı hiç değiştirmiyor, yalnız reddediyor ya da geçiriyor. İki araç da aynı komut ailesinin üyesi, ikisi de derleyicinin bakmadığı bir katmanda çalışıyor, ama biri kaynağı dönüştürerek, öbürü kaynağı yargılayarak iş görüyor. Bu ders ikisini de aynı yordamla — kaynağı geçici bir modüle yazıp komutu çağırarak — sınadı, ve ikisinin de sonucu yalnız bir bayrak (değişti mi, geçti mi) olarak okundu; hiçbir aşamada bir hata metni ya da bir biçimlendirilmiş diff satır satır yazdırılmadı.
Özet
- Dilin kendi komut ailesi yalnız derlemeyi değil biçimlendirmeyi ve incelemeyi de kapsar; üçü de bu derste olduğu gibi program içinden, argüman olarak çağrılabilir.
- Biçimlendirici, kendisine verilen kaynağı tek bir kanonik biçime dönüştürür; 18 satırlık bir kaynağın 7 satırı bu dönüşümde değişir, geri kalanı zaten kanonik biçimdeydi.
- İnceleyici, derleyicinin reddetmediği bir kalıbı (biçim damgası ile argüman tipinin uyuşmaması) yakalar; derleyici bu uyumu hiç denetlemez, çünkü biçim dizgisi onun gözünde sıradan bir dizgi değeridir.
- Sınırlayıcı ölçüm: biçimlendirme öncesi ve sonrası kaynak, çalıştırıldığında birebir aynı çıktıyı verir — biçim değişir, anlam değişmez.
- İki aracın adı da yalnız kod bloğunda, bir çağrının argümanı olarak geçti; düzyazıda yalnız iş adlarıyla anıldı.
Sonraki Adım
Bu derste bir kaynağın biçimi ve derleyicinin gözden kaçırdığı bir kalıbı kimin uyguladığı ölçüldü — ikisi de dilin kendi komut ailesinin ayrı üyeleri, ikisi de derleyicinin bakmadığı bir katmanda çalışıyor. Ama bu üç aracın (derleyici, biçimlendirici, inceleyici) hiçbiri, bir paketin dışarıya ne sunduğunu anlatan metni — bir adın ne işe yaradığını açıklayan yazıyı — hiç okumuyor. Kursun son dersi bu son köşeye bakıyor: bir belge kaynaktan nasıl üretiliyor, hangi adlar bu belgeye hiç girmiyor, ve belge aracı bir yorumun doğruluğunu denetliyor mu — yoksa yalnız konumunu mu?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.