- pslang, büyük oyunlarda modlama imkânları ve C++ derleyicilerinin ürettiği assembly’ye duyulan ilgiden yola çıktı; şu anda yaklaşık 1.000 LOC büyüklüğünde bir Monte-Carlo path tracer yazabilecek kadar çalışır durumda
- Bir modlama dili için C birlikte çalışabilirliği, düşük seviyeli dizi ve pointer işlemleri, kolay sandboxing, küçük derleyici boyutu ve hızlı derleme gerekiyor; Lua ve C++ native modlar ise sırasıyla performans bağlantısı, sandboxing ve dağıtım açısından sınırlamalar gösteriyor
- pslang; buyurgan, eager evaluation ve call-by-value temelli düşük seviyeli bir dil olup statik, strict, nominal tip sistemi, girinti tabanlı scope, yerleşik diziler, fonksiyon tipleri, pointer’lar ve garantili bellek yerleşimi sunuyor
- Derleyici; Bison tabanlı parser, AST tip denetimi, IR, interpreter ve JIT olarak ayrılıyor; şu anda yalnızca Aarch64 Mac hedefini destekliyor ve IR eklendikten sonra register allocator eksikliği nedeniyle üretilen kodun kalitesi hâlâ düşük
- Mevcut gerçekleştirim yaklaşık 10.000 satır C++ kodundan oluşuyor; ileride register allocator, IR optimizasyonu, IR interpreter, executable üretimi, debugging bilgisi, polymorphism, modüller ve standart kütüphane gibi özellikler değerlendiriliyor
pslang’i yapma motivasyonu
- Yaklaşık 17 yıl programlama yaptıktan sonra, oyuncak değil, belli ölçüde gerçek kullanım hedefi olan bir dili bizzat yapma isteği büyüdü
- Geçmişte FALSE gibi ezoterik diller için interpreter’lar ve çeşitli lambda hesaplama interpreter’ları yapmış olsa da, bunlar “gerçek” bir dil yapma arzusunu tatmin etmedi
- Geliştirilmekte olan büyük oyun modlamaya uygun bir yapıda olduğundan, modlama yöntemleri düşünülürken özel bir programlama dili basit çözümlerden biri olarak öne çıktı
- 2025 Aralık ayında Matt Godbolt’un Advent of Compiler Optimisations yazısını izlerken C++ derleyicilerinin ürettiği assembly’nin peşine düştü ve yeniden assembly ile uğraşmak istedi
- Dil şu anda production kalitesinden uzak olsa da, yaklaşık 1.000 LOC büyüklüğünde çalışan bir Monte-Carlo path tracer yazabilecek seviyeye ulaştı
Modlama gereksinimleri ve mevcut seçeneklerin sınırları
- Oyun, özel bir ECS motoru ile yüz binlerce entity’yi simüle ettiği için, modlama dilinin component pointer demetlerini alıp C’deki
for döngüsü gibi dolaşabilmesi isteniyor
- Modları kontrol etmek zor olduğundan, oyuncuyu korumak için sandboxing kolay olmalı; ideal olarak tüm IO ve benzeri yetenekler tek bir anahtarla devre dışı bırakılabilmeli
- Modlama, belirli bir klasöre script koyar koymaz mod olarak kullanılabilecek kadar kolay olmalı
-
Lua ve JIT scripting dilleri
- Lua standart bir seçenek olsa da, güvenilmeyen koddan önce standart kütüphanedeki IO ile ilgili fonksiyonları silen bir ön işleme kodu eklemek gibi bir sandboxing yaklaşımı gerektiriyor ve bu, sağlam bir çözüm gibi görünmüyor
- Lua yüksek seviyeli, dinamik tipli bir dil olduğu için C pointer’larını doğrudan anlayamıyor; bu yüzden ECS entity dolaşımını bağlamak için ya her entity’de native ↔ Lua ↔ native geçişi gerekiyor ya da native entity’leri bir Lua dizisine çevirip sonra yeniden açmak gerekiyor
- Standart Lua ile LuaJIT birkaç sürüm öncesinden beri ayrışmış durumda; bu da hem mod geliştiricileri hem de uygulayıcılar için kafa karıştırıcı olabilir
-
C++ ve native modlar
- Modlar C++ ile yapılırsa entity dolaşımı sorunu ortadan kalkıyor, ancak binary dağıtımı tüm platformlar için geliştirme ortamları ve binary artifact deposu gerektiriyor
- Kaynak kod olarak dağıtmak için oyuna bir C++ derleyicisi eklemek gerekiyor; temel bir LLVM kurulumu bile şu anki oyun boyutunun 10–20 katı disk alanı kaplıyor
- Native bir DLL
int open(); bildirip kullanabiliyorsa dosya sistemi veya ağ erişimini engellemek pratikte imkânsız olduğundan sandboxing yapılamıyor
- Rust gibi diğer native diller için de aynı sorun geçerli
- Modlama hedeflerden biri olsa da, bu dilin gerçekten oyun modlamasında kullanılıp kullanılmayacağı henüz belirsiz; ayrıca dili belirli bir kullanım senaryosuna fazla özelleştirmek de istenmiyor
Dil tasarım hedefleri
- C birlikte çalışabilirliğini kesintisiz sunarak native oyun kodu ile modlama kodu arasındaki bağlantıyı bir fonksiyon çağrısı kadar basit kılmak amaçlanıyor
- Ham entity dizileriyle çalışmak gerektiği için düşük seviyeli özellikler gerekiyor
- Mod geliştiricilerinin makul bir rahatlıkla kod yazabilmesi için dilin pratik ve kullanışlı olması gerekiyor
- Sandboxing kolay olmalı ve derleyici boyutu da küçük tutulmalı
- 50MB’lık bir oyuna 1GB’lık derleyici koymak istenmediğinden derleyici ayak izi azaltılmaya çalışılıyor
- Oyuncular mod derlenirken uzun süre beklememeli; bunun bir kısmı kapsamlı caching ile hafifletilebilir
- Gerçek çapraz platform desteği isteniyor, ancak yaygın masaüstü platformlarından birkaçı, 64-bit ve IEEE754 desteği gibi varsayımlar kabul ediliyor
- Çoğu dinamik dille karşılaştırıldığında makul derecede hızlı olması yeterli görülüyor
- Uzun süre ana dil C++ olduğu için dil anlayışını ciddi biçimde etkilese de, mümkünse C++’ı aynen yeniden yaratmaktan kaçınılıyor
pslang’in mevcut dil modeli
- Çalışma adı, psemek oyun motorundan gelen pslang; buyurgan, eager evaluation, call-by-value ve düşük seviyeli bir dil
- Tip sistemi; statik, strict ve nominal bir tip sisteminden oluşuyor
- Temel örnek; fonksiyon, struct, fonksiyon tipi ve dizi döndürmeyi birlikte kullanıyor
func min(x: i32, y: i32) -> i32:
return if x < y then x else y
struct vec3i:
x: i32
y: i32
z: i32
func apply(f: i32 -> i32, v: vec3i) -> vec3i:
return vec3i(f(v.x), f(v.y), f(v.z))
func as_array(v: vec3i) -> i32[3]:
return [v.x, v.y, v.z]
Scope ve temel tipler
- Girinti tabanlı scope kullanılarak dilin bir scripting dili gibi görünmesi ve yeni başlayanlara daha tanıdık gelmesi hedefleniyor
- Şu anda girinti için tab karakteri kullanılıyor, ancak ileride space’e geçilebilir
- Fonksiyonlar, döngü gövdeleri,
if gövdeleri vb. yeni bir scope oluşturuyor; fonksiyonlar ve struct’lar herhangi bir scope içinde tanımlanabiliyor ve yalnızca o scope içinde görünür oluyor
- Yerel fonksiyonlar tanımlandıkları scope’taki değişkenlere erişemediği için closure değiller; scope yalnızca isim çözümlemeyi etkiliyor
- En üst seviye scope da diğerleri gibi ele alınıyor ve dosya yüklendiğinde ya da ilklendirildiğinde çalışan bir entry point içeriyor
- Temel tipler
bool, 4 signed integer, 4 unsigned integer, 3 floating-point türü ve unit olmak üzere toplam 13 adet
i8 i16 i32 i64
u8 u16 u32 u64
f16 f32 f64
f8, çoğu masaüstü CPU’da desteklenmediği ve 8 bit floating-point’in anlamı konusunda ortak bir uzlaşı bulunmadığı için dahil edilmiyor
f16, genel kullanıcı için daha az faydalı olsa da HDR renkler, vertex attribute’ları gibi grafik işlerinde sık kullanılıyor; ayrıca modern masaüstü CPU’ların çoğu IEEE754 f16 uyguladığı için yerleşik olarak destekleniyor
- Tüm tamsayı aritmetiği taşmalı two’s complement mantığıyla çalışıyor; tanımsız davranış yok
unit yalnızca unit() adlı tek bir değere sahip ve geri dönüş değeri olmayan fonksiyonların resmî dönüş tipi
- Dönüş tipi belirtilmeyen fonksiyonlar otomatik olarak
unit döndürüyor; böyle fonksiyonların sonunda return yazılmazsa otomatik ekleniyor
unit fonksiyonu olmayan bir yerde değer döndürülmezse bu hata sayılıyor
Literal'ler, diziler, fonksiyon türleri, pointer'lar
10 sayısı varsayılan olarak i32'dir ve boyut 10b, 10s, 10l gibi son eklerle belirtilir
- İşaretsiz literal'ler
u son eki alır ve 10ub, 10us, 10u, 10ul gibi yazılır
- Ondalıklı kayan nokta literal'leri varsayılan olarak
f32'dir; 10.0h 16 bit, 10.0d ise 64 bittir
10. veya .5 gibi tam sayı ya da kesir kısmı atlanamaz; 10.0, 0.5 gibi tam yazılmalıdır
- Tüm sayısal literal'ler belirsiz olmayan bir türe sahiptir
- Diziler yerleşik birinci sınıf türlerdir ve C/C++'tan farklı olarak dizinin tamamı fonksiyona geçirilebilir, döndürülebilir veya başka bir diziye atanabilir
- Dizi boyutu her zaman derleme zamanında bilinir ve aynı türden birden çok alana sahip bir struct gibi davranır
- Dizi türü
i32[5], dizi literal'i ise [1, 2, 3, 4, 5] şeklinde yazılır
- Fonksiyon türleri C'deki function pointer'lara yakındır;
(a, b, c) -> d biçiminde yazılır ve tek argüman varsa parantez a -> b gibi atlanabilir
- Dahili olarak fonksiyon türü, yanında veri taşımayan sıradan bir function pointer'dır; closure değildir
- Pointer türleri
i32* gibi yazılır; varsayılan olarak değiştirilemez pointer'dır, değiştirilebilir pointer ise i32 mut* olarak tanımlanır
- Bir değişkenin adresi
&x, değiştirilebilir pointer &mut x, dereference *p, pointer aritmetiği ise *(p + 10) gibi kullanılır
Struct'lar, bellek yerleşimi, boş türler
- Struct'lar
struct anahtar sözcüğü ve alan listesiyle tanımlanır
struct string_view:
size: u64
data: u8*
- Struct'lar
string_view(10, data) gibi yerleşik fonksiyonel yapıcıyla oluşturulur ve alanlara v.x gibi nokta ile erişilir
- Struct pointer'larında da alanlara aynı nokta sözdizimiyle erişilebilir
- Struct alanlarında ayrı bir değiştirilebilirlik belirteci yoktur; değiştirilebilir nesnenin alanları değiştirilebilir, değiştirilemez nesnenin alanları ise değiştirilemezdir
- Erişim belirleyicileri yoktur ve alanlar her zaman public'tir
- Tüm nesneler garanti edilen bir bellek yerleşimine sahiptir; temel türler boyutlarıyla aynı hizalamaya sahiptir ve
bool 1 bayttır
- Pointer ve fonksiyon türleri her zaman 64 bittir ve aynı hizalamaya sahiptir
- Diziler öğeleriyle aynı hizalamaya sahiptir, struct'lar ise hizalama gereksinimlerini karşılamak için padding içerir
- Bu garanti esas olarak C birlikte çalışabilirliğini ve GPU programlama kullanımını basitleştirmek içindir
unit ve alanı olmayan struct'lar yalnızca tek bir geçerli değere sahip boş türler olarak değerlendirilir ve gerçek boyutları 0 bayttır
- Boş türleri fonksiyona geçirmek, değişken olarak tanımlamak veya alan olarak eklemek bellek kullanımını ya da struct boyutunu etkilemez
- Boş türler, tür düzeyinde derleme zamanı etiketi benzeri amaçlarla kullanılabilir
- Boş tür pointer'ları üzerinden okuma/yazma henüz kararlaştırılmamıştır; şu anda bu türlerin pointer'larıyla aritmetik yapmak geçersizdir
- C++'taki gibi her nesnenin kendine özgü bir bellek adresi olması kuralı izlenmez
Değişkenler, fonksiyonlar, kontrol akışı, harici fonksiyonlar
- Değiştirilemez değişkenler
let x = 10, değiştirilebilir değişkenler ise mut x = 20 gibi tanımlanır
- Değiştirilemez bir değişken için değiştirilebilir pointer oluşturulamaz
- Tür
let x: i32 = 10 gibi açıkça belirtilebilir, ancak tüm ifade türleri belirsiz olmayacak şekilde çıkarılabildiğinden zorunlu değildir
- Tüm değişkenler mutlaka başlatılmalıdır
- Fonksiyonlar
func foo(x: A, y: B) -> C: ardından gövde gelecek şekilde yazılır; dönüş türü atlanırsa unit kabul edilir
- Tüm fonksiyonlar çalıştırma platformunun yerel C ABI'sini izler; bu, C birlikte çalışabilirliği, callback'ler ve ECS sistemleri gibi kullanımlarda function pointer olarak aktarılabilmeleri için tercih edilmiştir
- Aynı scope içinde fonksiyon ve struct tanımlarının sırası serbesttir; daha sonra tanımlanan fonksiyon veya struct önce kullanılabilir
- Tüm fonksiyon argümanları ve dönüş türleri tamamen açık yazılmak zorunda olduğundan, tanım sırasını serbest bırakmak tür çıkarımını karmaşıklaştırmaz
if/else if/else ifadeleri ve while döngüsü vardır; henüz for döngüsü yoktur
- İfade biçimindeki
if, if A then B else C şeklinde kullanılır
- Harici fonksiyonlar
foreign func sin(x: f64) -> f64 gibi tanımlanır; uygulamanın başka bir yerde linklenmiş olması gerekir
- Mevcut yorumlayıcı bu tür fonksiyonları yorumlayıcı çalıştırılabilir dosyasının içinden
dlsym ile bulur
- Harici fonksiyonlar, C kütüphaneleri ve üçüncü taraf kütüphanelerle birlikte çalışmanın ana mekanizmasıdır; raytracer örneği karekök hesabı, dosya yazma, zaman ölçümü ve thread oluşturma için bu özelliği kullanır
Tür dönüştürme ve operatörler
- Örtük tür dönüşümü hiç yoktur; elle dönüştürme için
(x as f32) biçimindeki as operatörü kullanılır
- Tüm sayısal türler birbirine dönüştürülebilir ve tüm pointer türleri de birbirine dönüştürülebilir, ancak değiştirilemez pointer'ı değiştirilebilir pointer'a dönüştürmek bunun dışındadır
- Pointer türleri
u64'e, u64 de pointer türlerine dönüştürülebilir
bool hiçbir türle dönüştürülemez
T mut* türünden T* türüne tek bir örtük dönüşüm eklenip eklenmemesi değerlendirilmektedir
- Aritmetik, mantıksal, karşılaştırma vb. standart operatörlerin çoğu sağlanır
&, |, &&, || hem boolean hem tamsayılar üzerinde çalışır; & ve | her iki operandı da her zaman değerlendirirken && ve || kısa devre değerlendirmesi yapar
- Aritmetik ve karşılaştırmalar yalnızca aynı sayısal tür çiftleri üzerinde çalışır; sayısal tür yükseltme yoktur
- Şu anki dil özellikleri çok fazla görünmeyebilir, ancak pratikte gerçek programlar yazmak için şimdiden yeterince rahattır
Derleyici yapısı
- Proje birden fazla kütüphaneye ayrılmıştır
types: tür sistemi tanımları
ast: soyut sözdizim ağacı tanımları ve yardımcı araçlar
parser: parser
ir: ara gösterim
interpreter: yorumlayıcı
jit: JIT derleyici
- Tasarım, yorumlayıcı ve derleyiciyi bu kütüphaneleri kullanan basit CLI uygulamaları olarak tutmak yönündedir; şu anda yalnızca JIT modundaki yorumlayıcı vardır
- Dili embed etmek için
parser ve jit kütüphanelerini kullanmak yeterlidir
Parser ve girinti işleme
- Parser üreticisi olarak Bison kullanılır
- Token'lar lexer grammar, dil grameri ise parser grammar içinde tanımlanır
- Dosya bir ifade listesidir; ifadeler fonksiyon tanımı, kontrol akışı operatörleri, değişken tanımları, expression'lar vb. olabilir; expression'lar ise literal'ler, değişkenler, operatörler, fonksiyon çağrıları vb. olabilir
- Gramerde birkaç kez shift/reduce çatışması düzeltilmek zorunda kalındı ve Bison'ın
-Wcounterexamples bayrağıyla çatışmaya yol açan tam durumlar görüldü
- C++ parser sınıfı üretmek için Bison'ın
lalr1.cc skeleton'ı kullanılır
- Varsayılan Bison, parser durumunu global değişkenlerde tutan bir C parser'ı üretir; ancak yorumlayıcı veya oyun modu gibi durumlarda birden fazla dosyanın paralel parse edilebilmesi gerektiğinden bu uygun değildir
- Bison çalıştırma adımı, CMake scripts içindeki build aşamasına eklenmiştir
- Parser çıktısı, parse edilen dosyanın AST'sini temsil eden C++ nesnesidir
- Girinti nedeniyle gramer aslında context-free değildir; bir ifadenin
while gövdesine ait olup olmadığı, önceki girinti token'larının sayısına bağlıdır
- Çözüm olarak her satır bağımsız bir ifade ve girinti seviyesi olarak parse edilir, ardından basit bir lineer geçişte girinti seviyesine bakılarak scope'lar kesinleştirilir
- Bu yaklaşım biraz hacky olsa da çalışır ve çok hızlıdır, bu yüzden kabul edilmiştir
- Aynı geçişte
break ve continue'nun yalnızca döngü içinde, return'ün yalnızca fonksiyon içinde, alan tanımlarının ise yalnızca struct içinde yer aldığı da doğrulanır
Tür denetimi ve yorumlayıcı
- Ayrıştırmadan sonraki ilk geçiş, tüm tanımlayıcıları çözümler ve tanımlayıcı düğümlerini ilgili değişken, fonksiyon ve struct tanım düğümlerine doğrudan bağlar
- Sonraki temel geçiş, tüm türleri denetler ve çıkarımlarını yapar
- Tür çıkarımı çoğunlukla basittir ve belirli AST düğüm türlerine göre koşul denetimlerinden oluşur
- Örneğin
if ya da while içindeki ifade türü bool olmalıdır; toplamada ise iki işlenen aynı sayısal türde olmalı ya da biri tamsayı, diğeri işaretçi olmalıdır
- İlk yorumlayıcı, AST düğümlerini doğrudan gezerek C++ semantiğini çalıştıran bir tree-walking interpreter idi
- Ana işlevler
exec() ve eval() idi; exec() tek bir ifadeyi yürütür, eval() ise tek bir ifadenin değerini hesaplayıp döndürür
- C++ statik türlendiği için
eval(), dildeki tüm olası değer türlerini kapsayan bir variant döndürür
- Struct'lar, alan başına bir tane olacak şekilde isim-değer çifti dizileriyle temsil edilir; değişken değerlerini saklamak için de aynı
variant kullanılır
- Yorumlayıcının amacı dil kodunu çapraz platformda çalıştırmak, uygulamanın ve programların hata ayıklamasına yardımcı olmaktır; hızlı olmak için yapılmamıştır
- Mevcut yorumlayıcı şu anda çok bozuk durumda, bu yüzden IR tabanlı olarak tamamen yeniden yazılması planlanıyor
- Eski yorumlayıcı
foreign fonksiyonları çalıştıramaz
foreign fonksiyonları C çağrı kuralıyla çağrılmalıdır ve argüman sayısı ile türleri önceden bilinmediğinden, vararg tekniği ya da libffi gerekebilir
- Yorumlayıcı iç durumunu, yani değişkenlerin adını, türünü ve değerini stdout'a dökebilir; bu da düzgün bir derleyici yapılmadan önce ayrıştırıcı ve yorumlayıcı hata ayıklamanın başlıca yöntemiydi
İlk Aarch64 JIT derleyicisi
- 2026 Ocak başındaki tatilde elde yalnızca bir M1 Mac olduğu için, ilk derleyici hedef mimarisi Aarch64 Mac oldu
- Şu anda desteklenen tek mimari de bu
- Derleyici JIT tarzında çalışır; çıktı, çalıştırılabilir bitiyle eşlenmiş bir bellek bloğu ve her fonksiyonun başlangıç noktasına işaretçilerden oluşur
- Üst düzey yapı, neredeyse geleneksel bir stack tabanlı derleyiciye benzer; ancak ifade sonuçları, Aarch64 Mac'in standart C çağrı kuralı olan AAPCS64'te aynı dönüş türüne sahip fonksiyonların değer yerleştirme biçimine göre yerleştirilir
- Tamsayılar ve işaretçiler
x0 genel amaçlı yazmacında, kayan noktalı sayılar v0 kayan nokta yazmacında döndürülür; struct'lar ise boyutlarına göre yazmaçlarda ya da stack üzerinde döndürülür
- Bu yaklaşım bellek erişim sayısını azaltır, böylece üretilen kod daha hızlı olur ve fonksiyon çağrıları da basitleşir
- Stack daha çok ikili işlemler gibi ara sonuçlar için kullanılır
(eval A) # the value of A is in x0
push x0 # the value of A is on stack top
(eval B) # the value of B is in x0
pop x1 # the value of A is in x1
add x0, x0, x1 # the value of A+B is in x0
- Kontrol akışı yapıları koşullu atlamalara dönüştürülür; ancak tek geçişli derlemede
if ya da while gövdesi henüz derlenmediği için atlama hedefi bilinmez
- Bunu çözmek için önce ofseti 0 olan bir atlama komutu üretilir, ardından hedef ofset öğrenildiğinde gerçek atlama ofseti enjekte edilir
- Aynı yaklaşım fonksiyon çağrılarında da kullanılır
- Hedef CPU komutlarını üretmek için üçüncü taraf bir kütüphane kullanılmadı; derleyiciyi küçük tutmak için doğrudan uygulandı
- Uygulama, gereken bitleri tek tek yerleştirmek için instruction manual karıştırılarak yazıldı
Aarch64'te zor olan kısımlar
- Aarch64'te tüm komutlar 32 bittir, bu yüzden ilk bakışta kullanımı kolay görünür; ancak 32 bitlik bir sabiti yazmaca koymak için yazmaç seçim biti, komut bitleri ve sabit bitlerinin hepsi gerektiğinden bunlar tek bir 32 bitlik komuta sığmaz
- 64 bitlik sabitler daha da büyük sorundur
- Sabitler, 0, 16, 32 ve 48 bit konumlarına 16 bitlik parçaları yükleyen komutlarla birleştirilmeli ya da sabit belleğe konup oradan yüklenmelidir
- Kayan nokta sabitleri için sabit bellekten yükleme yöntemi kullanılır
- x86'dan farklı olarak push/pop komutları yoktur; yazmaç ile bellek adresi arasında okuma/yazma yapan ve adres yazmacını ayarlayan komutlar birleştirilmelidir
- Tüm komutlar tam olarak 32 bit olduğu için, ofsetin signed mı unsigned mı olduğu, belli bir sabitle önceden çarpılıp çarpılmadığı ya da adres yazmacını değiştirip değiştirmediği sürekli dikkate alınmalıdır
- Stack, SP yazmacı üzerinden okunup yazılırken stack pointer her zaman 16 bayt hizalı olmalıdır
- Kullanılabilir ofsetler 12 bit ile sınırlıdır; bu yüzden stack frame yaklaşık 16 KB'den büyük olduğunda özel kod gerekir, ancak bu henüz uygulanmadı
- Çağrı kuralında, struct'ların en fazla 2 genel amaçlı yazmaç, kayan nokta yazmaçları veya bellek işaretçisi üzerinden geçirilip döndürüldüğü özel durumlar vardır; derleyici kodu bunları ele almak zorundadır
IR'nin eklenmesi ve ikinci derleyici
- Temel yorumlayıcı ve derleyici yapıldıktan sonra, kod yeniden kullanımını artırmak, başka mimariler için derleyici yazmayı kolaylaştırmak ve optimizasyon yapabilmek için bir ara gösterim (IR) eklendi
- IR, SSA benzeri olarak başladı; ancak aynı düğüme değer yeniden atanabildiği ve phi düğümleri kullanılmadığı için aslında SSA değil
- IR, nodes dizisinden oluşur; her düğüm bir literal'i, girdi düğümlerine sahip bir işlemi, koşullu/koşulsuz atlamayı, fonksiyon çağrısını vb. temsil eder
- Bir değeri temsil eden düğümler, o değerin türünü de saklar
- Yeniden atamaya izin verildiği için, mevcut düğüm değerini yeniden atayan bir
assign IR komutu vardır
- Koşullu atlamalar
jump_if_zero ve jump_if_nonzero olarak ayrılır; bu genellikle farklı CPU komutlarına karşılık gelir ve değeri tersleyip karşıt komutu kullanmaktan daha hızlıdır
- Fonksiyon işaretçileri desteklendiğinden, bilinen bir IR düğümünü çağıran komut ile bilinmeyen bir işaretçi değerini çağıran komut ayrıdır
- Optimizasyon sırasında düğümleri istenen yerde silmek ya da eklemek kolay olsun diye düğümler
std::list içinde tutulur ve başvurular liste yineleyicileriyle yapılır
- Struct değer literal'i oluşturulamadığından, struct değerini temsil eden bir
alloc düğümü vardır ve bu genelde stack üzerinde başlatılmamış struct alanı ayıracak şekilde derlenir
- Struct'lar, tek tek alanlara atama yapılarak oluşturulur
- İç içe struct alanı
a.x.y basitçe ifade edilirse a.x yeni bir düğüm olarak okunur, sonra o düğümün y alanı okunur; bu da ciddi israfa yol açar
a.x.y = b ifadesi de t = a.x, t.y = b, a.x = t şeklinde gösterilirse verimsizdir; bu yüzden IR'de iç içe alanlar özel olarak ele alınır
copy düğümü bir struct içinden herhangi bir iç içe alanı çıkarabilir ve assign düğümü bir struct'ın herhangi bir iç içe alanına atama yapabilir
- İç içe alanlar, “0 numaralı alanı al, onun içindeki 2 numaralı alanı al, onun içindeki 5 numaralı alanı al” gibi bir indeks dizisiyle ifade edilir
- Daha sonra Aarch64 derleyicisi, AST → IR derleyicisi ve IR → Aarch64 derleyicisi olarak ikiye bölünerek yeniden yazıldı
- AST → IR nispeten basit, ancak IR → Aarch64 derleyicisi şu anda önceki stack tabanlı derleyiciden çok daha kötü durumda
- Fonksiyon başlangıcında, o fonksiyonun tüm IR düğümleri için gereken kadar stack alanı ayrılır; bu yüzden çok kısa ömürlü ara değerler bile stack frame içinde yer kaplar
- Raytracer'daki bir fonksiyon, stack frame'i yukarıda bahsedilen 12 bit sınırına sığdırabilmek için ikiye bölünmek zorunda kaldı
- Bu derleyici bir yazmaç tahsis edicisi kullanılacağı varsayımıyla tasarlandı; bu yüzden daha sonra üretilen kodun birkaç büyüklük mertebesi kadar iyileşmesi bekleniyor
Derleyici ve yorumlayıcı planları
- Mevcut uygulama yaklaşık 10.000 satır C++ kodundan oluşuyor; modern ölçütlere göre derleyicinin küçük olması ve gerçekten çalışması tatmin edici.
-
Register allocator
- Mevcut IR → Aarch64 derleyicisi için bir register allocator kesinlikle gerekli.
- Derleme hızı ile kod kalitesi arasındaki ödünleşim nedeniyle standart bir linear scan allocator kullanmayı planlıyor.
-
IR optimizasyonu
- IR üzerinde constant propagation, aritmetik sadeleştirme, dead code elimination, inlining ve loop unrolling eklemek istiyor.
- Amaç GCC ya da LLVM'i geçmek değil; ancak 3D vektör toplama gibi basit fonksiyonların mümkün olduğunca az CPU komutuyla derlenmesini istiyor.
-
IR yorumlayıcısı
- Yorumlayıcıyı IR'yi doğrudan değerlendiren bir yapıda yeniden yazmayı planlıyor; bunun yorumlayıcıyı epey basitleştireceğini düşünüyor.
-
Çalıştırılabilir dosya üretimi
- Mevcut derleyici yalnızca hemen çalıştırılacak JIT bellek blob'ları üretiyor.
- Platforma özgü biçimlerde çalıştırılabilir ikili dosyalar da üretmek istiyor; bunun için ELF, Mach-O, PE gibi ikili biçimlerin spesifikasyonlarına dalmak gerekiyor.
- Mümkün olduğunca küçük çalıştırılabilir dosyalar üretmek de hedeflerden biri.
-
Hata ayıklama
- JIT'in ürettiği assembly'yi lldb içinde çok takip etmiş ve dilin kendisini düzgün biçimde debug edebilmek istiyor.
- Bunun için büyük olasılıkla DWARF debug bilgi biçimi desteği gerekecek; şu anda bu konuda neredeyse hiçbir şey bilmiyor.
Eklemek istediği dil özellikleri
-
Struct constructor'ları
- Şu anda struct'lar yalnızca
vec3i(1, 2, 3) gibi tüm alanları ayarlayarak ya da vec3i() gibi sıfırla başlatarak oluşturulabiliyor.
- Struct ile aynı isimde bir fonksiyon tanımlanırsa onun keyfi bir constructor gibi davranmasını sağlamayı düşünüyor.
func vec3i(x: i32, y: i32) -> vec3i:
return vec3i(x, y, 0)
- Ancak böyle fonksiyonlara benzersiz bir ad vermek daha iyi olabilir; bu yüzden karar vermiş değil.
-
Global değişkenler
- Şu anda global değişkenler desteklenmiyor.
global anahtar sözcüğüyle global değişkenler oluşturmayı planlıyor; erişim yine scope kurallarıyla sınırlı olacağı için C'deki static değişkenler gibi fonksiyon-yerel global'ler yapılabilecek.
- En üst düzey değişkenler,
global kullanılmadıkça gerçek global değil; dosyanın entry point fonksiyonunun yerel değişkenleri.
- Bu yapı kullanıcılar için kafa karıştırıcı olabilir; bu yüzden başka seçenekleri de düşünüyor.
- Mac, aynı anda hem yazılabilir hem yürütülebilir bellek eşlemelerine izin vermediği için global değişkenlerin koddan ayrı tahsis edilip farklı bayraklarla eşlenmesi gerekebilir.
- Global erişim, derleme zamanında bilinen ofsetlerle değil, çalışma zamanında çözümlenen adreslerle yapılmak zorunda kalabilir.
- Ancak
mprotect() ile eşlemenin bir kısmının bayrakları değiştirilebiliyor gibi göründüğünden önce bunu denemeyi planlıyor.
-
Metot çağrısı sözdizimi
- Okunabilirlik için, mümkün olduğunda
x.f(y) ifadesinin f(&x, y) veya f(&mut x, y) anlamına gelmesini istiyor.
-
Çok biçimlilik
- Bunu en önemli potansiyel özellik olarak görüyor.
- Güçlü adaylar; C++ tarzı fonksiyon overloading'i ile sınırsız fonksiyon template'leri ve struct template'leri ya da Haskell/Rust tarzı açık trait'ler ve trait kısıtlı generic fonksiyonlar/struct'lar.
- C++ tarzı daha güçlü, basit durumlarda daha okunabilir ve derleyici tarafında da uygulaması daha kolay; ama hata mesajları çok anlaşılmaz hâle gelebiliyor.
- Açık trait'ler bazı durumlarda daha okunabilir ve hata mesajı sorununu çözüyor; ancak trait ve trait bound gibi yeni bir sistem gerektirdiğinden derleyiciyi uygulamayı zorlaştırıyor.
- Henüz karar vermemiş olsa da, C++'ı yeniden yapmak istememesine rağmen ilk seçeneğe güçlü biçimde eğiliyor.
struct vec2<t: type>:
x: t
y: t
func min<t: type>(x: t, y: t) -> t:
return if x < y then x else y
- Mümkün olduğunda fonksiyon argümanı çıkarımı da istiyor.
-
Operator overloading
- Bunun herhangi bir biçimi için çok biçimlilik gerekiyor.
a + b, add(a, b) gibi overload edilmiş bir fonksiyonu ya da Add::add gibi bir trait metodunu çağırabilir.
-
for döngüsü
while ile taklit edilebildiği için foru, C++'taki range-based loop veya Python döngüleri gibi koleksiyon temelli bir döngü olarak planlıyor.
- Bunun için bir range/iterator arayüzü gerekiyor; bu da yine çok biçimlilik gerektiriyor.
-
Otomatik kaynak yönetimi
- Pratik ve kullanışlı bir dilin, bellek, dosya, soket, mutex gibi kaynakların serbest bırakılmasına yardımcı olacak bir yönteme sahip olması gerektiğini düşünüyor.
- Adaylar C++ tarzı RAII ve move, Zig tarzı
defer ve linear type'lar.
- RAII örtük olduğu için gizli komutlar ve kontrol akışı ekleme dezavantajına sahip.
defer açık ama her seferinde elle yazılması gerekiyor, unutulmasını engellemiyor ve dosya dizileri gibi iç içe koleksiyonları serbest bırakırken kullanışsız.
defer free(array)
defer for file in array:
close(file)
- Linear type'lar,
free veya close çağrılarını elle yapmanın açıklığını korurken nesnelerin kaynak serbest bırakma fonksiyonları tarafından tüketilmesini zorunlu kılabildiği için umut verici görünüyor.
- Ancak dinamik dosya dizileri gibi iç içe koleksiyonlarla birlikte kullanımı zor olduğundan henüz karar vermiş değil.
-
Polimorfik literal'ler
- Boş dizi
[] için boyutun 0 olduğu bilinebilir ama eleman tipi çıkarılamaz.
null herhangi bir pointer tipi olabilir; eklemek istediği inf literal'i de herhangi bir kayan nokta tipi olabilir.
- Çözüm olarak Haskell tarzı polimorfik literal'leri, C++'taki
nullptr_t gibi özel yerleşik/kütüphane tipleri ve örtük dönüşümleri ya da AST içinde özel literal'ler ile derleyicide ad-hoc işlemeyi düşünüyor.
- Şu anda,
nullu yalnızca beklenen pointer tipinin bilindiği yerlerde, örneğin açık tipli değişken başlatma veya fonksiyon argümanı geçişinde izin veren son yaklaşıma eğiliyor.
- Bu yaklaşım en basiti ama genişletilebilir değil; dolayısıyla
nulldan özel tipler üretilemiyor.
-
Derleme zamanı değerlendirmesi
const anahtar sözcüğüyle derleme zamanı değişkenleri tanımlamak ve bunların dizi boyutu gibi derleme zamanı ifadelerinde kullanılabilmesini istiyor.
const değerler yeniden atanamaz ve adresleri alınamaz.
- Uygun fonksiyonlar, global değişken erişimi veya yan etki içermedikleri sürece derleme zamanı ifadelerinde çağrılabilir.
- Fonksiyon gövdesi normal bir fonksiyon gibi çalışır, ancak derleme sırasında yürütülür ve sonuç bir derleme zamanı ifadesine dönüşür.
- Matematik fonksiyonları veya bellek tahsisi gibi derleme zamanında çağrılması güvenli olan
foreign fonksiyonları işaretlemek için bir mekanizma gerekiyor.
-
Tip hesaplama
- Metaprogramming için tipler üzerinde hesaplama desteği istiyor.
- Statik tipli bir dilde çalışma zamanı tip kodlamaları üretmek istemiyor ve çalışma zamanı tiplerinin faydasını da sınırlı görüyor; bu yüzden bunu yalnızca derleme zamanı için planlıyor.
- C++ concepts'e benzer özelliklerin de ayrı bir sözdizimine gerek kalmadan derleme zamanı çağrılarıyla uygulanabileceğini düşünüyor.
func comparable(t: type) -> bool:
// Implemented somehow...
func min<t: comparable type>(x: t, y: t) -> t:
return if x < y then x else y
-
Coroutine'ler
- Python veya JS tarzı
async/await eklemek, plandan çok bir temenniye yakın.
Kütüphane ve modül planı
-
Modüller
- Tüm kodu tek bir dosyaya yazmak pratik olmadığı için modüllere ihtiyaç var
import lib.sublib gibi basit bir ifade planlanıyor; kodun herhangi bir yerine konulabilecek ve kapsam kurallarına da uyacak
- Kapsam yalnızca görünürlüğü etkiliyor; gerçek yükleme derleme zamanında gerçekleşecek ve içe aktarılan modülün giriş noktası mevcut modülden önce çalıştırılacak
- Kütüphane adı, derleyiciye veya yorumlayıcıya verilen kök yol baz alınarak dosya sistemi yoluyla doğrudan eşleşecek
- Tek bir kaynak dosyaysa yalnızca o dosya içe aktarılacak; bir dizinse o dizindeki tüm dosyalar belli bir sırayla içe aktarılacak
- Aynı dizindeki dosyaları işaret eden bir sözdizimine ihtiyaç var;
import .another gibi bir biçim düşünülüyor
- İçe aktarılan işlevler ve global değişkenler önek olmadan kullanılabilecek; belirsizlik olduğunda
io.print(x) gibi kütüphane adı öneki eklenebilecek
- Modül giriş noktaları, import sırası ve özyinelemeli importların topolojik sıralamasına göre belirli bir sırayla çalıştırılacak; bu da C veya C++'taki başlatma sırası sorununu çözebilir
- Çok modüllü programların bellek yerleşimi henüz kararlaştırılmadı
- Her modül için ayrı bellek yaması tutulup işlev çağrıları ile global değişken erişimlerinin çalışma zamanında çözümlenmesi düşünülebilir; ya da tek büyük bir bellek eşlemesi oluşturulup göreli ofsetler kullanılabilir
- Tek büyük eşleme çalışma zamanında daha hızlı olabilir, ancak birden fazla modülün paralel derlenmesini zorlaştırır
-
Prelude
- Modüller geldiğinde temel yardımcı araçlar, tüm programlara örtük olarak dahil edilen bir prelude modülüne konabilir
- Yerleşik diziler için
length() işlevi ve iterator arayüzü, string view türü, Python'daki range(n) benzeri sayısal aralıklar buna aday
-
String literal'ler
- String literal henüz yok ve ne anlama gelmesi gerektiğine henüz karar verilmiş değil
- Plan, prelude içinde değiştirilemez bir
string_view türü bulundurmak, string içeriğini çalıştırılabilir bellekte bir yere yerleştirmek ve literal'in kendisini bu belleği işaret eden bir string_viewa dönüştürmek
-
Standart kütüphane
- Modüller geldikten sonra standart bir kütüphane de gerekecek
- Dahil edilmesi istenen kapsam; vektör ve matrisleri içeren bir matematik kütüphanesi,
libc üzerinden bağlanan alloc/free tarzı bellek yönetimi, dinamik diziler, dinamik string'ler ve biçimlendirme, hash tabloları, konsol ve dosya G/Ç, dosya sistemi yardımcıları, zaman ve saat yardımcıları ile ağ iletişimi
Mevcut öncelik
- Planlanan özelliklerin ne zaman uygulanacağı ya da bu dilin gerçekten oyun modlama veya başka amaçlar için kullanılıp kullanılmayacağı henüz belli değil
- Aynı anda birden fazla iddialı projeyi ciddi biçimde yürütmenin iyi bir fikir olmadığı düşünülüyor; bu yüzden mevcut öncelik hâlâ oyun geliştirme
- Bir oyun yapılmadan o oyunun modlanamayacağı için, dil üzerindeki çalışma şimdilik yalnızca istenildiğinde ilerletiliyor
1 yorum
Lobste.rs görüşleri
Buradaki yorumlar, bu topluluktan beklediğimden çok daha acımasız geliyor.
Lua gibi başka bir dil de gayet yeterli olmuş olabilir. Yazarın devasa bir yak shaving sürecine kapılmış olma ihtimali de var.
Yine de çok yetenekli olduğu ve bundan epey keyif aldığı açık; yazının içinde ilginç teknik içerik de var.
Bir oyun motoru için bir script dili daha tasarlayan meslektaş bir nerd’ün yazısıysa bunu memnuniyetle okurum. Vibecoding ile üretilmiş SaaS çöpünün dünyayı kurtarıp yazarı zengin ettiğini anlatan yapay zeka üretimi bir zırvadan bir tane bile kaçabiliyorsam, böyle yazılardan günde bin tane okuyabilirim.
“Lua veya başka bir JIT derlemeli script dili standart seçimdir ama sandbox yapmak gerçekten zordur” iddiasını anlamak gerçekten güç.
Lua’da sandbox yapmak kolaydır; bu, en büyük artılarından biridir ve mod ya da eklentilerin ötesinde de büyük avantaj sağlar. Benim gördüğüm hiçbir dil buna yaklaşamadı.
Lua sürüm sorunları bir yere kadar geçerli, ama insanların buna çok öfkelendiğini pek görmedim. “Modern” Lua’yı bir amaçla kullanıp da başka bir iş yüzünden 5.1/5.2’ye geri dönmek zorunda kalmadıkça, çoğu kişi sanki yalnızca birini kullanıyor gibi.
Daha çok “kendi dilimi yapmak istiyorum” isteğini gerekçelendirmek için yapılmış bir araştırma hissi veriyor. Bu başlı başına sorun değil ama mevcut seçenekler hakkında düpedüz yanlış iddialar öne sürmek yerine dürüst olmak daha iyi.
Eğer sanal makine tasarımı ya da daha düşük seviyeli kısımlar ilginizi çekiyorsa, yazıda anlatılan yaklaşım elbette mümkün. Ama dil tasarımını öğrenmenin en iyi yolu olmaktan epey uzak.
En kolay örnek bytecode kaçışı. Bunun varlığını biliyorsanız devre dışı bırakabilirsiniz ama bunun tekrar tekrar ortaya çıkması daha geniş bir soruna işaret ediyor. Sandbox kurallarını, Lua belirtiminin birbirinden uzak parçalarının nasıl etkileştiğini anlayarak bir araya getirmeniz gerekiyor; yapı, hangi ek etkileşimlere izin verdiği açık olan temel bileşenlerle programları güvenli biçimde birleştirmeye uygun değil.
Daha zorlama bir örnek ise aynı Lua VM içindeki farklı ortamlar arasında yaşanan prototype pollution. Redis’te string’in metatable’ı kirletilebiliyordu; bu da Lua işlevlerini kullanan başka veritabanı kullanıcılarının yetkileriyle kod çalıştırmayı mümkün kılıyordu. Lua’nın prototype pollution yüzeyi JavaScript gibi dillere göre astronomik ölçüde daha küçük, ama küresel prototype sayısı kabaca 2 iken bunlardan biriyle yine aynı şeyin yapılabilmesi komik.
Yine de Luau’nun bu soruna epey yetkin bir çözümü var ve yazarın yeni bir sandbox yaparsa neden aynı sorunların hepsinden örtük biçimde kaçınabileceğini düşündüğünü pek anlayamıyorum.
“Benim oyunum çok ağır şekilde simülasyon odaklı. Özel bir ECS motoruyla yüz binlerce entity’yi simüle ediyorum. İdeal olarak modlama dili, birkaç component pointer alıp C’deki bir
fordöngüsü gibi bunların üzerinde gezebilmeli” kısmı daha iyi ideallere sahip olabilir.Özellikle Unity, Unreal, Blender ve Godot gibi render motorlarının bu sorunu nasıl ele aldığını karşılaştırmaya değer. Dış döngüleme, saniyede megapiksel düzeyini konuşmak için yeterince hızlı değil ve saniyede yüz binlerce entity için de uygun olmayabilir. Burada paralellik düşünmek gerekiyor.
Büyük motorların hepsi GPU dostu ve genelde utandırıcı derecede paralelleştirilebilir, dalsız algoritmaların dataflow tanımlarını kullanıyor. Yazar görsel editörlerden hoşlanmıyor olabilir; bu düşünce de yaygın. Ama bu, cevabın
fordöngüsü olduğu anlamına gelmiyor.Eğer yazar ECS’nin özünde ilişkisel bir paradigma olduğunu ve karşılaştırılması gereken, tarihsel yükü ağır dilin SQL olduğunu söyleseydi belki daha hoşgörülü olurdum.