just foo, justfile içindeki "\n" ifadesini işleyerek bar dosyasına tek bayt 0x0A yazar; yazı bu değerin nereden geldiğini adım adım izliyor
justın Rust ayrıştırıcısı, \n kaçış dizisiyle karşılaştığında Rust karakter kaçışı '\n' değerini string’e ekleyecek şekilde uygulanmış
- Güncel
rustc de Rust ile yazıldığı için iz sürme yeniden rustc lexer’ına uzanıyor; ancak self-hosted döneminden önceki OCaml uygulamasında daha doğrudan bir ipucu bulunabiliyor
- İlk OCaml sürümü
rustc, karakter kaçışı ni Char.code '\n' olarak işliyordu; OCaml lexer’ı bunu '\010' olarak tanımlıyor
0x0A değeri 10 olduğundan, justfile içindeki \n, Rust derleyicisi nesilleri boyunca aktarılan bir değer; başlangıç noktası ise OCaml derleyicisinin '\010'ı değerlendirip ilk rustc ikilisine koyduğu bayta kadar uzanıyor
justfile içindeki \nin 0x0A olmasına giden yol
just foo çalıştırıldığında aşağıdaki justfile, bar dosyasına tek bayt 0x0A yazar
x := "\n"
foo:
printf '{{x}}' > bar
just Rust ile yazılmıştır ve ayrıştırıcıdaki cook_string fonksiyonu, kaçış dizileri içeren just string token’larını UTF-8 string’lere dönüştürür
- Ters eğik çizgiden sonra
n geldiğinde bu fonksiyon cooked.push('\n') çalıştırır
State::Backslash => {
match c {
'n' => cooked.push('\n'),
…
}
}
- Bu aşamada
just, Rust karakter kaçışı '\n' değerlendirmesinin sonucunu string’e koyma işini rustcye bırakır
rustc ve OCaml’a kadar geriye giden yol
rustcnin kaçış işleme mantığı lexer’daki scan_escape fonksiyonundadır; n ile karşılaştığında bunu yine Rust karakter kaçışı '\n' olarak işler
let res: char = match chars.next().ok_or(EscapeError::LoneSlash)? {
…
'n' => '\n',
…
};
- Güncel
rustc Rust ile yazılır ve kendisini derler; bu nedenle '\n'in anlamını arama süreci rustcden yeniden rustcye gider
- Ancak
rustc en başından beri Rust ile yazılmış değildi; self-hosted öncesindeki ilk sürümler OCaml ile yazılmıştı
- OCaml sürümü
rustcnin lexer’ı, karakter kaçışı ni şöyle işliyordu
| 'n' { end_char (Char.code '\n') lexbuf }
- Burada da OCaml karakter kaçışı
'\n' kullanılıyor; ancak OCaml lexer’ında daha doğrudan bir tanım var
let char_for_backslash = function
'n' -> '\010'
- OCaml derleyicisi
\n gördüğünde 10’luk karakter kaçışı '\010' değerlendirmesinin sonucunu koyar; 0x0A da 10 olduğundan aranan bayt değeriyle eşleşir
- Dolayısıyla
justfile içindeki \n, just ikilisi içindeki bir tür 0x0A baytına uzanır; bu baytı rustc koymuştur ve önceki rustclerin nesiller boyunca aynı değeri aktardığı bir akış olarak görülebilir
- Güncel
rustc 1.81.0’dır; yalnızca rustc 1.0 sonrasına bakılsa bile bu süreç en az 81 kez gerçekleşmiştir, 1.0 öncesi de dahil edilirse muhtemelen daha fazladır
- İz sürmenin başlangıç noktası, OCaml derleyicisinin 10’luk karakter kaçışı
'\010' ifadesini değerlendirip ilk rustc ikilisine 0x0A baytını koyduğu noktadır
1 yorum
Hacker News yorumları
Bu fikri ilk okuduğum yer genel anlamda trusting trust değil, satır sonu karakteri hakkındaydı; https://www.sigbus.info/how-i-wrote-a-self-hosting-c-compile... yazısının 42. günüydü.
Bir string literal içindeki
"\n"ifadesini gerçek satır sonu karakteri olarak yorumlamak için kaynak kodda o ASCII kod bilgisi yok; bunun derleyiciyi derleyen önceki derleyiciden aktarılıyor olması ilginç.Sonuçta o derleyicinin satır sonu karakteri, kendisini derleyen GCC'ye kadar geriye izlenebilir.
'\n'değerini kendi derleyicisine bırakmasını bekliyordum ama gerçekte escape'lerin sayısal değerlerini hardcode etmişti[1] ve sanki yalnızca ASCII ile EBCDIC sistemleri için seçenekler bırakmıştı.[1] https://github.com/gcc-mirror/gcc/blob/8a4a967a77cb937a2df45...
Yazarın aklına gelen özgün metnin Ken Thompson'ın Turing Ödülü konuşması Reflections on Trusting Trust olması muhtemel görünüyor.
Quine'lar üzerine epey araştırma, makale ve açıklama var; yazar da bu türden bir yazı okumuş olabilir.
https://en.wikipedia.org/wiki/Quine_(computing)
https://www.teamten.com/lawrence/writings/coding-machines/
Ben de birkaç yıl önce Rust'ın
'\n'karakteriyle ilgili tam olarak aynı türden bir genel kültür yazısı gördüğümü hatırlıyorum ama artık kaynağını bulamıyorum.Aradan 10 saat geçmesine rağmen EBCDIC'ten bahseden bir thread olmaması ilginç.
İlk C derleyicileri,
\niçin “satır besleme (line feed)” karakterini onluk 10'a eşlemeyen ASCII dışı sistemlerde de vardı; dolayısıyla burada ortaya atılan tüm teorilerin bu gerçeği açıklaması gerekiyor.https://en.wikipedia.org/wiki/EBCDIC
Üstelik EBCDIC'te hem açık bir NextLine karakteri hem de LineFeed karakteri vardı.
ASCII'de
for (c = 'A'; c <= 'Z'; ++c) putchar(c);A'dan Z'ye kadar yazar; EBCDIC'te ise karakterlerin arasında boş aralıklar olduğu için atanmamış karakterler de dahil 41 karakter yazdırır.EBCDIC sıralamasında küçük harfler büyük harflerden, harfler de rakamlardan önce gelir; yani ASCII'nin tam tersidir.
C standardının karakter kodlaması hakkında garanti ettiği tek şey,
'0'ile'9'arasındaki rakamların ardışık ve artan sırada eşlenmesiydi.Teoride basit bir C programının ASCII'de de EBCDIC'te de aynı kaynakla derlenip aynı çıktıyı üretmesi gerekiyordu; pratikte ise çok sayıda tuzak vardı.
Erken dönem EBCDIC sistemleri (MVS, VM/CMS, OS/400, DOS/VSE vb.) metni bayt akışı dosyaları olarak değil, kayıt odaklı dosyalar olarak saklıyordu; her satır sabit uzunluklu ya da değişken uzunluklu bir kayıttı.
Sabit uzunluklu kayıtlarda dosya oluşturulurken 80 veya 132 gibi bir kayıt uzunluğu bildirilir, kısa satırlar genellikle EBCDIC boşluk karakteri 0x40 ile doldurulur, uzun satırlar ise kesilir ya da devam karakteri kullanırdı.
Değişken uzunluklu kayıtların başına uzunluğu içeren bir record descriptor word (RDW) eklenirdi; ancak metin dosyaları ve kaynak kod için bu nadirdi, sabit uzunluklu kayıtlar yaygındı.
Bu yüzden NEL var olsa da disk dosyalarında genellikle kullanılmazdı.
NEL gibi satır sonu karakterleri satır/kayıt sınırını gösteren bant içi sinyallerdir; kayıt odaklı dosya sistemleri ise bu sınırı bant dışında ifade ediyordu.
EBCDIC C derleyicisinin çalışma zamanı kütüphanesinde stdio'nun tam olarak nasıl uygulandığını bilmiyorum; ama içeride
\nkarakterini NEL'e eşleyip stdio katmanının bunu kayıt ayırıcı olarak ele aldığını, her kaydı ayrı bir sistem çağrısıyla yazarken gerektiğinde padding yaptığını tahmin ediyorum.Daha sonra bu işletim sistemlerinin çoğu POSIX uyumlu alt sistemler kazanarak ana akım sistemlerdeki gibi bayt akışı dosyalarına da sahip oldu.
IBM sistemleri genellikle dosyalara kod sayfası etiketi takma özelliğini destekler; böylece dosyalar EBCDIC ve ASCII'yi karışık barındırabilir ve işletim sistemi giriş/çıkış katmanında dönüşüm yapar.
Bu sayede çalışma zamanında EBCDIC kullanan uygulamalar bile ASCII dosyalarını ayrı bir dönüşüm API çağrısı ya da açık bir belirtim olmadan EBCDIC'miş gibi okuyabilir.
Yeni uygulamalar giderek daha fazla POSIX tabanlı dosya sistemlerini kullanıyor; eski uygulamalar ise verileri, metin dosyalarını ve hatta kaynak kodu hâlâ çoğu zaman klasik kayıt odaklı dosya sistemlerinde saklıyor.
Gerçek ortamlarda EBCDIC NEL'i en sık görebileceğiniz yerin IBM 2741, IBM 3767 gibi hardcopy terminallerinin satır modu terminal bağlantıları olduğunu anlıyorum.
Gerçekten ilginç bir yazı.
Bana edebi programlama ile şiirin karışımı gibi geliyor.
just fooçalıştırıldığında çıkan o 0x0A baytının belki de yüzlerce kod üretim döngüsünden geçerek geldiği fikrini anlatmaya çalışıyor.Uzun zaman önce birileri bu bilgiyi bir şekilde OCaml derleyicisine kodladı; yıllar sonra benim bilgisayarımdaki 0x0A bilgisinin bu tarih nedeniyle saklı olduğu söylenebilir.
Ama bu olgu gerçek kodla açıklanıyor.
Elbette kodun kendisi asıl mesele değil; kimsenin bu belirli kodu çalıştıracak ya da derleyecek gibi olduğunu da sanmıyorum, ama insanın tartışmayı takip edebilmesi için konmuş bir kod.
clang’in de aynı özelliğe sahip olup olmadığını merak etmiştim;
lib/Lex/LiteralSupport.cppiçinde açıkça 10 olarak hardcode edilmişProcessCharEscapestandart C kaçış dizilerini ayrıştırıyor vecase 'n': ResultChar = 10; break;gibi işliyorgcc/libcpp/charset.cciçinde hardcode edilmiş ve ASCII ya da EBCDIC’ten birini seçiyor\a \b \e \f \n \r \t \vdeğerlerinicharconstsdizisine koyuyor; ASCII ise{ 7, 8, 27, 12, 10, 13, 9, 11 }, EBCDIC ise{ 47, 22, 39, 12, 21, 13, 5, 11 }kullanıyor ve ardındancase 'n': c = charconsts[4]; break;ile işliyorBir C derleyicisiyle ilgili benzer bir yazıyı hatırlıyorum
Sonunda 0x10 değerinin göründüğü tek yerin derleyici ikilisi olduğu, kaynak kodda ise yalnızca
"\\n" -> "\n"gibi biçimlerde yer aldığı ortaya çıkmıştıBu benim seviyemi aşan bir konu
\n’nin neden değer 10 olan bir bayt olarak kodlandığını anlamak için niye bu kadar uzun bir yolculuk gerektiğini bilmiyorumBana apaçık bir şey gibi geliyor; yazar ya da yorumlar da açıklamadığı için kendimi aptal gibi hissediyorum
Bir ayrıştırıcı yazarken satır sonunu kaçış dizisi
\nolarak ayrıştırıyorsan, 10 değeri nereden geldi?Satır sonunu tamsayı literali
10olarak ayrıştırıyorsan, gerçek ikili değer1010nereden geldi?Bu düşünce deneyinin nihai amacı, ünlü Reflections On Trusting Trust sunumunda olduğu gibi derleyiciye bakışını değiştirmek
Yani derleyici yalnızca program çıktısı üreten bir şey değil, aynı zamanda programın girdisidir
Derleyicinin kendisi de bir program olduğuna göre, o derleyiciyi üreten derleyici mevcut derleyicinin girdisiydi ve geçişli olarak benim programımın girdisi haline gelir
Bu da derleyicinin derleyicisinin derleyicisine, onun üstündeki derleyiciye doğru böyle devam eder
'\n'nin gerçekte neye eşlendiğini söyleyen hiçbir bilgi yokKen Thompson hack’inin ilginç bir gerçek dünya örneği
Neden 9 ya da 11 değil?
Kod, “satır sonu karakteri dizisini görürsen satır sonu karakteri üret” diyor
Peki derleyici satır sonu karakterinin ne olduğunu nasıl biliyor?
O derleyicinin kodu da yine sadece “satır sonu karakteri dizisini görürsen satır sonu karakteri olarak ele al” diyor
İnsan “C string escape code” diye aratabilir, ama o tablo derleyicinin içinde hiçbir yerde yok
C 2025, Start of Heading’i
\holarak tanımlarsa,'h' => cooked.push('\h')sihirli biçimde çalışmaya mı başlayacak?Bunu nasıl bilebiliriz ki?
Belli ki bir noktada birileri
'n' => 10eşlemesini elle programlamış olmalı; soru bunun nerede olduğuC yüzünden sanırım
\0???’yi hep sekizlik kaçış olarak düşünmüştümBu yüzden kafamda
\012,\x0aya da0x0a;\010ise0x08Bu yüzden bu yazı epey kafa karıştırıcı
Belki de OCaml’da sekizlik kaçış değil, onluk kaçış vardır ve
\09tab karakteridirKontrol etmedim
Ters eğik çizgi kaçışları simgesel/hatırlatıcıdır;
\n“[Ne]wline”,\r“carriage [R]eturn”,\t“[T]ab” gibiBunun yerine
^C(interrupt),^G(bell),^M(carriage return) gibi kontrol karakteri geleneklerine bakmak gerekirBunlar C0 kontrol karakterleri kümesindedir;
^C\0x3,^G\0x7,^Mise\0xD’dirUnix öncesine uzanan zekice bir yöntemle, terminal görünmeyen ASCII C0 karakterlerini ifade etmek için başlarına
^karakterini koyar ve ilgili karaktere AND-0x40 uygulayarak onu görünür aralığa taşıyıp yazdırırdıTakip etmek için https://www.asciitable.com gibi bir ASCII tablosu açmak iyi olur
Her kontrol karakteri, o tabloda iki hücre yanındaki
^karaktere eşlenirBu yüzden
\0tuhaf biçimde^@olarak gösterilir, Esc tuşu^[olur ve ezberlemesi zor eşdeğer gösterimler ortaya çıkarBu Unix yazarlarının tercihi değil, ASCII numaralandırma sisteminin bir ürünüdür
Bildiğim string sözdizimleri arasında aşağı yukarı sadece OCaml, Lua, DNS var
Yanlış büyük harf görünümü yüzünden,
\n’den farklı ve neredeyse hiç bilinmeyen bir kaçış dizisi olan\Nvar sandımSatır sonu olmayan herhangi bir karakterle mi eşleşiyor diye düşündüm; öyle değilmiş, kaynak yazıdaki küçük büyük harf gösteriminden kaynaklanıyormuş
\n, ama şu CSS kuralı yüzünden öyle görünmüyor.title { font-variant: small-caps; }\Nkullanan yerler varPek çok sistem, CSV ya da benzer biçimlerde boş string’den ayırmak için
\N’yi NULL olarak kullanırBu yüzden ben de yazının bu konuyla ilgili olduğunu sanmıştım
\Nkaçış dizisi varUnicode karakterini adıyla ekler
Örneğin
'\N{PILE OF POO}', kaka emojisi olan tek bir Unicode string’idir\uya da\Uile onaltılık dizi yazmaktan çok daha kendini açıklayıcıdırYine de keyifle okudum
Bu yazıya ilham veren “diğer yazı” muhtemelen şu olmalı
https://research.swtch.com/nih
Running the "Reflections on Trusting Trust" Compiler - https://news.ycombinator.com/item?id=38020792 - Ekim 2023, 67 yorum