1 puan yazan GN⁺ 3 시간 전 | 1 yorum | WhatsApp'ta paylaş
  • Jam, C ailesi dillerin anında kullanılabilirlik hissini korurken GC olmadan güvenlik, düşük öğrenme eğrisi ve yüksek performansı birlikte hedefleyen, v1.0 öncesi aşamadaki bir dil
  • Temelinde mutable value semantics ve Rust tarzı drop sistemi var; derleyici, kullanıcı koduna reference veya lifetime sözdizimi açmadan ownership, borrow ve otomatik temizliği yönetiyor
  • İlklendirme modeli hem undefinedı hem de örtük zero initialization’ı önlüyor; gecikmeli ilklendirme ve out-parameter’lar Maybe(T) ve unsafeAssumeInit() analiziyle ele alınıyor
  • export, Jam fonksiyonlarını C ABI olarak dışa açıyor; Jam struct’ları da C uyumlu layout’a sahip olacak şekilde tasarlanıyor, böylece ayrı unsafe shim veya repr annotation yükü azaltılmak isteniyor
  • Derleyici henüz C++ ile uygulanmış bootstrap aşamasında ve herkese açık değil; Jam ile 108 ayrı proje yaptıktan sonra açık kaynak yapılması planlanıyor

Jam’in hedeflediği dil konumu

  • Jam hâlâ v1.0 öncesinde; şu an açıklanan mekanizmalar derleyicide çalışıyor olsa da kararlı hale gelmeden önce ayrıntılar değişebilir
  • Amaç; Go, Zig ve modern C gibi hemen anlaşılabilen C ailesi hissini korurken C’nin bug sınıflarını azaltan güvenli bir dil oluşturmak
  • Tasarımın iki ana ekseni var
    • Racordon, Abrahams ve diğerleri 2022’nin Mutable value semantics yaklaşımı
    • Rust’ın drop system’ı
  • Gerçek ekiplerde yetkinlik düzeyleri karışık ve daha az deneyimli üyelerin hata yapma olasılığı yüksek olduğundan, dilin code review’dan önce daha fazla hatayı engellemesi gerektiği düşüncesinden yola çıkıyor

Rust, Zig ve C++ ile farkları

  • Rust’ın güvenlik felsefesi güçlü, ancak “Rust’ı bir ölçüde kullanabilmek” ile “Rust ile üretken olmak” arasındaki mesafe büyük olduğu için ekibin öğrenme eğrisi yük haline gelebilir
  • Zig, C-like dillere yakın küçük bir yüzey alanı ve anında oluşan bir mental model sunar; ancak dil düzeyinde güvenli bir dil değildir
    • uninitialized read, manual cleanup, use-after-free önleme dil seviyesinde zorlanmaz
    • Büyük Zig veya C++ production projeleri Valgrind, AddressSanitizer, fuzzing gibi doğrulama araçlarına ciddi ölçüde dayanır
  • Yapay zeka çağında production code’un büyük bölümünün insanlar yerine araçlar tarafından yazıldığı veya taslaklandığı; darboğazın code writing’den code review’a kaydığı varsayılıyor
    • code volume artarken review surface düz kaldığı için compiler’ın daha fazla bug yakalaması gerekiyor

Otomatik drop sistemi

  • Jam’de binding bir değerin sahibidir ve drop-bearing type’a ait bir binding scope dışına çıktığında compiler drop çağrısını sentezler
  • Örnek File type’ı fn drop(self: mut File) bildirir; useFile() içinde yalnızca const f: File = { fd: 7 }; yazılır
    • Açık cleanup, defer veya lifetime sonu işareti yoktur
    • LLVM IR’de ret hemen öncesinde call void @__drop_File(ptr %1) üretilir
  • Mangled name olan __drop_File, birden fazla type’ın drop fonksiyonlarının LLVM level’da çakışmasını engeller
  • self: mut File pointer parameter’a lowering edilir ve call site binding adresini doğrudan geçirir
  • Zig’de aynı cleanup için defer f.deinit() açıkça yazılmalıdır
    • Bu satır kaldırılırsa IR’deki deinit call da kaybolur
    • file descriptor leak, programmer cleanup’ı hatırlamadığında oluşur
  • C++ RAII de scope exit’te destructor’ı otomatik çalıştırır; ancak Jam, Rust’ın daha basit drop modelini benimser
    • C++’taki rule of 0/3/5, virtual destructor, constructor exception, destructor exception, std::exit, std::abort, longjmp, signal gibi karmaşıklıklardan kaçınmayı hedefler
    • Jam, type başına tek bir drop function bulundurur ve bunu tüm scope exit’lerde çalıştırır

İlklendirme ve Maybe(T)

  • Jam’de undefined değer yoktur ve binding değersiz bildirilemez
    • Tüm var ve const gerçek bir initializer gerektirir
    • struct’ta önce field değerleri hesaplanır, ardından struct literal ile oluşturulup binding yapılır
  • Zig, var f: File = undefined; return f.fd; yazımına izin verir ve runtime’da stack garbage okunabilir
    • Debug mode’da misuse görünür olsun diye 0xaa fill uygulanır
    • Release mode’da arbitrary bytes olur
  • Go, garbage read’i engellemek için tüm varları zero-initialize eder; ancak yakında overwrite edilecek field’lara da zero pattern yazma maliyeti vardır
  • Jam hem undefined hem de implicit zero tarafını önler
  • Gecikmeli ilklendirme ve out-parameter için Maybe(T) kullanılır
    • empty() henüz anlamlı contents’e sahip olmayan bir slot oluşturur
    • write() slot’u doldurur
    • unsafeAssumeInit() değeri çıkarır
  • lint pass, slot’un write edilip edilmediğini izler ve analyzer’ın ilklendirildiğini kanıtlayamadığı unsafeAssumeInit() çağrılarını compile error olarak reddeder
    • unsafe prefix’i, human ve AI reviewer’ların grep edebileceği bir anchor olarak kalır

Scope exit, return, break, continue

  • compiler drop scope stack’ini izler ve her lexical block boundary’de yeni bir scope push eder
  • Block bittiğinde veya branch ile çıkılmadan hemen önce ilgili scope’taki binding drop’larını emit eder
    • if, else, match arm, while, for body içindeki binding’ler ilgili block sonunda drop edilir
    • nested block içindeki return, gerçek ret öncesinde active scope’ları innermost-first sırayla drop eder
    • break ve continue, loop body içinde açık scope’ları drop ettikten sonra loop exit’e veya bir sonraki iteration’a geçer
  • nested break örneğinde outer, iteration 0 sonunda drop edilir; iteration 1’in break path’inde ise önce inner, sonra outer drop edilir

Parameter mode ve first-class reference’ın kaldırılması

  • Bir fonksiyon çağrısında binding’in drop edilip edilmeyeceğini parameter mode belirler
  • Varsayılan mode read-only borrow’dur
    • callee değeri okur ve caller’ın binding’i initialized durumda kalır
    • call return sırasında drop gerçekleşmez
  • mut, exclusive read-write borrow’dur
    • caller’ın binding’i call sonrasında da initialized durumda kalır
  • Yalnızca move değeri consume eder
    • callee ownership’i alır ve callee sonunda drop edilir
    • caller’ın binding’i call sonrasında Uninit olur; okunursa compile error’dur
  • call site marker yoktur; f(x) biçimi tüm mode’larda aynıdır
  • Jam’de first-class reference type yoktur
    • borrow bir variable’da saklanamaz, return edilemez veya struct field içinde tutulamaz
    • parameter borrow yalnızca call-frame süresince var olur ve call return ile sona erer
    • lifetime annotation gerekmemesinin nedeni, attach edilecek lifetime olmamasıdır
  • collection API de value-shaped tutulur
    • v[i] = x, v.setAt(i, x) biçimine desugar edilir
    • let y = v[i], element’i value olarak döndüren v.at(i) getter’ıdır
  • call site exclusivity check, argument’ın oluşturduğu borrow set içindeki path overlap’ları denetler
    • swap(p.x, p.y) ayrık sub-path’ler olduğu için OK
    • moveX(p, p.x) ise p ile p.x overlap yaptığı için error’dur

C ABI ve FFI

  • Rust’ın native ABI’si unstable olduğu için distribution boundary aşıldığında yeniden C biçiminde encoding etmek gerekir
    • raw pointer dereference unsafe’tır
    • ownership Box::into_raw ve Box::from_raw ile elle aktarılır
    • struct’ı by value geçirirken #[repr(C)] gibi ayrı annotation gerekir
    • cbindgen ve abi_stable gibi araçlar bu sınırdaki el işini azaltmak için vardır
  • Jam’de first-class reference, lifetime veya niche-packed layout olmadığı için Jam value’nun baştan sona value-shaped olduğu düşünülür
    • Jam struct zaten C-compatible layout’a sahip olacak şekilde tasarlanır
  • export, Jam fonksiyonlarını C calling convention’a sahip plain unmangled name ile dışa açar
    • export fn counterAdd(c: mut Counter, n: i64) i64, C’de int64_t counterAdd(Counter *c, int64_t n); olarak çağrılabilir
    • mut Counter parameter, caller-owned storage’a yönelik Counter * olarak lowering edilir
  • Jam tarafındaki function body ordinary Jam olduğundan drop, init analysis ve call-site exclusivity rule uygulanmaya devam eder
  • C yönüne giriş extern ile C signature bildirerek yapılır
    • extern function C ABI’yi literal olarak izler
    • parameter-mode machinery boundary dışında uygulanmaz
    • C’ye raw pointer ile buffer geçirilir; C’nin pointer ile ne yaptığı Jam tarafından doğrulanmaz
  • Jam’in sunmak istediği kapsam, Jam tarafının safe by default kalması ve bir Jam library’si C ABI ile dışa açılırken ayrı bir unsafe API mirror veya shim layer oluşturmak zorunda kalınmamasıdır

Pattern matching

  • Jam’in match yapısı Pattern Block biçimindedir ve => kullanmaz
    • scrutinee, match (opcode) gibi parantez kullanır
    • _, catch-all arm’dır
    • arm’lar top-to-bottom sequential first-match şeklindedir ve implicit fallthrough yoktur
  • Game Boy emulator’ındaki opcode dispatcher başlıca kullanım örneğidir
    • 256 base opcode ve 256 prefix opcode’u dispatch eden biçimdedir
  • enum payload matching de desteklenir
    • variant pattern tag’i match eder ve payload field’ını arm içinde fresh local olarak bind eder
    • compiler variant set için exhaustiveness kontrolü yapar
    • Yeni variant eklendiğinde, o variant’ı ele almayan match site compile fail olur
  • match expression olarak da çalışır
    • Her arm block trailing expression değerini üretir
    • Tüm arm’lar aynı type’ı produce etmelidir
    • match exhaustive olmalıdır
  • Dahili olarak tüm match’ler Luc Maranget 2008 tabanlı decision tree pipeline üzerinden compile edilir
    • integer literal cascade, LLVM simplifycfg kârlı olduğunda switch ve jump table’a fold edilir

Compile time tasarımı

  • Rust compile pipeline’ı birden fazla IR ve analiz aşamasından geçer
    • tokens → AST → HIR → THIR → MIR → monomorphization → LLVM IR → machine code
    • trait solving bir search problem’dir; borrow checking ise whole-function region analysis’tir
    • monomorphization, LLVM öncesinde code volume’u artırır
  • Jam pipeline’ı daha kısa tasarlanmıştır
    • tokens → AST → AstGen → JIR → codegen → LLVM IR → machine code
    • typed IR olan tek bir JIR kullanır
  • JIR, AstGen tarafından oluşturulduğu andan itibaren typed durumdadır
    • Jam’de untyped lowering’i zorunlu kılan comptime-as-values olmadığı varsayılır
    • drop placement, init-before-use check ve call-site exclusivity rule, JIR üzerindeki local dataflow pass ile gerçekleştirilir
  • Her binding’de type annotation olduğu için global type inference ve open-ended trait search yükünün daha az olduğu düşünülür
  • AST ve JIR flat data structure’dır
    • small fixed-size node’ları contiguous array içinde packing eder
    • pointer yerine index kullanır; oversized payload side pool’da saklanır
    • compiler’ın heap-allocated tree’yi izlemesi yerine cache-friendly array’leri dolaşması sağlanır
  • Backend’de LLVM, release build optimization süresine hâkimdir
    • debug build’de Cranelift, release build’de LLVM kullanılan bir split planlanıyor
    • Cranelift roadmap’te yer alıyor ve henüz tamamlanmadı
  • Mevcut compiler, language’i bootstrap etmek için C++ implementation aşamasında ve alıntılanabilecek build-time benchmark henüz yok
    • compile-time ile ilgili claim’ler ölçüm sonucu değil, design claim’dir

Runtime performance ve örnek

  • Hedef, Jam’in performance açısından Rust ve Zig’i yakalaması
  • Jam’de GC, managed-memory runtime veya per-allocation header yok
    • codegen, straightforward LLVM IR’dir
  • Henüz Rust ve Zig seviyesine ulaştığı düşünülmüyor
    • Rust ve Zig, standard library’de target-specific intrinsic, auto-vectorization hint, allocator-aware container, hot path tuning, LLVM pass tuning gibi çalışmaları uzun süredir yapıyor
    • Jam’in de son %10–30’luk farkı kapatması için aynı türden çalışmalara ihtiyacı var
  • Şu anda ölçülen workload’da gap’in “farklı bir class” değil, small constant factor içinde olduğu düşünülüyor
  • Terminalde çalışan Tetris demo’su Jam ile yazıldı

Yayın planı ve kalan işler

  • Jam henüz public değil
    • compiler mevcut ve çalışıyor, ancak wider release öncesinde
  • day-to-day kullanım için şu işler yürütülüyor
    • stable surface
    • package manager
    • LSP
    • formatter
    • kalan tooling
  • Ayrı yazılarda ele alınması planlanan konular var
    • parameter mode system
    • exclusivity rule
    • generics
    • Jam’in comptime’ı
    • standard library
    • allocator systems
    • panic model
    • GPU codegen pipeline için MLIR exploration
    • FFI için Rust ABI work
    • Cranelift
    • self-hosted compiler yolu
  • Açık kaynak planı, Jam ile 108 ayrı proje yaptıktan sonra yayımlamak
    • 108 sayısı, Suikoden 2’deki 108 Stars of Destiny’den gelen arbitrary milestone
    • Şu anda small group of users’a ulaştırıldı; tooling yetiştikçe kapsamın genişletilmesi planlanıyor
  • early access, jamlang.org üzerindeki beta listesiyle alınabilir

1 yorum

 
GN⁺ 3 시간 전
Lobste.rs yorumları
  • Trait çözümleme bir arama problemidir. Borrow checking, tüm fonksiyon kapsamını analiz etmektir. Monomorfizasyon, en yavaş aşama olan LLVM daha görmeden kod miktarını artırır…

    Bu tür LLM üretimi yazılar, mühendislerin, özellikle de genç mühendislerin dikkat etmesi gereken bir şeyi yapıyor: nicel verilerin yerine nitel ve kulağa makul gelen düzyazı koymak
    Hikâyeyle ikna etmek, sağlam sayılar toplayıp analiz etmekten hem yazar hem de okur için daha kolay. İnsan beyni hikâyeleri sever; hikâyeler de basit ve derli toplu olduğunda en iyi etkiyi gösterir. Gerçek veriler ise çoğu zaman, bakmaya ne kadar istekliyseniz o kadar nüans barındıran karmaşık bir dünyayı yansıtır
    Bunu bir rustc katkıcısının yazdığı Rust derleyicisi profillemesine dair nicel blog yazısıyla karşılaştırabilirsiniz

    • Bu projeyi ciddiye almamız gerekip gerekmediğinden bile şüphe duyuyorum
    • “Nicel verilerin yerine nitel/çağrışımsal düzyazı koymak” ifadesini özellikle beğendim
      İyi bir teknik yazı, uygunsa ikisini de içerebilir ve içermelidir; ama asıl aktarılması gereken şeyi kaçırmamalıdır. Büyük bir güvence işi organizasyonu yürütünce teknik yazmanın ne kadar zor olduğunu gördüm; LLM erişilebilirliği kolaylaştıkça bu sorunun ne kadar daha kötüleşebileceğine karşı dikkatli olmak gerekiyor
  • Zig ile arasındaki temel fark, drop olması ve kolayca yanlış kullanılan belirli bir yapı taşı olan undefinedın olmaması mı?

    undefined yok ve tüm değerlerin başlatılması gerekiyor; ama Maybe(T).empty() içeriği “henüz anlamlı olmayan” bir değer döndürüyor ve hemen ardından unsafeAssumeInit() çağrılırsa çöp bir değer döndürecek gibi görünüyor. O hâlde bu, Rust’ta derleyicinin unsafeı açık bir unsafe { .. } gerektiren bir bulaşma olarak ele alması anlamında güvenli değil

    Güvenliği ve drop özelliğini gösteren örnek kod bu:

    const File = struct {  
        fd: i32,  
        fn drop(self: mut File) {  
            close(self.fd);  
        }  
    };
    
    export fn useFile() i32 {  
        const f: File = { fd: 7 };  
        return f.fd;  
    }  
    

    Yanlış görmüyorsam bu güvenli değil mi? Elle dosya tanımlayıcısı atamayı bir kenara bıraksak bile, close(7) çağrıldıktan sonra 7 döndürülüyor. Ömür takibi olmadığı için kullanıcının dosya tanımlayıcısının ömrünün useFile() dönmeden önce bittiğini ifade etmesinin bir yolu yok

    ABI örneğinde export fn counterAdd(c: mut Counter, n: i64) i64 { .. }, int64_t counterAdd(Counter *c, int64_t n); hâline geldiğinde, cnin NULL olup olamayacağı nasıl ifade ediliyor? Rust’ta bu kısım için tanımlı bir ABI var; extern "C" fn counterAdd(c: &mut Counter, n: i64) -> i64 da mümkün, extern "C" fn counterAdd(c: Option<&mut Counter>, n: i64) -> i64 da mümkün

    Rust sürümünde de unsafe gerekmiyor. API’yi referanslarla tanımlayabilirsiniz. İronik olarak unsafe gerekebilecek tek yer, modern Rust’ta #[unsafe(no_mangle)] olan #[no_mangle] civarı; örnek ise nedense Rust tarafını ham pointer kullanacak şekilde kurgulamış

    Sondaki şu örnek de:

    extern fn snprintf(buf: *mut[] u8, size: u64, fmt: *const[] u8, ...) i32;
    
    fn render(value: i32) i32 {  
        var buf: [16]u8 = [0; 16];  
        return snprintf(&buf[0], 16, "n=%d", value);  
    }  
    

    Burada bir yerlerde unsafe olması gerekmiyor mu? snprintf ham pointer aldığına göre, yukarıda söylenen unsafe işlemler adlarından bulunabilmeli yönergesine göre unsafeSnprintf ve sembolü yeniden tanımlama gibi bir şey olmalı gibi

    “Dürüst bir ipucu: extern satırında C ile konuşuyorsunuz ve C’nin kuralları geçerli olur” denmiş, hmm

    • Ben de böyle okudum. Yalnız Rust’ın .as_raw_fd()sinden farkı yok ve orada da aynı güvenlik sorunu var
  • Rust’ın ABI’sini kararsız yapan şey Jam’de yok. Birinci sınıf referanslar yok, ömürler yok, silinmesi gereken niche-packed yerleşimler yok

    Bu, Rust standart kütüphanesinin FFI kararlılığını yanlış anlamak. Paylaşılan referanslar, değiştirilebilir referanslar, Box ve bunların Optionları tanımlı ve kararlı ABI’ye sahiptir. Bu yüzden örnekteki tüm Box::into_raw/from_raw prosedürü gereksiz
    Ömürler ikili düzeyde zaten hiç yoktur. Bir enum için kararlı ABI tanımlamayı seçerseniz niche optimizasyonu devre dışı kalır

    Çoğu tipin kararlı ABI tanımlamamasının nedeni, tipin içini değiştiremez hâle getireceği için çoğu zaman kararlı ABI istenmemesidir

  • Jam henüz kamuya açık değil. Derleyici var ve çalışıyor; ancak günlük kullanıma uygun kılan şeyler—kararlı bir yüzey, paket yöneticisi, LSP, biçimlendirici, yokluğunu ancak olmadığında fark ettiğiniz diğer araçlar—üzerinde çalışırken daha geniş açılışı erteliyor…

    Bu tercihi anlamıyorum. Eksik bir şeyi “yayınlamak” ile sadece kaynak kodunu açmak arasında büyük fark var. Zaten daha sonra yapılacaksa, projeyi geliştirirken açık tutmanın ne zararı var?
    Avantajı, yönünü beğenen insanların bizzat deneyip belki katkıda bulunabilmesi. Elbette “AI çağı” olduğu için böyle katkıların net fayda sağlayıp sağlamayacağı belli değil. Ayrıca insanların ne inşa edildiğini daha iyi anlamasını ve neden harika olduğuna dair iddiaları değerlendirebilmesini sağlar. Bunu yapamazlarsa proje çok daha az ilgi çekici olur

    Üstelik bu araçların hiçbirini kullanmayan insanlar da var. Şu anda benim ekibim otomatik biçimlendirici benimsemekte bile anlaşamıyor, ama bunun dışında harika. Dolayısıyla bu araçları yaparken kamuya açmayı ertelemek pek bir fark yaratmıyor

  • İnsanlar sürekli “can sıkıcı lifetime’ları olmayan Rust” yapmaya çalışıyor ve sürekli başarısız oluyor. Başka bir yorum başarısızlık biçimlerinden birini ele almıştı; drop edilmiş bir değerin bir kısmını döndürme sorunu, referans döndürülememesinden kaynaklanıyor. Diğer klasik sorun da şu:

    let mut arr = vec![1];  
    let x = &arr[0];  
    arr.push(2);  
    // `x` kullanılırsa ne olur?  
    

    Üç yanıt var:

    1. Reddetmek. Bunun için bir tür ödünç alma kavramı gerekir. Genelde paylaşımlı XOR değiştirilebilir olur; yalnızca değiştirilebilir varsa kullanışsızdır, yalnızca paylaşımlı varsa güvenli değildir
    2. İzin vermek. Çünkü başka değişkenler üzerinden referans yoktur ve her şey GC ya da referans sayımlı işaretçidir
    3. İzin verip çalışma zamanında tanımsız davranışa yol açmak

    Bu üçünden hangisini seçseniz kendince iyi nedenleri var; ancak Jam, Rust gibi 1 numara olmak istiyor gibi görünürken gerçekte değer semantiği nedeniyle 2 numara gibi duruyor. Bunun anlamı her şeyin kopyalanmasıysa, güvenli ama aynı zamanda verimli veri yapıları yazmayı engelleme olasılığı yüksek

    • Inko’nun oldukça iyi iş çıkardığını düşünüyorum. Elbette bariz önyargımı bir kenara koymak gerek, ama onun da kendine özgü tavizleri kesinlikle var
      Özellikle borrow checker’dan vazgeçerseniz, çeşitli ek ipuçları getirmeden stack’te ayrılan tipleri desteklemek çok daha zorlaşıyor. Örneğin ödünç alırken kopyalama yaklaşımı; Inko ve Swift’in ikisi de bunu yapıyor
    • Jam’den emin değilim, ama Hylo tarzı değiştirilebilir değer semantiğinde subscripts denen bir ödünç alma biçimi var. Bu yüzden biraz daha orta noktaya yakın
    • O kısmı okuduğumda aklıma ilk gelen soru şuydu: “Referanslar ve lifetime anotasyonları yoksa, bir struct’ın içinde referansı nasıl saklarsın?”
      Dil referansına bakınca referans yok ama mut ve const işaretçiler var; bunların güvenliğiyle ilgili bir şey bulamadım
  • Zig’i Zig yapan büyük unsurlardan biri RAII’nin olmaması, Rust’ınki ise borrow checker. Ama bu tasarım seçimlerinin vardığı nokta olan “referanssız RAII”ye gerçekte kimin ihtiyaç duyduğunu pek bilmiyorum
    Yine de bu nişte deney yapacak alan olduğunu düşünüyorum ve bu tür denemeleri olumlu karşılıyorum. Sadece bu yaklaşım o değil gibi

    Son zamanlarda sürekli düşündüğüm yön; Zig’in comptime’ı, Pony’ye benzer referans yetkileri, lifetime’ları derleme zamanı değeri olarak ele almak ve lifetime’ları allocator’lara markalamak gibi şeylerin bir birleşimi
    Beklentim, Zig’in allocator stratejisine referans güvenliği eklemek ve neredeyse anotasyon gerektirmeyen lifetime’lar elde etmek

  • Yeni diller güzel, ama her şeyin LLVM’in frontend’i haline gelmesini istemiyorum. Backend’in zor olduğunu biliyorum, ama bazen başka seçenekler de olsa keşke

  • Neredeyse Swift gibi duyuluyor