1 puan yazan GN⁺ 2024-10-07 | 1 yorum | WhatsApp'ta paylaş
  • 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

 
GN⁺ 2024-10-07
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.

  • 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.

    • O sunum da bu tekniğin daha genel olarak quine'larda kullanıldığına işaret eder.
      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)
    • 2009'da yayımlanmış bu kısa ve harika web yazısı da okunmaya değer.
      https://www.teamten.com/lawrence/writings/coding-machines/
    • Sanırım o değil.
      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, \n iç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ı.

    • EBCDIC'te satır sonu/sonraki satır karakteri NEL gerçekten var ama birçok EBCDIC sisteminde sık karşılaşılan bir şey değil.
      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 \n karakterini 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.cpp içinde açıkça 10 olarak hardcode edilmiş
    ProcessCharEscape standart C kaçış dizilerini ayrıştırıyor ve case 'n': ResultChar = 10; break; gibi işliyor

    • GCC de benzer şekilde gcc/libcpp/charset.cc içinde hardcode edilmiş ve ASCII ya da EBCDIC’ten birini seçiyor
      \a \b \e \f \n \r \t \v değerlerini charconsts dizisine 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ından case 'n': c = charconsts[4]; break; ile işliyor
  • Bir 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 bilmiyorum
    Bana apaçık bir şey gibi geliyor; yazar ya da yorumlar da açıklamadığı için kendimi aptal gibi hissediyorum

    • Esas mesele, o baytı değer 10 olarak kodlayan “kim” sorusu
      Bir ayrıştırıcı yazarken satır sonunu kaçış dizisi \n olarak ayrıştırıyorsan, 10 değeri nereden geldi?
      Satır sonunu tamsayı literali 10 olarak ayrıştırıyorsan, gerçek ikili değer 1010 nereden 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
    • İlginç olan nokta, 10 değerinin Rust kaynak kodu içinde tanımlı olmaması; derleyiciden derleyiciye sözlü gelenek gibi aktarılması
    • Rust derleyicisini sıfırdan yeniden derlemen gerekiyorsa ve elinde yalnızca rustc kaynak kodu varsa, kaynak kodda '\n'nin gerçekte neye eşlendiğini söyleyen hiçbir bilgi yok
      Ken Thompson hack’inin ilginç bir gerçek dünya örneği
    • Esas mesele, neden özellikle 10 olduğu
      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 \h olarak 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' => 10 eşlemesini elle programlamış olmalı; soru bunun nerede olduğu
  • C yüzünden sanırım \0???’yi hep sekizlik kaçış olarak düşünmüştüm
    Bu yüzden kafamda \012, \x0a ya da 0x0a; \010 ise 0x08
    Bu yüzden bu yazı epey kafa karıştırıcı
    Belki de OCaml’da sekizlik kaçış değil, onluk kaçış vardır ve \09 tab karakteridir
    Kontrol etmedim

    • O yönde kısmen doğru yanlar var, ama ters eğik çizgi kaçışlarıyla ilgili değil
      Ters eğik çizgi kaçışları simgesel/hatırlatıcıdır; \n “[Ne]wline”, \r “carriage [R]eturn”, \t “[T]ab” gibi
      Bunun yerine ^C (interrupt), ^G (bell), ^M (carriage return) gibi kontrol karakteri geleneklerine bakmak gerekir
      Bunlar C0 kontrol karakterleri kümesindedir; ^C \0x3, ^G \0x7, ^M ise \0xD’dir
      Unix ö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şlenir
      Bu yüzden \0 tuhaf biçimde ^@ olarak gösterilir, Esc tuşu ^[ olur ve ezberlemesi zor eşdeğer gösterimler ortaya çıkar
      Bu Unix yazarlarının tercihi değil, ASCII numaralandırma sisteminin bir ürünüdür
    • OCaml gerçekten onluk kaçış kullanır: https://ocaml.org/manual/5.2/lex.html#char-literal
    • Ters eğik çizgi-onluk karakter kaçışları gerçekten nadir
      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 \N var sandım
    Satı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ş

    • Kaynağı görüntüleyince aslında \n, ama şu CSS kuralı yüzünden öyle görünmüyor
      .title { font-variant: small-caps; }
    • Gerçekten \N kullanan yerler var
      Pek çok sistem, CSV ya da benzer biçimlerde boş string’den ayırmak için \N’yi NULL olarak kullanır
      Bu yüzden ben de yazının bu konuyla ilgili olduğunu sanmıştım
    • Python’da \N kaçış dizisi var
      Unicode karakterini adıyla ekler
      Örneğin '\N{PILE OF POO}', kaka emojisi olan tek bir Unicode string’idir
      \u ya da \U ile onaltılık dizi yazmaktan çok daha kendini açıklayıcıdır
    • Ben de bu yüzden yazıya tıklamıştım
      Yine de keyifle okudum
  • Bu yazıya ilham veren “diğer yazı” muhtemelen şu olmalı
    https://research.swtch.com/nih