Jam programlama dili
(rapha.land)- 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’larMaybe(T)veunsafeAssumeInit()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 veyareprannotation 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
Filetype’ıfn drop(self: mut File)bildirir;useFile()içinde yalnızcaconst f: File = { fd: 7 };yazılır- Açık cleanup,
deferveya lifetime sonu işareti yoktur - LLVM IR’de
rethemen öncesindecall void @__drop_File(ptr %1)üretilir
- Açık cleanup,
- Mangled name olan
__drop_File, birden fazla type’ın drop fonksiyonlarının LLVM level’da çakışmasını engeller self: mut Filepointer 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
- C++’taki rule of 0/3/5, virtual destructor, constructor exception, destructor exception,
İlklendirme ve Maybe(T)
- Jam’de
undefineddeğer yoktur ve binding değersiz bildirilemez- Tüm
varveconstgerçek bir initializer gerektirir - struct’ta önce field değerleri hesaplanır, ardından struct literal ile oluşturulup binding yapılır
- Tüm
- 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
0xaafill uygulanır - Release mode’da arbitrary bytes olur
- Debug mode’da misuse görünür olsun diye
- 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
undefinedhem de implicit zero tarafını önler - Gecikmeli ilklendirme ve out-parameter için
Maybe(T)kullanılırempty()henüz anlamlı contents’e sahip olmayan bir slot oluştururwrite()slot’u doldururunsafeAssumeInit()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 reddederunsafeprefix’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,matcharm,while,forbody içindeki binding’ler ilgili block sonunda drop edilir- nested block içindeki
return, gerçekretöncesinde active scope’ları innermost-first sırayla drop eder breakvecontinue, 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 önceinner, sonraouterdrop 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
movedeğ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 edilirlet y = v[i], element’i value olarak döndürenv.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 OKmoveX(p, p.x)isepilep.xoverlap 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_rawveBox::from_rawile elle aktarılır - struct’ı by value geçirirken
#[repr(C)]gibi ayrı annotation gerekir cbindgenveabi_stablegibi araçlar bu sınırdaki el işini azaltmak için vardır
- raw pointer dereference
- 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çarexport fn counterAdd(c: mut Counter, n: i64) i64, C’deint64_t counterAdd(Counter *c, int64_t n);olarak çağrılabilirmut Counterparameter, caller-owned storage’a yönelikCounter *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ş
externile C signature bildirerek yapılırexternfunction 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
matchyapısıPattern Blockbiç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
- scrutinee,
- 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
matchexpression 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
simplifycfgkârlı olduğundaswitchve jump table’a fold edilir
- integer literal cascade, LLVM
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
Lobste.rs yorumları
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
İ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,
dropolması ve kolayca yanlış kullanılan belirli bir yapı taşı olanundefinedın olmaması mı?undefinedyok ve tüm değerlerin başlatılması gerekiyor; amaMaybe(T).empty()içeriği “henüz anlamlı olmayan” bir değer döndürüyor ve hemen ardındanunsafeAssumeInit()çağrılırsa çöp bir değer döndürecek gibi görünüyor. O hâlde bu, Rust’ta derleyicininunsafeı açık birunsafe { .. }gerektiren bir bulaşma olarak ele alması anlamında güvenli değilGüvenliği ve
dropözelliğini gösteren örnek kod bu: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 sonra7döndürülüyor. Ömür takibi olmadığı için kullanıcının dosya tanımlayıcısının ömrününuseFile()dönmeden önce bittiğini ifade etmesinin bir yolu yokABI örneğinde
export fn counterAdd(c: mut Counter, n: i64) i64 { .. },int64_t counterAdd(Counter *c, int64_t n);hâline geldiğinde,cninNULLolup 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) -> i64da mümkün,extern "C" fn counterAdd(c: Option<&mut Counter>, n: i64) -> i64da mümkünRust sürümünde de
unsafegerekmiyor. API’yi referanslarla tanımlayabilirsiniz. İronik olarakunsafegerekebilecek 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:
Burada bir yerlerde
unsafeolması gerekmiyor mu?snprintfham pointer aldığına göre, yukarıda söylenenunsafeişlemler adlarından bulunabilmeli yönergesine göreunsafeSnprintfve sembolü yeniden tanımlama gibi bir şey olmalı gibi“Dürüst bir ipucu:
externsatırında C ile konuşuyorsunuz ve C’nin kuralları geçerli olur” denmiş, hmm.as_raw_fd()sinden farkı yok ve orada da aynı güvenlik sorunu varBu, Rust standart kütüphanesinin FFI kararlılığını yanlış anlamak. Paylaşılan referanslar, değiştirilebilir referanslar,
Boxve bunlarınOptionları tanımlı ve kararlı ABI’ye sahiptir. Bu yüzden örnekteki tümBox::into_raw/from_rawprosedü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
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ı;
dropedilmiş bir değerin bir kısmını döndürme sorunu, referans döndürülememesinden kaynaklanıyor. Diğer klasik sorun da şu:Üç yanıt var:
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
Ö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
Dil referansına bakınca referans yok ama
mutveconstişaretçiler var; bunların güvenliğiyle ilgili bir şey bulamadımZig’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şimiBeklentim, 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