Goose Membuktikan Kode Sistem Tidak Butuh Heap
Goose adalah bahasa sistem aman memori tanpa heap, GC, dan anotasi lifetime. Apa yang diajarkan desain stack-only-nya tentang Rust dan region.

Ini klaim yang bakal bikin kesal setidaknya dua komunitas bahasa pemrograman: borrow checker itu sebenarnya jalan memutar. Bukan kesalahan — model kepemilikan Rust memecahkan masalah nyata dan mendorong industri secara keseluruhan — tapi jalan memutar melewati ide lama yang lebih sederhana dan sekarang sedang kita temukan lagi. Ide itu adalah region: memori yang dialokasikan di arena besar bergaya stack, lalu dibebaskan sekaligus saat scope berakhir. Bahasa pemrograman Goose, bahasa sistem baru yang ramai dibicarakan minggu ini, adalah taruhan paling agresif di ide tersebut yang pernah saya lihat, dan desainnya memaksa kita menjawab pertanyaan yang kurang nyaman: seberapa banyak hal yang kita tanggung di C++ dan Rust itu benar-benar penopang, dan seberapa banyak sekadar ulah heap?
Goose tidak punya alokator heap. Tidak ada garbage collector, tidak ada reference counting, tidak ada destructor, tidak ada anotasi lifetime, tidak ada unsafe. Setiap nilai dinamis hidup secara inline di salah satu dari beberapa data stack yang ditentukan compiler secara statis. Pertumbuhan cukup menggeser pointer. Membebaskan struktur bersarang berisi satu juta elemen cuma satu penulisan ke puncak stack. Sang penulis melaporkan benchmark-nya lebih cepat dari C++ idiomatis (3,3x) dan Rust aman (1,12x) dengan memori lebih sedikit. Reaksi publik John Carmack menyoroti bagian yang paling menarik buat saya, yaitu referensi relatif yang membuat sebuah struktur data menjadi format file tersendiri. Entah Goose nanti dipakai untuk perangkat lunak produksi atau tidak, ini eksperimen pemikiran yang serius tentang bagaimana wujud bahasa sistem ketika heap dihapus dan penciptanya tidak minta maaf soal itu.
Model memori adalah inti dari seluruh bahasa
Sebagian besar fitur bahasa itu bisa dinegosiasikan. Sintaks, sistem modul, bahkan penanganan error: kamu bisa membayangkan dua bahasa masuk akal yang mengambil pilihan berlawanan. Model memori tidak begitu. Begitu memilih satu, semua yang di bawahnya ikut terpaksa: struktur data, cerita konkurensi, FFI, sampai pesan error. Goose memahami ini dan benar-benar mengambil jalan itu sampai habis.
Aturannya sederhana: program punya native call stack, data statis, dan N data stack, di mana compiler yang menentukan N. Paling banyak satu nilai yang bisa diubah ukurannya aktif per stack, dan nilai itu selalu di puncak. Invarian tunggal ini memberi keuntungan yang luar biasa. Karena nilai yang bisa tumbuh tidak pernah berpindah (tidak ada yang di atasnya perlu dipindahkan), referensi ke array yang sedang tumbuh tetap valid selamanya. Di C++, std::vector<T> yang dialokasi ulang membuat setiap pointer dan iterator ke dalamnya tidak valid, itulah sebabnya banyak programmer C++ pernah kena jebakan memegang referensi melewati push_back. Rust yang aman menghindarinya dengan membuatnya tidak bisa diekspresikan; kamu bakal pakai indeks u32 sebagai gantinya, dan membayarnya dengan bounds check serta kerepotan ergonomi. Goose cuma membiarkan kamu tetap memegang referensi. Aturan tipe dan lifetime dicek saat compile, dengan compiler menyimpulkan setiap referensi “berakar” pada apa dan menolak yang bisa bertahan lebih lama dari pemiliknya, tanpa anotasi, tanpa aturan aliasing, tanpa eksklusivitas.
struct Item { id: i32, weight: f32 }
var items: Item[>..] = [];
let first .= items.push(Item { 1, 0.5 });
for i: i32 in 2..1000001 {
items.push(Item { i, 0.0 });
}
first.weight = 99.5; // still valid, a million pushes later
Kalau kamu pernah berkutat dengan borrow checker Rust, ini terasa seperti trik sulap. Ke mana perginya lifetime? Jawaban jujurnya: masuk ke dalam batasan. Sistem kepemilikan Rust bisa membuktikan keamanan untuk pola berbagi yang arbitrer, seperti graf, siklus, dan struktur mutable yang dibagi lintas scope. Goose hanya membuktikan keamanan untuk himpunan pola yang jauh lebih sempit dan menolak sisanya begitu saja. Pertukaran antara ekspresivitas dan kesederhanaan ini adalah yang paling tua dalam desain bahasa pemrograman, dan inilah benang yang akan saya tarik di sini. Jika kamu pernah membaca tulisan kami tentang apa arti soundness sebenarnya di borrow checker Rust, Goose adalah sisi lain dari koin itu: soundness dicapai bukan dengan analisis yang lebih kuat, melainkan dengan bahasa yang lebih kecil.
Semua inline: tata letak data sebagai fitur kelas satu
Ide besar kedua adalah tidak ada satu tipe array tunggal. Goose punya keluarga jenis array yang hanya berbeda dalam apa yang terjadi pada ukurannya: tetap saat compile, dibekukan setelah konstruksi, bisa tumbuh dalam kapasitas inline, hanya-tumbuh, atau tumbuh-dan-menyusut. Semuanya berbentuk [metadata][elemen...]: bersebelahan, padat, tidak pernah berupa pointer ke blok elemen. String hanyalah array u8, sehingga struct bisa memuat bagian berukuran variabel yang duduk inline sesuai urutan deklarasi. Sebuah record dengan field string berpanjang variabel dan daftar sub-record berpanjang variabel adalah satu rangkaian byte tanpa indirection sama sekali.
Angka yang diklaim cukup mencolok: sebuah record order berukuran 29 byte dan nol alokasi di Goose, dibanding 160 byte dan satu alokasi sebagai std::string + std::vector<Item> di C++, atau 153 byte dan empat alokasi di Rust. Sekarang, satu koreksi yang adil langsung muncul dalam diskusi: contoh tutorial “vector of strings” milik Goose sendiri mengabaikan small-string optimization, yang diterapkan setiap implementasi STL arus utama. String pendek di C++ tidak melakukan alokasi. Ini tidak meruntuhkan argumennya, sebab SSO membantu string di bawah sekitar 15–22 byte dan tidak berpengaruh untuk kasus umum data bersarang berukuran variabel. Tapi ini pengingat bahwa tabel benchmark di homepage sebuah bahasa adalah promosi, meskipun bahasanya bagus. Kekalahannya juga tercantum di sana, patut dipuji dari sang penulis, dan itu lebih dari yang dilakukan kebanyakan proyek.
Ada harga nyata, dan Goose tidak menyembunyikannya: array elemen berukuran variabel itu sekuensial. Kamu bisa melakukan iterasi, tapi tidak bisa mengindeksnya. Elemen ke-k berada di mana pun byte-nya jatuh. Jika butuh akses acak, kamu menyimpan array kedua berisi referensi sebagai indeks. Ini jenis pertukaran yang sudah dilakukan orang data-oriented design secara manual selama puluhan tahun (pada dasarnya begitulah cara kerja format kolumnar dan layout gaya flatbuffers), tapi di sini ini menjadi default, dan keluar darinya butuh kerja ekstra. Apakah itu fitur atau jaket sempit bergantung sepenuhnya pada pola aksesmu.
Referensi relatif: struktur datamu adalah format file-nya
Ide favorit saya di Goose adalah referensi relatif, dan inilah yang juga disorot Carmack. Referensi biasa adalah alamat mesin: delapan byte, absolut, tidak berarti di luar proses. Referensi relatif menyimpan tautan yang sama sebagai offset 1, 2, atau 4 byte, diukur dari field itu sendiri atau dari base sebuah pool bernama. Struktur yang dibangun dari tautan relatif-diri itu independen posisi, artinya sama di mana pun ia berada di memori. Jadi menyimpan struktur data cukup dengan write(), dan memuatnya cukup dengan read() ditambah satu tahap verifikasi.
Klausa terakhir itu lebih penting daripada kedengarannya. Versi naif dari trik ini, yaitu memetakan file penuh pointer ke memori (mmap), adalah bencana keamanan klasik. Versi Goose melakukan validasi: from_bytes memeriksa framing, setiap tag, setiap panjang, dan setiap tautan sebelum byte berubah menjadi nilai, sehingga file yang rusak atau berbahaya menghasilkan false, bukan pointer liar. Kamu mendapat kecepatan struktur ber-mmap dengan semacam keamanan seperti parser.
Framing Carmack tepat sekali: “‘Parsing’ data file dan jaringan sering kali 100x lebih lambat dari yang seharusnya.” Siapa pun yang pernah profiling jalur panas deserialisasi paham ini. Jawaban industri sejauh ini adalah format zero-copy berbasis skema seperti Cap'n Proto dan FlatBuffers, yang berhasil tapi menempelkan compiler skema dan library runtime ke proyekmu. Jawaban Goose: layout di memori dan layout di kabel adalah hal yang sama secara konstruksi, karena bahasa ini memang tidak pernah punya pointer ke alamat absolut sejak awal. Contoh binary search tree hampir nakal: node 12 byte, termasuk tautannya, insert cuma satu push dan satu store, dan seluruh pohon bisa dipersistenkan dengan menulis byte-nya keluar. Para penggemar Lisp akan mengenali gerakan filosofisnya, yaitu meruntuhkan perbedaan yang semua orang anggap fundamental. Ini bagian dari mengapa Lisp masih penting sebagai titik acuan desain enam puluh tahun kemudian.
Kita pernah di sini: region, arena, dan ML Kit
Respons yang paham sejarah terhadap Goose muncul tepat waktu di kolom komentar: “Saya ingat orang-orang ML pernah menempuh jalur ini. Saya rasa mereka menyebutnya region.” Benar. Manajemen memori berbasis region berasal dari karya ML Kit di tahun 1990-an. Region inference dari Tofte dan Talpin memungkinkan compiler ML menggantikan garbage collection dengan region bergaya stack yang ditentukan secara statis, mengalokasikan dan membebaskan region utuh tanpa pembukuan per objek. Vonis sejarah yang jujur adalah bahwa ide ini dianggap semacam jalan buntu: region inference berhasil, tapi program nyata menunjukkan pertumbuhan memori yang patologis, yaitu region yang menahan data mati terlalu lama, sehingga komunitas riset sebagian besar berpindah. Garbage collector mempertahankan mahkotanya.
Tapi ide ini tidak pernah benar-benar mati; ia hanya berganti pekerjaan. Alokasi arena adalah praktik standar di game engine, compiler, dan server berperforma tinggi. Memory pool Apache, arena per-request di web server, dan pola “reset scratch buffer setiap frame” di game loop semuanya adalah region dengan nama lain, dikelola manual karena bahasanya tidak membantu. Yang dilakukan Goose berbeda dari ML Kit perlu dinyatakan dengan tepat: alih-alih menyimpulkan region untuk bahasa fungsional serbaguna (sulit dan tidak bisa diprediksi), Goose mengekspos model stack secara langsung dan menjadikan maksud programmer bagian dari tipe. T[>..] berarti “ini tumbuh, referensi tetap valid.” Tugas compiler adalah memeriksa disiplin yang dideklarasikan, bukan menemukannya sendiri. Itu masalah yang jauh lebih mudah, dan itulah alasan yang sama mengapa Rust berhasil di tempat skema lifetime otomatis sebelumnya kesulitan: programmer mau menerima kontrak yang bisa mereka lihat.
Ada pelajaran yang lebih luas tentang bagaimana bahasa produksi menyerap riset. Region inference sebagai pengganti GC yang sepenuhnya otomatis: gagal. Region sebagai pola sistem manual: ada di mana-mana. Borrow checking: sukses besar yang juga banyak meminjam dari ide region dan linear types. Proses share-nothing bergaya Erlang: terbukti benar, dan thread Goose secara harfiah adalah itu, yaitu worker yang dikompilasi sebagai program terpisah dengan memori sendiri, nilai menyeberang lewat antrean bertipe sebagai memcpy, dan data race tidak bisa direpresentasikan. Tidak ada yang langsung memahami gambaran utuhnya di percobaan pertama. Ide beredar selama puluhan tahun sebelum mendarat dalam bentuk yang benar-benar berfungsi.
Di mana model ini mulai goyah
Saya bilang akan membuka dengan klaim kontrarian lalu mengakui di mana ia gagal. Inilah pengakuannya. Borrow checker bukan jalan memutar jika pola berbagi programmu benar-benar arbitrer, dan kelas program yang sangat besar memang berbentuk seperti itu. Pertanyaan paling tajam dalam diskusi Goose juga yang paling sederhana: bagaimana kamu membangun hash table yang memetakan kunci ke string, ketika program menyisipkan dan menghapus string acak pada waktu acak? Penghapusan adalah pembunuhnya. Stack yang hanya-tumbuh tidak bisa mengambil kembali lubang di tengah. Solusi yang disarankan, seperti satu stack per size class, kompaksi berkala, atau pool dengan freelist di mana “membebaskan” slot hanya berarti slot itu boleh dipakai lagi, semuanya bekerja, dan desain pool-nya memang cerdas (slot yang basi membaca nilai lain dengan tipe yang benar, bukan merusak memori). Tapi masing-masing adalah programmer yang mengimplementasikan ulang sebagian memory manager, secara manual, untuk struktur data itu.
Demikian pula soal penggunaan memori. Tanpa GC, stack yang tumbuh menahan semua yang pernah ia tampung sampai scope berakhir. Program yang berjalan lama dengan lifetime tidak teratur, misalnya pertanyaan dokumen JSON yang diangkat dalam thread, cache, dan state sesi, entah cocok dengan pola berdisiplin scope atau tidak. Ketika tidak cocok, tidak ada elegansi yang menyelamatkanmu. Kegagalan ML Kit (data mati yang tertahan di region hidup) tidak hilang di Goose; ia menjadi terlihat dan manual, yang bisa dibilang lebih baik, tapi tetap ada. Seorang komentator merumuskan sikap yang tepat: Goose adalah bahasa untuk tujuan yang terbatas, dan itu tidak apa-apa. Beban kerja berorientasi data, compiler, sistem entitas game, apa pun yang berbentuk pipeline (baca, transformasi, keluarkan, reset) sangat cocok. Graf objek mutable berumur panjang tidak.
Dan satu catatan lagi tentang angka utama. Suite benchmark yang dijalankan sendiri adalah genre dengan mode kegagalan yang sudah dikenal: kamu membandingkan Goose terbaikmu dengan C++ “idiomatis” milik orang lain, dan idiom adalah tempat pertengkaran dimulai. Kesalahan SSO menunjukkan kode perbandingannya tidak di-tuning secara adversarial. Angka 1,12x atas Rust aman lebih menarik daripada 3,3x justru karena kecil. Ia menyatakan bahwa model memori menang di layout dan penghapusan alokasi, bukan karena sihir, dan versi Rust yang di-tuning dengan arena dan indeks akan menutup sebagian besar selisihnya. Itulah intinya: kamu bisa menulis Rust berbasis arena, tapi bahasanya membuatnya canggung, dan kecanggungan itulah yang menentukan kode apa yang benar-benar ditulis.

Apa yang akan benar-benar dicuri bahasa produksi dari Goose
Hampir tidak ada yang akan menulis Goose untuk produksi dalam beberapa tahun ke depan. Bahasa satu orang yang dikompilasi lewat satu backend C tidak akan sampai ke sana dengan cepat, dan sang penulis pun mengakui statusnya. Itu bukan pertanyaan yang menarik. Yang menarik adalah ide mana yang lolos ke bahasa yang sudah kita pakai, karena begitulah cara ini selalu bekerja. Prediksi saya, diurutkan berdasarkan tingkat keyakinan:
- Struktur yang independen posisi dengan pemuatan terverifikasi. Serialisasi zero-copy dengan tahap keamanan terlalu bagus untuk tetap jadi ceruk. Harapkan versi yang terintegrasi dengan bahasa dari apa yang FlatBuffers lakukan dengan canggung, terutama saat layanan yang berat data terus menghadapi biaya parsing.
- Array sebagai keluarga, bukan satu tipe. “Apa yang terjadi pada ukurannya?” adalah pertanyaan pengorganisasi yang lebih baik daripada “stack atau heap?”. Tetap, dibekukan, dibatasi kapasitas, hanya-tumbuh: perbedaan ini sudah ada di setiap codebase sebagai konvensi informal. Menjadikannya tipe itu murah dan menangkap bug nyata.
- Nilai fungsi saat compile. Goose memperlakukan nilai fungsi sebagai entitas compile-time, sehingga
xs.filter() { it > 0 }dikompilasi menjadi loop seperti yang terlihat. Template C++ dan monomorfisasi Rust sudah melakukan ini; bedanya Goose menjadikannya satu-satunya cara, sehingga jebakan function pointer saat runtime hilang sepenuhnya. - Return multi-frame yang dicek secara statis.
return err from loadkembali dari fungsi N frame ke atas, dicek saat compile, tanpa unwinder, tanpa piramidaResult, tanpa?di setiap pemanggilan. Sistem efek sedang berputar di sekitar ide yang sama dari arah lain. Salah satunya akan masuk ke bahasa arus utama dekade ini.
Model threading share-nothing layak disebut juga, terutama karena ini yang paling tidak baru dan paling terbukti: ini proses Erlang, ini isolate Dart, ini model actor tanpa ideologinya. Fakta bahwa ia terus diciptakan ulang menunjukkan konsensus industri tentang state mutable bersama sudah mapan dalam teori tapi diabaikan dalam praktik. Goose setidaknya membuat pengabaian itu tidak mungkin.
Argumen sebenarnya: kesederhanaan lewat pembatasan
Mundur selangkah dan desain Goose adalah satu gerakan yang diulang: perkecil himpunan program yang bisa direpresentasikan sampai keamanan menjadi murah untuk dibuktikan. Tanpa heap berarti tidak ada pointer heap yang menggantung. Stack yang hanya-tumbuh berarti referensi tidak bisa tidak valid. Thread worker terpisah berarti race tidak bisa ada. Konstruksi tanpa salinan berarti tidak ada temporary yang tidak disengaja. Setiap pembatasan menghapus satu kategori bug dan satu kategori mesin bahasa, seperti aturan aliasing borrow checker, destructor, dan unwinder, yang ada untuk mengelolanya.
Ini argumen “less is exponentially more” dari Rob Pike tanpa sarung tangan, dan ia menyentuh ketegangan yang sama dengan Go milik Pike: pembatasan terasa seperti pembebasan sampai programmu sendiri yang dikecualikan oleh pembatasan itu, lalu terasa seperti tembok. Go mengecualikan generics selama satu dekade dan tetap bertahan; ia juga mengecualikan banyak program yang benar. Goose mengecualikan jauh lebih banyak. Alasan tetap memperhatikannya adalah aturan lama Pike tentang kesederhanaan justru menua dengan baik karena anggaran kompleksitas itu nyata. Setiap jaminan yang diberikan bahasa secara otomatis adalah jaminan yang tidak perlu lagi dipegang di kepala setiap programmer yang memakainya.
Jadi, apakah borrow checker itu jalan memutar? Untuk pemrograman sistem serbaguna, tidak. Ekspresivitas Rust benar-benar dibutuhkan untuk kasus berbagi arbitrer, dan keberhasilannya memberi industri kosakata untuk membicarakan kepemilikan sama sekali. Tapi bagaimana dengan mayoritas besar kode sistem yang tenang, yang berbentuk pipeline, berbentuk arena, berbentuk request? Goose adalah bukti bahwa kita mungkin bisa punya keamanan memori, kecepatan, dan binary kecil dengan sebagian kecil seremoni, jika kita mau berkata “tidak” pada lebih banyak program. Desain bahasa adalah seni memilih program mana yang dilarang. Goose melarang program yang berbeda dari yang biasa kita kenal, dan hasilnya adalah sketsa bahasa sistem paling menarik yang pernah saya baca dalam beberapa tahun. Saya tidak berharap akan mengirimkannya ke produksi. Saya berharap akan terus mencuri darinya.
Taruhan Goose adalah bahwa sebuah bahasa bisa melarang lebih banyak program daripada Rust dan tetap berguna. Benchmark-nya menunjukkan ini layak dipertimbangkan serius, termasuk kekalahannya.


