j JawaRustGo++ dokumentasi
jrgc 0.1.0 Interpreter jadi acuan. VM dan LLVM diuji terhadapnya.
Bahasa eksperimental · ditulis dengan Rust · versi 0.1.0

JawaRustGo++

JRG adalah bahasa pemrograman yang program tulisan Anda bisa dijalankan tiga cara: langsung (paling gampang buat coba-coba), lewat mesin virtual (lebih cepat), atau dikompilasi jadi program native (paling cepat). Satu kode, tiga cara jalan, hasil yang sama persis.

Yang membedakannya bukan jumlah mesinnya, melainkan cara ketiganya dijaga tetap sama. Ratusan program yang sama dijalankan di dua mesin sekaligus, lalu keluarannya dibandingkan huruf per huruf — kalau berbeda sedikit saja, itu dihitung sebagai kerusakan yang harus diperbaiki, bukan sebagai “ya beda implementasi”.

halo.jrg
package main

pub fn main() {
    let nama = "Nusantara"
    wok("""sugeng rawuh, ${nama}!""")
}
3mesin eksekusi
471tes, 0 gagal
18,3 KBbiner native, di-strip

JRG belum dipaket dan belum punya installer, jadi cara memakainya hari ini adalah mengkompilasinya sendiri. Yang dibutuhkan cuma Rust; kalau mau mencoba backend native, tambahkan clang. Hasilnya dua program: jrgc (menjalankan dan mengkompilasi berkas .jrg) dan jrgm (mengurus proyek berisi banyak berkas).

Kodenya ada di github.com/Kodok-Kejepit/jrg. Salin, bangun, lalu jalankan berkas pertama:

git clone https://github.com/Kodok-Kejepit/jrg
cd jrg
cargo build --release

# berkas paling pendek yang bisa ditulis — tanpa `main`, tanpa `package`
echo 'wok("halo")' > halo.jrg
./target/release/jrgc halo.jrg
# butuh Rust toolchain
cargo build --release

# jalankan satu berkas — default: tree-walking interpreter
./target/release/jrgc halo.jrg

# mesin lain
./target/release/jrgc halo.jrg --vm
./target/release/jrgc halo.jrg --emit-llvm
i
Backend LLVM butuh clang

Ia menghasilkan LLVM IR lalu menyerahkannya ke clang, dan runtime native-nya dibangun sekali lewat Rust toolchain. Kalau clang tidak ada, dua mesin lain tetap jalan penuh.

Berkas tanpa package: skrip

Program terkecil yang bisa ditulis dulu butuh dua baris upacara sebelum satu baris kerja pun ada. Sekarang sebuah berkas tanpa package dijalankan sebagai skrip: pernyataan telanjang jadi isi main yang dibuatkan, deklarasi tetap di tingkat atas.

// halo.jrg — tanpa `package`, tanpa `main` yang ditulis sendiri
wok("halo dari JRG")

// Deklarasi tetap ter-hoist, jadi `sapa` boleh dipanggil lebih dulu.
wok(sapa("Nusantara"))

fn sapa(nama) {
    return «sugeng rawuh, ${nama}!»
}

// `let` justru TIDAK ter-hoist di sini, dan itu disengaja: sebuah
// skrip dibaca dari atas ke bawah, jadi urutannya berlaku.
let angka = [5, 3, 8, 1]
wok("terurut:", arr::sort(angka))

struct Titik { pub x: int, pub y: int }
impl Titik {
    fn jarak_kuadrat(self) -> int { return self.x * self.x + self.y * self.y }
}
wok("jarak kuadrat:", Titik{x: 3, y: 4}.jarak_kuadrat())
deklarasi ter-hoist, let tidakdan keduanya keputusan, bukan warisan bentuk

fn/struct/enum/trait/impl tetap item tingkat atas, jadi tetap boleh dipakai sebelum barisnya — sifat yang sudah dimiliki JRG.

let/var/const justru jadi pernyataan. Di berkas ber-package keduanya item dan karena itu ter-hoist, artinya wok(x) sebelum let x = 5 akan jalan. Di sebuah skrip yang dibaca dari atas ke bawah itu mengejutkan tanpa menguntungkan siapa pun.

tidak ada berkas yang berubah artiprogram lama tetap berarti sama

Nol dari 57 berkas .jrg di pohon ini tanpa package. Jadi yang berubah cuma program yang tadinya ditolak menjadi diterima — arah yang sama dengan argumen keamanan ASI ketika sebuah baris boleh diawali operator.

salah ketik package tetap dituduh packagebukan diperlakukan skrip

Tanpa penjagaan itu, pakage main masuk mode skrip dan gagal jauh belakangan dengan pesan yang tidak menyebut package sama sekali — sebuah berkas modul yang salah ketik satu huruf mengeluh tentang hal lain. Ambangnya jarak sunting ≤ 2, definisi yang sama dengan saran nama di seluruh bahasa ini.

fn main() plus pernyataan telanjang ditolakdua badan untuk satu program

Memilih salah satunya diam-diam berarti separuh berkasnya tidak pernah jalan dan tidak ada yang mengatakannya. Kalau tidak ada pernyataan telanjang, tidak ada main yang dibuatkan — jadi fn main() { … } tanpa package juga jalan.

REPL: mencoba tanpa membuat berkas

jrgc tanpa argumen membuka REPL. Keadaannya menetap antar masukan, sebuah ekspresi mencetak nilainya sendiri, blok yang belum tertutup dilanjutkan otomatis, dan sebuah galat tidak mengakhiri sesi — orang mencoba sesuatu justru pada saat ia belum yakin, jadi REPL yang mati pada galat pertama lebih buruk daripada tidak ada REPL sama sekali.

$ jrgc
jrg> let x = 10
jrg> x * 3
30
jrg> struct P { pub x: int, pub y: int }
jrg> impl P { fn total(self) -> int { return self.x + self.y } }
jrg> P{x:3, y:4}.total()
7
jrg> fn faktorial(n) {
  ...   if n <= 1 { return 1 }
  ...   return n * faktorial(n - 1)
  ... }
jrg> faktorial(10)
3628800
jrg> :cari sort
arr::sort
arr::sort_by
arr::sort_by_key
arr::sort_desc
wokwok::argsort
jrg> :doc arr::sort
arr::sort(a) -> array
  Salinan terurut menaik; `a` sendiri tidak berubah.
jrg> :simpan coba.jrg
ditulis ke coba.jrg
:simpan menulis sesi jadi program yang jalandan kesetaraannya diperiksa, bukan dijanjikan

Deklarasi ditulis di top level, pernyataan di dalam main, dan sebuah ekspresi telanjang dibungkus wok(...) — karena REPL mencetak nilai sebuah ekspresi sedangkan berkas tidak, jadi tanpa itu berkas hasilnya akan jalan tapi diam.

Sebuah tes menjalankan satu sesi, menyimpannya, menjalankan berkas hasilnya, lalu membandingkan keluarannya byte per byte di kedua mesin. Itu satu-satunya alasan seseorang boleh belajar di REPL: apa yang ia kerjakan di sini berarti hal yang sama di dalam berkas.

:cari / :doc / :daftarmenemukan 658 builtin dari dalam bahasa

Python punya dir() dan help(). Sebelum ini JRG cuma punya halaman HTML yang harus dibuka di browser dan dibaca dari atas — bukan jawaban untuk seseorang yang sedang mengetik dan lupa nama fungsinya.

Ketiganya juga ada di CLI: jrgc --cari <kata> dan jrgc --doc <nama>, dari satu definisi. Daftar namanya diturunkan dari runtime, bukan dirawat tangan, jadi sebuah builtin baru bisa dicari pada hari ia didaftarkan.

Yang dirawat tangan cuma penjelasannya, dan itu tidak bisa berbohong ke arah yang berbahaya: nama yang belum dijelaskan tetap muncul di pencarian lengkap dengan aritasnya dan status “belum ada penjelasan” — bukan “tidak dikenal”, yang akan jadi kebohongan tentang nama yang jelas ada.

:doc juga menjawab untuk sintaks, bukan cuma builtin: :doc for, :doc ??, :doc """. Tabelnya dipisah dari tabel builtin karena tes yang menjaga tabel builtin menuntut tiap entri benar-benar terdaftar di runtime, dan sebuah kata kunci tidak akan pernah ada di sana — menggabungkannya akan memaksa tes itu dilonggarkan, dan tes yang dilonggarkan supaya sesuatu muat berhenti menjaga apa yang seharusnya ia jaga.

di REPL sebuah nama boleh didefinisikan ulangdi dalam berkas tidak

Mengetik ulang sebuah baris adalah kelakuan paling wajar yang ada di REPL, sementara nama top-level ganda ditolak di dalam berkas. Itu satu-satunya perbedaan, dan ia hilang sendiri di keluaran :simpan: sesi cuma menyimpan deklarasi terakhir per nama, jadi berkas hasilnya tidak pernah memuat nama ganda.

belum ada riwayat panah-atasdan itu disebutkan di :bantuan

REPL ini tidak menautkan pustaka penyuntingan baris apa pun, karena itu akan menukar sifat yang sudah dibayar sekali di jembatan Python: satu biner yang berperilaku sama di Termux arm64 dan Kali x86_64. rlwrap jrgc memberikannya kalau rlwrap terpasang.

Proyek, bukan cuma satu berkas

jrgm mengurus proyek berisi banyak berkas: manifest jrgm.toml, entry point src/main.jrg, dan resolusi import antar-berkas.

jrgm new halo-nusantara
cd halo-nusantara
jrgm run

jrgm run --vm          # pakai bytecode VM
jrgm run --watch       # jalankan ulang tiap berkas berubah
jrgm exec sekali.jrg   # satu berkas, tanpa proyek

jrgm fmt               # rapikan tiap .jrg di src/
jrgm fmt --check       # keluar tidak-nol kalau ada yang belum rapi
jrgm check             # cek sintaks entry point
jrgm build             # cek sintaks SEMUA berkas proyek + dependensinya

# dependensi lokal: bantu = { path = "../bantu" } di jrgm.toml
jrgm add bantu --path ../bantu
jrgm remove bantu
jrgm tree              # graf dependensi yang sebenarnya

# test: tests/*.jrg, fungsi ber-awalan test_ atau uji_, tanpa main
jrgm test
jrgm test --vm
jrgm test --filter tambah
jrgm test --timeout 10  # batas per berkas, detik

Lapisan web: middleware, cookie, sesi, template

Router, server dan pembangun respons sudah lama ada. Yang ditambahkan adalah empat hal yang membuat sebuah aplikasi bisa ditulis sampai selesai — dan yang justru paling gampang salah kalau tiap orang menuliskannya sendiri. Tiap keputusan yang menyangkut keamanan mengambil arah aman secara bawaan, karena bawaan yang salah tidak pernah terlihat sebagai kesalahan: ia terlihat sebagai program yang bekerja.

let r = dok::router_new()

// Middleware SEBELUM route dicari — juga sebelum berkas statis, supaya
// penjaga otentikasi tidak bisa dilewati lewat /static/.
//   nil            -> lanjut, request tidak berubah
//   sebuah request -> lanjut dengan itu (path/method dibaca ULANG)
//   apa pun lain   -> itulah responsnya, handler tidak dipanggil
dok::router_use(r, |req| {
    if str::starts_with(req.path, "/admin") {
        match dok::cookie_get(req, "sesi") {
            Some(c) => match dok::session_decode(c, KUNCI) {
                Ok(d)  => { return nil }
                Err(_) => { return dok::response(401, "sesi tidak sah") }
            }
            None => { return dok::response(401, "perlu masuk dulu") }
        }
    }
    return nil
})

// Middleware SESUDAH handler: menerima (req, resp), mengembalikan respons.
dok::router_after(r, |req, resp| dok::header_set(resp, "X-Frame-Options", "DENY"))

// Cookie: HttpOnly + SameSite=Lax secara bawaan.
dok::cookie_set(resp, "sesi", s, {"max_age": 3600})

// Sesi: DITANDATANGANI, bukan dienkripsi — isinya terbaca siapa pun,
// yang dijamin cuma bahwa ia tidak bisa DIUBAH tanpa rahasianya.
dok::session_encode({"user": "budi"}, KUNCI)
dok::session_decode(cookie, KUNCI)   // tanda tangan dicek SEBELUM diurai

// Sandi: PBKDF2-HMAC-SHA256, garam acak per sandi, putaran disimpan
// DI DALAM hash-nya supaya bisa dinaikkan tanpa membatalkan yang lama.
dok::password_hash("rahasia")
dok::password_verify("rahasia", hash)

// Template: {{ x }} MELOLOSKAN HTML, {{& x }} tidak — dan yang tidak
// aman harus ditulis eksplisit.
dok::render("halo {{nama}}{{#if ada}} ({{jml}}){{/if}}", data)

js:: — JavaScript lewat Node

Bentuknya sama persis dengan py::, karena mesin worker-nya memang satu. Sebuah Promise di-await otomatis — itulah yang membuat fetch, fs/promises dan driver basis data bisa dipakai apa adanya.

Harganya disebut terus terang: sebuah fungsi JRG yang diserahkan ke JavaScript menjadi fungsi async. Ia bekerja dengan apa pun yang menunggu, dan tidak dengan API yang memanggilnya lalu langsung memakai hasilnya — [1,2].map(f) menghasilkan array berisi Promise. Itu terlihat apa adanya, bukan hasil salah yang diam.

js::eval("[1,2,3].map(x => x * 2)")     // Ok([2, 4, 6])
js::eval("Promise.resolve(42)")          // Ok(42) — di-await
js::eval("1/0")                          // Ok(inf), BUKAN nil

js::import("path")
js::call("path", "join", "a", "b")       // Ok("a/b")

// Nilai tanpa padanan di JRG datang sebagai HANDLE, bukan sebagai
// teks yang tampak seperti data.
match js::eval("Buffer.from('halo')") {
    Ok(b) => { wok(typeof(b)); wok(b.toString("hex")) }   // js:Buffer
    Err(e) => wok(e)
}

// Fungsi JRG diserahkan ke JavaScript.
js::exec("globalThis.pakai = async (f) => (await f(3)) + (await f(4))")
js::method(o, "pakai", |x| x * 10)       // Ok(70)

WebSocket, di server yang sama

Tidak ada mesin baru untuk ini. dok::serve sejak dulu menangani tiap koneksi di thread-nya sendiri, dan sebuah WebSocket persis itu: satu koneksi yang berumur panjang. Yang berubah cuma satu percabangan — kalau ada route WS yang cocok dan header-nya meminta upgrade, handshake-nya dijawab dan soketnya diserahkan ke handler alih-alih ditutup sesudah satu respons. Route HTTP biasa tetap hidup di router yang sama.

let r = dok::router_new()

// Middleware `sebelum` JUGA menutupi endpoint WebSocket — upgrade-nya
// diperiksa sesudah itu, karena endpoint WS justru yang paling sering
// lupa dijaga. Tapi sebelum routing biasa: sesudah handshake selesai,
// tidak ada lagi respons HTTP yang bisa ditulis di koneksi itu.
dok::router_use(r, |req| { dok::log_info(req.method + " " + req.path); return nil })

dok::router_get(r, "/", |req| dok::html("<h1>halo</h1>"))

// Handler WS menerima (koneksi, request); soketnya terbuka selama handler
// berjalan. `ws_recv` menjawab `None` HANYA untuk "ditutup", tidak pernah
// untuk "belum ada" — jadi loop ini berhenti tepat waktu alih-alih
// berputar terus. Ping dijawab pong otomatis, tidak sampai ke sini.
dok::router_ws(r, "/gema", |c, req| {
    var n = 0
    while true {
        match dok::ws_recv(c) {
            Some(m) => { n = n + 1; dok::ws_send(c, "gema #" + n as string + ": " + m) }
            None    => { break }
        }
    }
    dok::log_info("putus sesudah " + n as string + " pesan")
})

dok::serve(r, 7099)
panjang frame datang dari jaringanjadi dibatasi sebelum sebyte pun dialokasikan

Header panjang 64-bit adalah delapan byte yang boleh mengatakan apa saja. Tanpa batas, siapa pun yang bisa membuka soket bisa meminta delapan exabyte. Batasnya 16 MiB, dan sengaja lebih ketat daripada batas yang dipakai untuk ukuran yang datang dari program sendiri: di sini angkanya datang dari luar, dan yang membayar batas longgar adalah servernya, bukan penulisnya.

byte yang belum lengkap bukan galatia cuma belum lengkap

Sebuah pesan bisa terbelah dua segmen TCP. Menyamakan “belum lengkap” dengan “rusak” berarti memutus koneksi yang sehat setiap kali kebetulan itu terjadi.

Basis data: SQLite

Lewat dlopen, bukan lewat crate. Menautkan sebuah crate akan menukar sifat yang sudah dibayar sekali di jembatan Python: satu biner yang berperilaku sama di Termux arm64 dan di Kali x86_64. libsqlite3 ada di hampir semua sistem, dan sistem yang tidak punya diberi kalimat yang menyebut nama paketnya per distribusi — bukan kegagalan yang tidak menjelaskan apa pun. dok::db_tersedia() ada supaya sebuah program bisa memilih jalur lain, alih-alih harus menebaknya dari teks galat.

// Tanya dulu, jangan tebak dari teks galat.
if !dok::db_tersedia() { wok("libsqlite3 tidak ada di mesin ini"); return }

let db = dok::db_open(":memory:")?
dok::db_exec(db, "CREATE TABLE murid(id INTEGER PRIMARY KEY, nama TEXT, nilai REAL)")?

// Parameter DIIKAT dengan `?`. Tidak ada fungsi yang merakit SQL dari
// sebuah string, dan itu disengaja: begitu ada, ia jadi jalur terpendek.
dok::db_exec(db, "INSERT INTO murid(nama, nilai) VALUES (?, ?)", ["budi", 8.5])?
dok::db_exec(db, "INSERT INTO murid(nama, nilai) VALUES (?, ?)", ["ani", 9.25])?
wok("id terakhir:", dok::db_last_id(db)?)          // id terakhir: 2

// Satu baris adalah DICT, bukan array: posisi kolom berubah saat
// `SELECT *` bertemu tabel yang berubah, sedangkan namanya tidak.
let semua = dok::db_query(db, "SELECT * FROM murid ORDER BY nilai DESC")?
for _, m in semua { wok(str::format("  {} {:.2}", m.nama, m.nilai)) }
//   ani 9.25
//   budi 8.50

// Percobaan injeksi terikat sebagai NILAI, bukan sebagai perintah.
let jahat = "budi'; DROP TABLE murid; --"
wok("hasil cari:", dok::db_query(db, "SELECT * FROM murid WHERE nama = ?", [jahat])?)
let sisa = dok::db_query(db, "SELECT count(*) AS n FROM murid")?
wok("tabel utuh, baris:", sisa[0].n)               // hasil cari: []  /  2

dok::db_close(db)
tidak ada fungsi yang merakit SQL dari stringdan itu keputusan, bukan kelalaian

Begitu fungsi seperti itu ada, ia jadi jalur terpendek — dan injeksi SQL bukan bug yang terlihat pada saat sedang ditulis. Siapa pun yang memang mau merangkai SQL tetap bisa, tapi ia harus menuliskannya sendiri dan melihat apa yang sedang ia lakukan.

bilangan yang tidak muat DITOLAK, bukan dipotongid yang berubah diam-diam adalah baris yang salah

INTEGER milik SQLite 64-bit, sedangkan bilangan bulat JRG 128-bit. Sebuah nilai yang tidak muat menghasilkan galat yang menyebutkan sebabnya. Dipotong diam-diam, ia bukan galat melainkan baris lain.

BLOB boleh memuat NULpanjangnya dibaca, bukan ditebak

Panjangnya diambil dari column_bytes, bukan dari strlen. Memotong di NUL pertama adalah data yang hilang tanpa ada yang melaporkannya.

01
Mulai dari interpreter

Paling cepat memberi umpan balik, dan ia yang jadi acuan semantik. Kalau sebuah program benar di sini, ia benar.

02
Pindah ke VM kalau perlu cepat

Bahasanya utuh, kecuali dua namespace yang memang interpreter-saja.

03
LLVM untuk biner

Hasilnya kode native. Kesebelas berkas konformans jalan dengan keluaran identik, tapi beberapa fitur memang belum ada di sini — baca bagian batasan.

Baru pertama kali?istilah yang sering muncul

Halaman ini memakai beberapa istilah yang mungkin belum familier. Ini artinya, sesingkat mungkin:

  • Compiler / kompilator — program yang membaca kode Anda dan mengubahnya jadi sesuatu yang bisa dijalankan komputer.
  • Runtime — bagian yang menemani program Anda saat berjalan; misalnya yang mengurus memori dan pesan galat.
  • Backend / mesin — cara sebuah program dijalankan. JRG punya tiga, dan ketiganya harus memberi hasil yang sama.
  • Builtin — fungsi yang sudah tersedia tanpa perlu Anda tulis atau pasang, seperti wok untuk mencetak.
  • Namespace — pengelompokan nama, ditulis dengan titik dua ganda: str::upper berarti fungsi upper dari kelompok str.
  • Closure — fungsi kecil yang ditulis di tempat, dan masih ingat variabel di sekitarnya.
  • FFI — cara memanggil kode yang ditulis dalam bahasa lain (C, Python, Java, dan seterusnya) dari dalam JRG.
  • Diagnostik — pesan yang muncul saat ada yang salah, lengkap dengan lokasi dan penjelasannya.

Kalau ada istilah lain yang mengganjal, tombol pencarian di kanan atas mencari ke seluruh halaman ini.

Kode di bawah ini bukan ilustrasi. Ia dijalankan lewat ketiga mesin sebelum halaman ini ditulis, dan keluarannya sama di ketiganya.

tur.jrg
package main

struct Titik { pub x: int, pub y: int }

impl Titik {
    fn geser(self, dx: int) -> Titik { return Titik { x: self.x + dx, y: self.y } }
}

enum Bentuk { Lingkaran(int), Kotak(int, int), Titik_ }

fn luas(b: Bentuk) -> int {
    return match b {
        Bentuk::Lingkaran(r) => 3 * r * r,
        Bentuk::Kotak(p, l)  => p * l,
        Titik_               => 0,
    }
}

// parameter pengumpul: sisa argumen jadi array
fn jumlah(xs...) -> int {
    var t = 0
    for i in xs { t = t + xs[i] }
    return t
}

// `let … else`: kalau polanya tidak cocok, blok else harus menyimpang
fn ambil(d, kunci: string) -> int {
    let Some(v) = dict::get(d, kunci) else { return -1 }
    return v
}

pub fn main() {
    let p = Titik { x: 1, y: 2 }
    wok(p, p.geser(9).x)

    wok(luas(Bentuk::Lingkaran(2)), luas(Bentuk::Kotak(3, 4)), luas(Titik_))

    let nilai = { "a": 1, "b": 2 }
    wok(ambil(nilai, "b"), ambil(nilai, "z"))
    wok(dict::get(nilai, "z") ?? 0)

    wok(jumlah(1, 2, 3), jumlah())

    let umur = 21
    wok(«halo, umurmu ${umur} — ${umur >= 18 $ "dewasa" : "bocah"}»)

    match umur {
        n @ 0..=17 => wok("muda", n),
        n          => wok("dewasa", n),
    }

    var hitung = 0
    let naik = || { hitung = hitung + 1 }
    naik(); naik()
    wok(hitung)

    wok([3, 1, 2].map(|x| x * 10).filter(|x| x > 10))
}
keluaran — sama di interpreter, VM, dan LLVM
Titik {x: 1, y: 2} 10
12 12 0
2 -1
0
6 0
halo, umurmu 21 — dewasa
dewasa 21
2
[30, 20]

Beberapa keputusan yang tidak biasa

BentukContohKenapa begitu
ternary $ kond $ a : b Pembukanya $, bukan ?. Dulu ? melayani tiga hal sekaligus, dan parser harus menebak lewat pemindaian maju sampai 4096 token, dan tebakan itu punya tiga kelas kegagalan yang terukur. Sekarang ? berarti satu hal: propagasi error.
template """ / «» """halo ${nama}""" Multi-baris dan menyisipkan nilai lewat ${...}. Dua pembatas, satu mesin — bukan dua jalur yang harus dijaga tetap sama: «» lebih ringkas kalau papan tik Anda punya, """ bisa diketik di mana saja. Kutip tunggal di dalam """ tetap teks biasa, jadi """dia bilang "halo"""" tidak perlu escape. Nilainya diformat lewat jalur yang sama persis dengan wok, jadi """${x}""" dan wok(x) tidak pernah menampilkan bentuk berbeda untuk nilai yang sama. Untuk teks mentah tanpa penyisipan, pakai backtick.
for atas array for i, x in arr Bentuk dua nama mengikat indeks dan nilai, dan berlaku atas array, string dan dict (for k, v in d). Pakai _ untuk separuh yang tidak dipakai: for _, x in arr. Satu nama saja (for i in arr) menyebut indeks, bukan nilai — itu jebakan ergonomi terbesar bahasa ini, dan bentuk dua nama adalah jawabannya. arr.each(fn) tetap tepat kalau yang diinginkan sebuah closure.
indeks negatif a[-1]
urutan agregat [1,2] < [1,3] Array dan tuple dibandingkan leksikografis — elemen per elemen, lalu panjangnya. Ditambahkan untuk menutup sebuah kontradiksi, bukan sebagai gagasan baru: arr::sort, min, max, sort_by_key dan binary_search sudah memakai urutan itu, sementara < menolaknya — jadi mengurutkan daftar berisi daftar bekerja, dan membandingkan dua daftar tidak. Elemen yang tidak sebanding tetap membatalkan pembandingannya ([1] < ["a"] galat, bukan “dianggap sama”), dan pesannya menyebut tipe elemen: array memang bisa dibandingkan, yang tidak bisa int dengan string di dalamnya. Menghitung dari belakang. Di luar jangkauan selalu error, tidak pernah dijepit. Berlaku juga untuk potongan: a[2:1] ditolak.
?? o ?? 0 Hubung-singkat: sisi kanan cuma dievaluasi kalau sisi kiri None. Beda dari o.unwrap_or(x), yang sebuah pemanggilan method dan karenanya selalu mengevaluasi argumennya. Tidak ada ??=, dan itu keputusan: ?? membuka Option-nya, jadi a ??= b cuma bisa berarti a = a ?? b — yang mengubah tipe a dari Option<T> jadi T bahkan waktu isinya ada, kebalikan dari artinya di JS/C#/PHP.
n @ pola n @ 0..=17 Mengikat nilainya ke n sekaligus mengujinya dengan polanya.
array kosong a.first() Apa pun yang mungkin tidak menemukan sesuatu mengembalikan Option, tidak pernah nil telanjang.
anotasi tipe let x: int = … Tidak ditegakkan. JRG bertipe dinamis; anotasi adalah dokumentasi. Yang diperiksa cuma apakah nama tipenya ada, jadi let x: Hantu = 5 ditolak sementara let x: int = "teks" tidak.

let mengikat, var tidak

Kata kunci yang mendeklarasikan sebuah nama menentukan apakah nama itu boleh ditugasi ulang. let dan const mengikat; var, :=, dan let mut tidak. Menulis lewat binding ikut terjaga, karena nilai di JRG tidak pernah beralias: a benar-benar memiliki isinya.

let x = 1
x = 2          // error: `x` dideklarasikan dengan `let`

let a = [1, 2]
a[0] = 9       // error juga — `a` memiliki isinya

var b = [1, 2]
b[0] = 9       // boleh

Parameter, variabel perulangan, dan pengikat pola tidak punya kata kunci, jadi ketiganya bisa diubah. Itu bukan kompromi: parameter di JRG sudah berupa salinan, jadi menguncinya tidak melindungi siapa pun.

Closure move

Closure biasa menangkap binding-nya, jadi perubahan terlihat dari kedua arah. move menangkap salinan, dan itu memutus keduanya sekaligus. Salinannya dimiliki closure, jadi mutasi di dalamnya bertahan antar pemanggilan.

var n = 1

let berbagi = || n
let salinan = move || n

n = 9
wok(berbagi())     // 9 — melihat perubahan di luar
wok(salinan())     // 1 — memotret saat dibuat

var c = 0
let hitung = move || { c = c + 1; return c }
wok(hitung())      // 1
wok(hitung())      // 2 — salinannya bertahan
wok(c)             // 0 — yang di luar tak tersentuh

spawn memang sudah menangkap salinan, jadi spawn move tetap benar tapi tidak menambah apa pun.

Operator yang ditentukan sendiri

Sebuah tipe memberi arti pada operator dengan mendefinisikan metode bernama tertentu — mekanisme yang sama dengan to_string, jadi pewarisan dan default trait ikut bekerja.

struct Vektor { pub x: int, pub y: int }

impl Vektor {
    fn add(self, o) -> Vektor { return Vektor { x: self.x + o.x, y: self.y + o.y } }
    fn neg(self) -> Vektor { return Vektor { x: 0 - self.x, y: 0 - self.y } }
    fn cmp(self, o) -> int { return (self.x + self.y) - (o.x + o.y) }
    fn to_string(self) -> string { return «(${self.x}, ${self.y})» }
}

pub fn main() {
    let a = Vektor { x: 1, y: 2 }
    let b = Vektor { x: 3, y: 4 }
    wok(a + b)         // (4, 6)
    wok(-a)            // (-1, -2)
    wok(a < b)         // true
}

== dan != lewat satu metode eq; keempat pembanding urutan lewat satu cmp yang mengembalikan bilangan bertanda. Sengaja satu, bukan empat: metode terpisah memungkinkan sebuah tipe menjawab a < b dan a >= b dua-duanya benar. && dan || tidak bisa ditentukan sendiri, karena keduanya hubung-singkat sementara metode selalu mengevaluasi argumennya.

Menentukan sendiri cara sebuah tipe dicetak

Sebuah struct atau enum boleh mendefinisikan to_string, dan setiap jalur tampilan memakainya: wok, template literal, penggabungan string, dan nilai yang berada di dalam array, tuple, dict, atau field struct lain.

struct Uang { pub rupiah: int }

impl Uang {
    fn to_string(self) -> string {
        return "Rp" + fmt::sprint(self.rupiah)
    }
}

pub fn main() {
    let harga = Uang { rupiah: 2500 }
    wok(harga)                    // Rp2500
    wok([harga, harga])           // [Rp2500, Rp2500]
    wok(«total: ${harga}»)        // total: Rp2500
}

Bentuknya diperiksa, tidak diasumsikan. to_string yang meminta parameter tambahan atau mengembalikan selain string adalah error yang menyebutkan sebabnya, karena mengabaikannya diam-diam persis seperti tidak punya hook sama sekali. Hook yang menampilkan self juga ditolak, bukan menggantung.

defer membekukan argumennya

Lingkupnya fungsi, bukan blok, dan urutannya LIFO. Yang sering mengejutkan: argumennya dibekukan saat baris defer dibaca, bukan saat ia benar-benar jalan.

fn tulis() {
    var berkas = "laporan.txt"
    defer wok("tutup", berkas)
    berkas = "berubah"
    wok("isi", berkas)
}

// isi berubah
// tutup laporan.txt   <- bukan "berubah"

JRG dipakai dengan gaya prosedural, berorientasi objek, fungsional, atau konkuren — dan keempatnya bercampur dalam satu berkas tanpa jahitan. Yang membedakan daftar ini dari daftar pemasaran: tiap paradigma pernah diukur konstruksi demi konstruksi, dan yang ternyata cuma terlihat ada dibuang atau diperbaiki. async/await dulu ter-parse dan berjalan sinkron; move dulu terbalik artinya; const dulu bisa ditugasi ulang. Ketiganya bukan lagi begitu.

Prosedural

Yang paling langsung, dan yang paling banyak dipakai orang. Kata kunci yang mendeklarasikan sebuah nama menentukan boleh-tidaknya nama itu ditulis ulang: let dan const mengikat, var dan := tidak. Aturan itu ditegakkan di parser, jadi ketiga mesin mewarisinya.

prosedural.jrg
package main

let BATAS = 10              // tingkat modul, dihoist

fn deret(n) {
    var hasil = []
    defer wok("selesai menyusun")   // argumennya dibekukan di sini
    for i in 0..n {
        if i % 3 == 0 { continue }
        hasil = arr::push(hasil, i)
    }
    return hasil
}

pub fn main() { wok(deret(BATAS)) }

Berorientasi objek

Struct dengan pewarisan tunggal, impl, trait yang boleh punya method abstrak maupun berdefault, dan visibilitas field yang benar-benar ditegakkan — termasuk lewat garis pewarisan. Dua hal yang sering absen di bahasa muda ada di sini: operator bisa didefinisikan tipe, dan sebuah tipe bisa menentukan cara dirinya ditampilkan.

objek.jrg
package main

trait Bentuk {
    fn luas(self) -> float          // abstrak: tiap impl WAJIB menyediakan
    fn nama(self) -> string = "bentuk"  // default
}

struct Vektor { pub x: int, pub y: int }

impl Vektor {
    fn add(self, lain) { return Vektor{x: self.x + lain.x, y: self.y + lain.y} }
    fn to_string(self) -> string { return "(" + self.x as string + ", " + self.y as string + ")" }
}

pub fn main() {
    wok(Vektor{x:1,y:2} + Vektor{x:3,y:4})   // (4, 6)
}

+ menemukan add lewat pencarian method yang sama dengan panggilan biasa, jadi warisan dan default trait ikut bekerja. && dan || sengaja tidak bisa didefinisikan ulang: keduanya hubung-singkat, sedangkan sebuah method selalu mengevaluasi argumennya — membolehkannya akan memindahkan kapan efek samping terjadi, diam-diam. Dan to_string dipakai setiap jalur tampilan, termasuk untuk nilai yang bersarang di dalam array atau dict; nilai yang tampil beda saat sendirian dan saat di dalam daftar lebih membingungkan daripada tidak ada hook sama sekali.

Fungsional

Fungsi adalah nilai, closure menangkap pengikatan (bukan salinan — kecuali diminta dengan move), dan pencocokan pola punya guard, pengikatan nama @ pola, pola array, serta ADT lewat enum. Option dan Result adalah tipe biasa, dengan ? untuk propagasi dan ?? untuk nilai cadangan yang hubung-singkat.

fungsional.jrg
package main

enum Bentuk { Lingkaran(float), Kotak(float, float) }

fn jelaskan(b) {
    return match b {
        Lingkaran(r) if r > 5.0 => "lingkaran besar",
        Lingkaran(_)              => "lingkaran",
        Kotak(p, l)               => "kotak",
    }
}

pub fn main() {
    let angka = [5, 2, 9, 1]
    wok(angka.filter(|x| x > 2).map(|x| x * 10))

    let [pertama, ...sisa] = angka        // pola array
    wok(pertama, sisa)

    wok(match 7 { n @ 1..=9 => n * 10, _ => 0 })   // 70
    wok(angka.find(|x| x > 100) ?? -1)                // -1, tanpa memanggil apa pun
    wok(jelaskan(Bentuk::Lingkaran(9.0)))
}

?? diturunkan jadi match, bukan jadi pemanggilan method, dan itu yang membuatnya hubung-singkat: cache ?? hitung_lambat() tidak menghitung apa pun kalau cache berisi. Ia sengaja hanya untuk Option — tidak ada cabang nil.

Konkuren

Modelnya CSP: goroutine yang berkomunikasi lewat channel, dengan select untuk menunggu beberapa sekaligus. Untuk bentuk “panggil sekarang, tunggu nanti” ada Future dan Task. Semuanya jalan di interpreter maupun VM.

konkuren.jrg
package main

fn berat(n) { return n * n }

pub fn main() {
    let a = Future::new(berat, 12)
    let b = Future::new(berat, 30)
    wok(future::await(a) + future::await(b))

    let c = make_chan(int, 2)
    spawn { c <- berat(3) }
    spawn { c <- berat(4) }
    wok(<-c + <-c)
}

Goroutine ditunggu sebelum program berakhir, dan itu satu-satunya tempat JRG sengaja berbeda dari Go: di sana main yang selesai meninggalkan goroutine yang masih berjalan, di sini ia ditunggu. Alasannya semantik skrip yang lebih terduga — sebuah program pendek tidak kehilangan keluarannya cuma karena goroutine-nya belum sampai ke wok pertamanya. Goroutine yang benar-benar tidak pernah selesai membuat penungguan itu abadi juga, persis seperti di bahasa mana pun yang punya thread sungguhan.

Aturan itu sudah lama berlaku di interpreter dan tidak di VM, jadi satu program yang sama mencetak dua baris di satu mesin dan satu baris di mesin lain — tanpa galat di kedua sisi. Yang menemukannya bukan tabel paritas melainkan sebuah tes profiler yang gagal sekali: angkanya benar sementara satu baris utuh hilang dari laporannya, dan itu bukan gejala mesin yang sedang sibuk.

async dan await tidak ada di JRG, dan itu keputusan. Di bahasa mana pun, async menandakan task murah yang di-multiplex di sedikit thread. Konkurensi JRG adalah satu thread OS per task — terukur di mesin ini sekitar 127 ms untuk seribu goroutine. Memberi katanya tanpa model biayanya berarti sepuluh ribu pemanggilan async jadi sepuluh ribu thread: sintaksnya menjanjikan satu hal, harganya lain. Menolaknya lebih jujur daripada memindahkan kejutan itu ke waktu jalan.

Sebuah Future menyerahkan hasilnya sekali. Mengambilnya lagi bukan menggantung tanpa kabar — ia gagal keras dan menyebutkan alasannya, termasuk kalau yang mengambil adalah salinan nama yang lain.

Kalau istilahnya asing, begini singkatnya. Interpreter membaca program Anda lalu langsung menjalankannya — paling gampang dipakai, paling lambat berjalan. VM bytecode menerjemahkannya dulu jadi instruksi ringkas lalu menjalankan instruksi itu — disiapkan sebentar, jalannya jauh lebih cepat. Native mengubahnya jadi bahasa mesin yang dimengerti prosesor langsung, jadi programnya berdiri sendiri tanpa perlu JRG lagi.

Ketiganya memakai bagian depan yang sama untuk membaca kode Anda; yang berbeda cuma tahap terakhirnya. Aturannya satu dan tegas: interpreter adalah acuannya. Kalau dua mesin lain menjawab berbeda, merekalah yang salah, bukan sebaliknya.

MESIN 01

Interpreter

Menjalankan AST langsung. Acuan semantik, dan satu-satunya yang punya py::. Goroutine di sini adalah thread OS sungguhan.

jrgc program.jrg
MESIN 02

Bytecode VM

Lebih cepat, dan bahasanya utuh: closure, defer, pola array, template literal, bilangan 128-bit. Galatnya membawa lokasi yang sama persis dengan interpreter, sampai ke kolom caret-nya.

jrgc program.jrg --vm
MESIN 03

LLVM native

Menghasilkan kode mesin lewat clang. Alokatornya bump allocator, dan itu sah justru karena backend ini menolak concurrency secara terbuka.

jrgc program.jrg --emit-llvm

Bagaimana ketiganya dijaga

Bukan dengan menguji tiap mesin sendiri-sendiri, tapi dengan menjalankan program yang sama di dua mesin lalu membandingkan keluarannya byte per byte. Tabelnya bekerja dua arah: yang harus sama dan yang harus sama-sama ditolak.

InstrumenJumlahYang dijaga
PARITAS345Program yang keluarannya harus identik di interpreter dan VM.
DITOLAK_DUA_BACKEND88Program yang harus ditolak keduanya, masing-masing dengan pesan yang menyebut hal yang benar.
PARITAS_LLVM198Sama, untuk interpreter lawan LLVM.
DIVERGENSI_DIKETAHUI0Tidak ada program yang jalan di dua mesin dan menjawab berbeda.
Konformans11 / 11 / 11Berkas semantik lengkap, lewat interpreter / VM / LLVM.
!
Kenapa dua arah

Tabel yang cuma menguji program yang berhasil bisa hijau seluruhnya sementara sebuah mesin diam-diam menerima program yang seharusnya ditolak. Dan tabel yang cuma menguji penolakan bisa hijau dengan menolak segalanya. Dua-duanya perlu ada.

Setiap nilai dimiliki tepat satu pengikatan. Menugaskan, mengoper, mengembalikan, atau membaca dari sebuah koleksi selalu menyalin. Karena itu masa hidup sebuah nilai persis sama dengan lingkup pengikatannya, dan dangling pointer tidak bisa muncul untuk nilai biasa.

Yang bisa mengejutkan orang yang datang dari bahasa lain: menyalin itu tidak mahal. Salinan sebenarnya baru dibuat saat ada yang menulis, dan setiap penulisan melepas perkongsian lebih dulu, di setiap tingkat jalur tulisnya, bukan cuma akarnya.

var a = [1, 2, 3]
var b = a        // O(1) — belum ada yang disalin
b[0] = 99       // di sini baru menyalin
wok(a[0], b[0])  // 1 99

Kalau memang mau mengatur sendiri

jawa:: mengalokasi, jawir:: membebaskan. Ini simulasi semantik malloc/free, bukan penunjuk mentah: yang disimpan sebuah id di registri, jadi salah pakai menghasilkan pesan, bukan segfault.

let p = jawa::alloc(42)
wok(jawa::get(p), jawa::is_valid(p))   // 42 true

// handle itu nilai biasa — salinannya menunjuk penyimpanan yang SAMA
let salinan = p
jawa::set(salinan, 99)
wok(jawa::get(p))                        // 99

jawir::free(salinan)
wok(jawa::is_valid(p))                   // false

Membebaskan lewat salinan mana pun membuat semua salinan handle-nya tidak sah. Memakainya setelah itu bukan perilaku tak terdefinisi:

error: jawa::get: use-after-free — pointer ini sudah di-`jawir::free`
error: jawir::free: double free — pointer ini sudah pernah di-`jawir::free` sebelumnya
!
Satu kebocoran yang diketahui

Closure yang memanggil dirinya sendiri membentuk siklus kepemilikan yang tidak pernah direklamasi. Untuk rekursi berulang, pakai fn bernama. Terukur pada 200 ribu closure rekursif: sekitar 461 MB di interpreter, 46 MB di VM.

Pesan galat adalah satu-satunya bagian bahasa yang benar-benar mengajar. Kalau ia kaku, orang membacanya sebagai kegagalan alih-alih sebagai petunjuk. Jadi tiap galat di JRG diawali satu baris sapaan, lalu badan pesannya yang biasa.

jrgc contoh.jrg
kepentok di baris 4 waktu jalan. keterangannya di bawah.
error: index [9] di luar batas — array ini isinya 3 elemen
       (index valid: 0..2, atau -1..-3 kalau mau hitung dari belakang)
 --> contoh.jrg:4:9
  |
4 |     wok(a[9])
  |         ^^^^
  |

Ada enam nada, dipilih lewat --nada= atau JRG_NADA=: sopan, galak, judes, hangat, cool, lawak. --sopan membuang sapaannya sama sekali, yang berguna untuk CI yang membandingkan keluaran.

nada tidak mengubah isibadan pesannya identik

Yang berubah cuma barisan sapaannya. Kode galat, lokasi, kutipan sumber, caret, baris bantuan: dan catatan: tidak disentuh, jadi tidak ada informasi yang cuma hidup di satu nada.

kemunculan pertama selalu seriusdi semua nada

Lelucon pada saat seseorang pertama kali menabrak galat cuma menunda satu-satunya hal yang ia butuhkan: tahu harus melihat ke mana. Nada baru muncul dari kemunculan kedua, setelah versi seriusnya sudah dibaca dan ternyata belum nyangkut.

nadanya naik kalau galatnya berulangdan pada akhirnya menambah bantuan

Galat yang sama muncul tujuh kali adalah bukti bahwa pesannya tidak menyampaikan. Jadi tingkat teratas menambahkan baris bantuan:, bukan sekadar bicara lebih keras.

variasinya diturunkan, bukan diundikeluaran yang sama untuk masukan yang sama

Kalimatnya dipilih dari (identitas galat, jumlah pengulangan). Keacakan sungguhan akan membuat keluaran jrgc berbeda tiap jalan, yang merusak pembandingan antar-mesin dan membuat laporan bug tidak bisa diulang.

Salah ketik dapat nama yang benarsaran diturunkan, bukan ditulis

Berlaku untuk variabel, builtin, dan method — termasuk method yang diwarisi dari struct induk.

[1,2,3].leng()   // `array` tidak punya method atau field bernama `leng` — maksudnya `len`?
"halo".uppr()   // `string` ... `uppr` — maksudnya `upper`?

Calonnya diambil dari registry yang sedang berjalan, bukan dari daftar yang ditulis tangan — jadi sebuah builtin baru bisa disarankan pada hari ia didaftarkan. Daftar tangan di sini akan menua ke arah yang tidak terlihat: nama yang hilang cuma berarti “tidak ada saran”, yang terbaca sama persis dengan “tidak ada yang mirip”.

Jaraknya dibatasi, dan batasnya diukur bukan dipilih: jarak satu selalu; jarak dua hanya kalau ia satu-satunya calon dan namanya cukup panjang sehingga dua suntingan tetap bagian kecil darinya. Di antara 641 nama builtin ada 314 pasang yang berjarak dua — pada jarak itu sebuah saran sering menunjuk fungsi lain yang sama-sama nyata, dan saran yang salah menyesatkan lebih jauh daripada tidak ada saran.

Pesan galat tidak membanjiri layarmenyebut tipe, bukan isi

Kalau sebuah nilai dipakai dengan cara yang salah, JRG menyebut tipe-nya, bukan isinya. Jadi salah ketik method pada array berisi tiga angka dan pada array berisi tiga ribu angka menghasilkan pesan yang sama pendeknya.

[1,2,3].panjang()   // `array` tidak punya method atau field bernama `panjang` — maksudnya `len`?

Alasannya sederhana: pesan yang ikut membesar mengikuti datanya justru berhenti bisa dibaca pada program yang paling butuh dibaca. Dan caret di bawah kode sudah menunjuk tepat ke bagian yang bermasalah, jadi yang belum diketahui pembaca adalah tipe apa bertemu tipe apa — bukan isi datanya lagi.

Aturan yang sama berlaku untuk operator (5 % "a"), untuk indexing, dan untuk argumen builtin yang tidak cocok. Semuanya memakai satu definisi pesan, jadi satu kesalahan tidak punya tujuh kalimat yang berbeda-beda.

Galat menunjukkan jalan yang membawanyajejak pemanggilan

Kalau sebuah galat terjadi jauh di dalam — misalnya di dalam fungsi pustaka yang tidak pernah Anda buka — caret-nya akan berdiri di berkas orang lain, menunjuk baris yang tidak salah. Karena itu setiap galat runtime juga mencetak rantai pemanggilan yang membawanya ke sana.

error: `5` itu bukan fungsi, jadi nggak bisa dipanggil pakai `(...)`
  --> <std/functional>:13:37
   |
13 | pub fn compose(f, g) { return |x| f(g(x)) }
   |                                     ^^^^
   |
   = jejak: `<closure>` dipanggil di program.jrg:3:56

Baris jejak: itulah yang menunjuk kembali ke berkas Anda sendiri. Untuk rekursi yang dalam, hanya sepuluh pemanggilan terdekat yang dicetak dan sisanya dihitung, supaya pesannya tidak mengubur dirinya sendiri.

Berlaku sama di penerjemah maupun di --vm. Backend native belum punya ini — kode yang dipancarkannya tidak menyimpan tumpukan pemanggilan JRG.

i
Aturan suaranya

Nakal boleh, membuat orang merasa bodoh tidak. Yang membaca pesan galat biasanya sudah bingung, dan kalimat yang cuma menjatuhkan membuat orang menutup terminal.

arr:: str:: math:: dict::koleksi dan angka

Tersedia di interpreter maupun VM, dari implementasi yang sama. Aritmetika bilangan bulat diperiksa: + - * ** / % menghasilkan galat kalau melewati 128-bit, bukan membungkus diam-diam.

Bentuk namespace dan bentuk method menjawab hal yang sama: arr::max_by(a, f) dan a.max_by(f) adalah satu operasi dengan dua cara menulis. Itu tidak selalu benar — empat nama (count_if, max_by, min_by, sort_by_key) sempat cuma punya bentuk method, sementara registry sudah mengiklankan bentuk namespace-nya: nama itu muncul di :cari, punya batas argumen, dan memanggilnya gagal. Sekarang keduanya ada, dan sebuah tes memanggil tiap nama yang diiklankan untuk memastikan ia benar-benar bisa dipanggil.

Apa pun yang mungkin tidak menemukan sesuatu mengembalikan Option.first(), .get(i), .find(f), dict::get. Pakai ?? atau .get_or(k, bawaan) kalau cuma butuh nilai cadangan.

import "std/…"pustaka yang ditulis dengan JRG

Sebelas modul pustaka standar ditulis dalam JRG sendiri, bukan di Rust, dan ikut menyatu di dalam biner — jadi bisa dipakai dari direktori mana pun tanpa memasang apa-apa.

import "std/set"
import "std/functional"

var s = set::new()
s = s.add(3).add(3)
wok(s.len())        // 1 — himpunan, bukan daftar

Isinya: set queue deque heap untuk struktur data, csv dan path untuk hal yang gampang salah kalau ditulis sendiri, functional untuk menyusun fungsi, plus option result iterator testing.

Masing-masing ada karena memberi sesuatu yang tidak dipunyai arr::: pengecekan keanggotaan yang tidak perlu menyusuri seluruh daftar, antrean yang tidak melambat saat memanjang, dan aturan tanda kutip CSV yang salah dibaca kalau Anda cuma memotong di setiap koma.

Berkas .jrg milik Anda sendiri menang atas nama ini. Kalau ada math.jrg di sebelah program Anda, import "math" memuat berkas itu.

time::kalender dan cap waktu

time::format(t, pola) menerima %Y tahun, %y dua digit, %m bulan, %d tanggal, %H jam, %M menit, %S detik, %L milidetik, %A nama hari, %B nama bulan, dan %%. Arahan yang tidak dikenal ditolak, tidak diteruskan sebagai teks biasa — orang yang menulis %Q sedang meminta sesuatu, dan mengembalikan %Q apa adanya adalah jawaban salah yang tidak melapor.

Satuannya milidetik. time::now_ms() adalah yang dipakai seluruh keluarga ini; time::now() menjawab detik dan tidak boleh disusun dengan mereka. Dulu bisa, dan hasilnya bukan galat melainkan tahun 1970 — sebuah jawaban yang terlihat persis seperti jawaban. Sekarang argumen pecahan ditolak sambil menyebut fungsi mana yang benar.

Kalendernya dihitung sendiri lewat civil_from_days (algoritma yang juga dipakai <chrono> C++), aritmetika bilangan bulat murni, tanpa crate tanggal. Waktunya UTC: belum ada zona waktu, dan itu disebutkan di sini alih-alih dibiarkan mengejutkan seseorang yang jamnya meleset tujuh jam.

let t = time::now_ms()
wok(time::format(t, "%d/%m/%Y %H:%M"))   // 04/09/2026 14:10
wok(time::format(t, "%A, %d %B %Y"))     // Jumat, 04 September 2026
wokwok::numerik, 86 fungsi

Punya tipe array n-dimensi sendiri dengan buffer bertipe dan kontigu. Alasannya diukur lebih dulu: arr::sum atas sejuta f64 memakan 162 ms lawan 0,53 ms milik numpy. Selisih itu tidak bisa ditutup dengan menyetel interpreter, karena penyebabnya representasi.

Penjumlahannya memakai pairwise, bukan Neumaier: bisa divektorisasi dan lebih akurat pada kasus umum. Sejuta angka 0,1 berjumlah tepat 100000; numpy menjawab 100000.00000000003.

Canvas::menggambar, 52 method

Anti-aliasing, tumpukan transformasi afin, clip rect, poligon, elips/busur/Bezier, gradien, dan pengekspor PNG yang menulis DEFLATE-nya sendiri. PNG juga bisa dibaca (Canvas::load_png), dan satu canvas bisa digambar ke atas canvas lain (blit, blit_region) — tanpa keduanya sebuah engine tidak bisa berdiri: tidak ada sprite, tidak ada tekstur, dan segala yang tampil harus digambar ulang dari kode tiap bingkai.

let atlas = Canvas::new(8, 8)
atlas.fill_rect(0, 0, 4, 4, 0xFF3355)
atlas.save_png("atlas.png")

let dimuat = Canvas::load_png("atlas.png")
let layar  = Canvas::new(16, 16)
layar.blit(dimuat, 2, 2)                      // seluruh gambar
layar.blit_region(dimuat, 0, 0, 4, 4, 9, 9)   // satu petak sprite

Dekodernya ditulis di sini seperti enkodernya, tanpa dependensi baru, dengan INFLATE lengkap termasuk Huffman dinamispng.rs menulis Huffman tetap sementara zlib/PIL/GIMP menulis dinamis, jadi sebuah pembaca tanpa jalur itu akan lulus setiap uji putar-balik dan gagal pada berkas pertama yang datang dari luar. Berkas ujinya karena itu dibuat PIL, bukan oleh penulis proyek ini.

Blit memakai set_pixel, dan itu bukan kemudahan melainkan alasan: pintu itu sudah memadukan alfa, jadi sprite berlatar transparan tidak tergambar berlatar kotak buram. Blit sebuah canvas ke dirinya sendiri disalin lebih dulu — Mutex Rust tidak reentrant, jadi mengunci handle yang sama dua kali menggantung selamanya, tanpa pesan. Berjalan di dua mesin. Window sungguhan butuh --features canvas; bawaannya perender perangkat lunak yang menulis ke berkas.

py::interpreter saja — dan alasannya nyata

py:: adalah interop Python (24 fungsi) dan js:: interop JavaScript lewat Node (21 fungsi). Keduanya interpreter saja karena alasan yang sama persis: protokol worker-nya harus bisa MEMANGGIL BALIK sebuah fungsi JRG di tengah satu permintaan, dan builtin VM adalah penunjuk fungsi biasa yang tidak pernah menerima runtime-nya. Mesin worker-nya satu, dipakai berdua, lengkap dengan handle objek: numpy.arange(4_000_000) tetap tinggal di sisi Python, dan yang menyeberang cuma jawabannya.

Ia satu-satunya namespace yang benar-benar terikat interpreter, dan alasannya bukan soal kerapian: protokol worker-nya perlu memanggil balik sebuah fungsi JRG di tengah pekerjaan Python, dan hanya interpreter yang bisa melakukannya.

dok:: dulu ada di daftar ini dengan catatan “keputusan desain”. Diukur, itu keliru: 94 dari 96 fungsinya — jumlahnya saat itu; namespace-nya kini 119 — tidak pernah menyentuh interpreter sama sekali, dan yang menahan mereka cuma berkas pendaftaran yang belum ditulis. Sekarang seluruh dok:: — jaringan, HTTP, JSON, statistik, log, servernya sendiri, lapisan web, WebSocket dan driver SQLite — jalan di kedua mesin.

Menjalankan py:: lewat --vm ditolak dengan alasan yang sebenarnya, bukan dengan pesan “variabel belum didefinisikan” yang tidak bisa dibedakan dari salah ketik.

mutex:: atomic::VM saja — juga disengaja

Kedelapan fungsi ini menyimpan keadaannya di dalam sel bersama milik VM. Memaksanya menyeberang ke interpreter akan kehilangan setiap penulisan secara diam-diam, karena nilai di interpreter tidak pernah beralias. Jadi interpreter menolaknya sambil menyebut alasan itu.

future:: dan task:: dulu ada di baris ini dengan alasan yang sama, dan alasan itu keliru untuk mereka: keduanya cuma membungkus sebuah channel, yang justru berupa handle — bentuk yang memang bisa menyeberang. Yang sebenarnya menahan mereka adalah Future::new(f) harus memanggil f. Sekarang Future::new, future::await, is_done, all, race, timeout, try_get, Task::spawn dan task::join jalan di kedua mesin.

task::cancel dan task::id tidak ada di sisi mana pun, dan itu perbaikan: keduanya dulu berpura-pura. cancel menutup channel hasilnya lalu membiarkan task-nya jalan sampai selesai — terbukti lewat sebuah Atomic yang ditulis task itu sesudah “dibatalkan” — dan id mengembalikan angka yang tidak menunjuk apa pun. Keduanya sekarang menolak dari satu kalimat yang sama, dan cancel menyebutkan pola yang benar-benar bekerja: sebuah flag Atomic yang dibaca task-nya sendiri.

i
Tidak ada lubang yang tidak disengaja

Daftar builtin diturunkan langsung dari sumbernya oleh sebuah tes, lalu tiap nama dijalankan di kedua mesin. Sebuah fungsi entah ada di dua-duanya, atau ditolak sambil menyebut alasannya. Asimetri yang tidak disengaja tidak bisa tumbuh diam-diam di sini.

Sapuannya menyentuh 664 nama. Angka itu naik dari sekitar 500 ketika satu titik buta ditutup: nama telanjang (cetak, sin, zip) dulu dikecualikan seluruhnya, dengan alasan “punya kasus khususnya sendiri” — alasan untuk berhati-hati, bukan untuk tidak melihat. Yang ditemukan di baliknya: 21 builtin yang cuma hidup di satu mesin, dan 17 di antaranya justru diterima oleh mesin acuan dan ditolak VM, sehingga program yang memakainya cuma bisa jalan tanpa --vm. Semuanya sekarang hidup di dua-duanya.

C, lewat extern "C"

Deklarasikan tanda tangannya, lalu panggil. Pustakanya boleh disebut eksplisit. Hasilnya selalu Result — sebuah simbol bisa saja tidak ada, dan memaksanya jadi nilai telanjang berarti kegagalan itu harus muncul di tempat lain.

ffi_c.jrg
package main

extern "C" {
    fn abs(n: i32) -> i32
    fn getpid() -> i32
}
extern "C" "libm.so.6" {
    fn pow(base: f64, exp: f64) -> f64
}

pub fn main() {
    match abs(-17) { Ok(v) => wok(v), Err(e) => wok("gagal:", e) }
}

Sisi C jalan di interpreter maupun VM, dari satu aturan marshalling yang sama: jembatan VM-nya sengaja cuma penerjemah nilai dan tidak memutuskan apa pun sendiri, jadi setiap penerimaan dan penolakan — dan karenanya setiap pesan galat — datang dari satu tempat. Menjadikannya penjaga kedua sempat dicoba, dan langsung menghasilkan pesan FFI yang berbeda antar-mesin.

C++: dipanggil langsung, tanpa pembungkus

Sebuah fungsi C++ memakai konvensi pemanggilan yang sama dengan C, jadi yang menghalangi bukan ABI-nya melainkan nama simbolnya: int add(int, int) diekspor sebagai _Z3addii. Tanpa mangling, sebuah pustaka C++ cuma terjangkau kalau kamu sendiri yang membungkus tiap fungsinya dengan extern "C" — artinya pustaka yang tidak kamu kompilasi sendiri tidak terjangkau sama sekali.

extern "C++" "./libmilik.so" {
    fn add(a: i32, b: i32) -> i32
    fn math::mul(a: i32, b: i32) -> i32
    fn isi_ptr(p: *mut i32) -> i32
}

Namanya boleh berbentuk path, karena hampir semua fungsi C++ ada di dalam namespace — dan nama JRG-nya sama dengan nama C++-nya, jadi yang kamu tulis adalah yang ada di pustakanya. Tipe yang ditunjuk sebuah pointer ikut menentukan simbolnya (int* adalah Pi, void* adalah Pv), jadi presisinya tidak dibuang.

Aturan mangling-nya diadu dengan g++, bukan dibaca dari spesifikasi: 47 tanda tangan dicocokkan dengan keluaran nm atas berkas objek yang dikompilasi g++ sungguhan. Itu menemukan dua hal yang tidak akan ketemu dengan cara lain — sebuah nama fungsi bernamespace ternyata ikut mengisi tabel substitusi, dan salah satu nilai harapan yang ditulis tangan ternyata salah sementara kodenya benar.

Simbol yang tidak ketemu menyebut simbol yang dicari, supaya bisa langsung dibandingkan dengan nm -D: kalau tanda tangannya meleset, yang salah bukan pustakanya.

Spread di pemanggilan

fn f(a, sisa...) sudah lama mengemas argumen sisa jadi array. Kebalikannya — menyebarkan isi array jadi argumen satu per satu — sebelumnya tidak ada, jadi sebuah fungsi tidak bisa dipanggil dengan daftar argumen yang panjangnya baru diketahui saat program jalan.

let v = [2, 3]
wok(tambah(...v))          // 5
wok(banyak(1, ...v))       // campuran juga boleh
wok(terap(tambah, [10, 20]))  // mesinnya, kalau mau eksplisit

f(...v) adalah gula di atas satu builtin (terap), bukan sebuah bentuk AST tersendiri — dan itu bukan soal kemudahan: sebuah bentuk baru akan lewat di depan setiap penelusur ekspresi yang punya cabang “selain itu”, dan yang mengabaikannya diam-diam adalah analisis penangkapan closure. Dengan diturunkan jadi panggilan biasa, tidak ada penelusur mana pun yang perlu tahu spread itu ada.

Backend LLVM menolaknya sambil menyebut sebabnya: jumlah argumen sebuah spread baru diketahui saat program jalan, sementara backend itu menentukannya saat kompilasi.

Pola saat iterasi

for _, (a, b) in pasangan { wok(a + b) }
for i, [a, b] in baris { wok(i) }

Posisi pertama sengaja tidak menerima pola, dan itu bukan pilihan gaya: for i in arr di JRG memberi indeks, jadi sebuah pola di sana tidak punya apa pun untuk dibongkar. for (a, b) in ... ditolak sambil menyebut bentuk yang benar.

Pola di posisi parameter

fn jarak((x1, y1), (x2, y2)) { return math::abs(x2 - x1) + math::abs(y2 - y1) }
wok(arr::map(pasangan, |[a, b]| a * b))

Sebelumnya bentuk ini ke-parse dan tidak mengikat apa pun: polanya disimpan, dan daftar parameter sebuah fungsi cuma daftar nama di ketiga mesin — jadi f((1,2)) diterima dan a di dalam badannya gagal dengan “variabel a belum dikenal”, lengkap dengan saran ejaan untuk nama yang tertulis dua baris di atas.

Argumen bernama

bagi(pembilang: 10, penyebut: 2)
bagi(penyebut: 2, pembilang: 10)   // urutan bebas
bagi(10, penyebut: 2)              // campuran juga boleh

Urutannya ditentukan dari nama parameter fungsinya saat program jalan, karena parser tidak menyelesaikan panggilan — ia tidak tahu apakah f fungsi berkas ini, fungsi modul lain, atau sebuah variabel.

Empat cara salah memakainya punya kalimatnya masing-masing: nama yang tidak ada (sambil menyebut nama yang ada), parameter yang sudah diisi lewat posisi, nama yang sama dua kali, dan parameter yang dilewati. Yang terakhir ditolak walaupun parameter itu punya nilai default — dan sebabnya bukan kehati-hatian: nilai default dihitung di dalam fungsinya (fn f(a, b = a + 1) sah), jadi tidak ada yang bisa mengisinya dari luar.

Java: metode statis lewat JNI

Tanpa crate baru — libjvm.so di-dlopen, sama seperti driver SQLite, dan mesin tanpa JDK mendapat kalimat yang menyebut paketnya per distro alih-alih kegagalan yang tidak menjelaskan apa pun.

jvm::mulai(["-Djava.class.path=./kelas"])
wok(jvm::panggil_statis("java.lang.Math", "abs", "(I)I", [-17]))
wok(jvm::panggil_statis("UjiJrg", "sapa", "(Ljava/lang/String;)Ljava/lang/String;", ["dunia"]))

Tanda tangannya deskriptor JNI apa adanya, bukan ejaan buatan sendiri: itulah yang dicetak javap -s, jadi yang kamu tulis bisa disalin langsung dari keluaran perkakas Java — dan kalau tanda tangannya meleset, pesan galatnya menunjuk ke sana.

Tiga kegagalan yang berbeda terbaca berbeda: exception Java-nya sendiri (lengkap dengan toString()-nya, karena NoSuchMethodError dan ArithmeticException adalah dua masalah yang sama sekali berbeda), tanda tangan yang tidak ketemu, dan kelas yang tidak ada. null dari Java jadi nil, bukan string kosong — string kosong akan menyamarkan “tidak ada” jadi “ada tapi kosong”.

Yang paling berbahaya di sini bukan JNI-nya melainkan aritmetika indeks: JNIEnv* menunjuk sebuah array pointer fungsi, jadi indeks yang meleset berarti memanggil fungsi yang salah dengan tanda tangan yang salah — proses mati tanpa pesan. Indeksnya karena itu diturunkan dari jni.h milik JDK, bukan diketik; empat dari tebakan pertama meleset satu. Dan sebelum indeks mana pun dipakai, GetVersion dipanggil dulu: kalau tata letak tabelnya tidak seperti dugaan, tidak ada fungsi JNI lain yang disentuh.

Yang belum ada: objek Java belum bisa dipegang sebuah nilai JRG, jadi tipe yang bisa menyeberang adalah primitif dan String. Tipe lain ditolak dengan menyebutkan yang didukung, bukan diterjemahkan jadi sesuatu yang mirip.

Rust dan Go: sudah bisa, lewat extern "C"

Keduanya tidak butuh apa pun yang baru. Fungsi Rust yang ditulis #[no_mangle] pub extern "C" fn ... dan paket Go yang dibangun -buildmode=c-shared dengan //export keduanya mengekspor simbol C biasa. Diverifikasi dengan pustaka Rust sungguhan, termasuk bentuk &str yang diserahkan sebagai (pointer, panjang) — sesuatu yang tidak punya padanan di C.

Untuk fungsi Rust tanpa #[no_mangle], jawabannya bukan "menunggu diimplementasi": nama simbolnya memuat hash yang dihitung compiler dari metadata crate-nya, jadi ia tidak bisa diturunkan dari sebuah tanda tangan. Pesannya mengatakan itu apa adanya.

.NET: metode [UnmanagedCallersOnly]

Tanpa crate baru: libhostfxr.so di-dlopen, runtime-nya dinyalakan dari sebuah .runtimeconfig.json, lalu yang diambil adalah pointer fungsi sebuah metode [UnmanagedCallersOnly].

dotnet::mulai("./App.runtimeconfig.json")
let f = dotnet::fungsi("./App.dll", "UjiJrg, App", "Tambah",
                    ["i32", "i32"], "i32")?
wok(dotnet::panggil(f, [2, 3]))

Langkah terakhir itu yang membuat lapisan ini tipis: yang keluar adalah pointer fungsi C biasa, jadi pemanggilannya memakai mesin marshalling extern "C" yang sudah ada — sebuah metode .NET diperlakukan persis seperti fungsi C, karena sesudah [UnmanagedCallersOnly] memang itulah ia.

Nama tipenya adalah nama tipe bahasa ini (i32, i64, f64, bool, string), dan itu bukan kebetulan: dotnet:: membaca pemetaan nama→tipe yang sama yang dipakai parser untuk anotasi. Sebuah salinan kedua akan berarti nama yang diterima di satu tempat dan ditolak di tempat lain, untuk bahasa yang sama.

Yang sudah bisa menyeberang: int, long di luar jangkauan 32-bit, double, dan string JRG yang menyeberang sebagai pointer ber-NUL ke byte* di sisi C#. Ketiga bentuk kegagalannya terbaca berbeda: metode tidak ketemu (sambil menyebutkan bahwa nama tipenya harus berkualifikasi assembly), assembly tidak ada, dan handle yang dipakai sesudah dilepas.

WebAssembly: modul .wasm dijalankan di sini

Penerjemahnya ditulis di dalam proyek ini, bukan dipinjam: tidak ada runtime WASM yang bisa diandalkan ada di mesin mana pun, dan memaketkan salah satunya berarti menukar sifat “satu biner berperilaku sama di mana saja” untuk satu fitur.

let h = wasm::muat("modul.wasm")?
wok(wasm::ekspor(h))
wok(wasm::panggil(h, "fib", [20]))
wasm::tulis_memori(h, 200, "halo")
wok(wasm::baca_memori(h, 200, 4))

Sebuah modul WASM adalah cara paling wajar menjalankan kode dari luar tanpa memercayainya: ia tidak punya akses ke berkas, jaringan, atau memori JRG — hanya ke memorinya sendiri, dan hanya lewat apa yang diberikan pemanggilnya.

Yang membuktikannya benar bukan tesnya sendiri. Modul ujinya dibuat wat2wasm, dan tiap jawabannya diadu dengan WebAssembly milik Node — dua puluh sembilan panggilan, semuanya cocok. Itu menangkap dua hal yang terlihat jelas benar kalau ditulis dengan cara Rust: f64.nearest di WASM membulatkan ke genap di titik tengah (2.5 → 2, bukan 3), dan f64.min menularkan NaN sementara f64::min Rust justru mengabaikannya. Dua-duanya menghasilkan angka yang masuk akal, salah, dan tidak melapor.

Yang belum didukung ditolak, bukan dilewati — dan itu bukan kehati-hatian: melewati seksi impor akan menggeser indeks setiap fungsi, jadi tiap call sesudahnya memanggil fungsi yang salah tanpa suara. Impor, call_indirect, SIMD, tipe referensi, dan blok multi-value semuanya menolak dengan menyebut apa yang tidak didukung.

Python: pustaka apa saja, dengan satu syarat

Jawaban singkatnya ya — dan alasannya penting: jembatannya tidak punya dukungan per-pustaka yang harus ditulis satu per satu. Ia menjalankan sebuah worker Python dan berbicara dengannya, jadi apa pun yang bisa di-import bisa dipakai — ke-24 fungsi py:: itu berbicara ke Python, bukan ke sebuah daftar pustaka yang didukung. Yang menentukan bukan pustakanya, melainkan nilai yang menyeberang.

Diukur langsung terhadap pustaka nyata di mesin ini: math, json, re, datetime, collections, hashlib, statistics, itertools, numpy, pandas, scipy, sqlite3 (termasuk koneksi dan cursor yang berkeadaan), dan PIL (objek gambar) — semuanya bekerja, termasuk keyword argument dan objek yang tinggal di sisi Python.

ffi_py.jrg
package main

fn kali_sepuluh(x) { return x * 10 }

fn kerja() -> Result<string, string> {
    py::import("pandas")?

    // Handle: DataFrame-nya TINGGAL di Python, yang menyeberang cuma jawabannya.
    let d = py::ref("pandas.DataFrame({'x':[3,1,2]})")?

    // Keyword argument.
    let urut = py::ref_method_kw(d, "sort_values", ["x"], [["ascending", false]])?

    // Fungsi JRG dipakai Python sebagai callback.
    let kolom = py::ref_item(urut, "x")?
    wok(py::method(kolom, "apply", kali_sepuluh)?)

    return Ok("selesai")
}

pub fn main() {
    match kerja() { Ok(v) => wok(v), Err(e) => wok("gagal:", e) }
}

Nilai atau handle — dan kenapa itu diputuskan eksplisit

Sebuah hasil yang bisa dikonversi datang sebagai nilai JRG biasa. Yang tidak bisa datang sebagai handle: set, bytes, complex, objek kelas apa pun. Handle tetap bisa dipanggil method-nya, diteruskan sebagai argumen, dan dilepas otomatis saat pemegang terakhirnya mati. Yang tidak pernah terjadi: sebuah objek berubah jadi teks repr() yang menyamar sebagai data.

Pilihannya juga tidak pernah ditentukan besar data. py::ref(...) meminta handle meski konversinya mungkin — itulah caranya menahan sebuah array sejuta elemen tetap di sisi Python. Ambang berdasarkan ukuran akan membuat tipe kembalian bergantung pada isi berkas yang sedang dibaca.

Dari PythonTiba sebagai
int, float, bool, str, Noneint, float, bool, string, nil
list, tuplearray
dictdict
NaN, inf, -inffloat (utuh, dua arah)
set, bytes, complex, objekhandle py::
int di luar 128 bitditolak, dengan sebabnya

Baris terakhir itu disengaja. Integer Python tidak terbatas lebarnya; int JRG 128 bit. Membulatkannya diam-diam jadi float mengembalikan angka yang salah sambil terlihat berhasil, jadi ia ditolak dan pesannya menyebutkan jalan keluarnya (str(x) kalau angkanya harus utuh).

Batasnya, terus terang

py:: hanya jalan di interpreter. Bukan karena belum sempat: protokolnya perlu memanggil balik sebuah fungsi JRG di tengah pekerjaan Python — itulah yang membuat sorted(key=f) dan df.apply(f) bekerja — dan hanya interpreter yang bisa melakukannya. Menjalankannya lewat --vm ditolak dengan alasan itu, bukan dengan pesan yang tidak bisa dibedakan dari salah ketik.

Sebuah panggilan py:: dari dalam callback juga ditolak, dan itu satu-satunya jawaban yang jujur: worker-nya sedang berhenti menunggu jawaban callback itu sendiri, jadi permintaan baru tidak akan pernah dilayani. Dibiarkan, programnya tidak akan pernah selesai — kegagalan tanpa pesan, tanpa lokasi, tanpa apa pun untuk dilacak.

Dependensinya diurus jrgm: ia memindai seluruh berkas .jrg di proyek (bukan cuma entry-nya), mencari py::import, dan memasang yang belum ada — dengan tabel yang memetakan nama modul ke nama paketnya, karena cv2 dipasang dari opencv-python dan menebaknya berarti memasang paket yang keliru.

Tujuh kernel, masing-masing ditulis empat kali dengan algoritma yang sama persis dalam JRG, C, Rust, dan Go. Tiap program mencetak checksum yang diverifikasi sama sebelum waktunya dihitung.

KernelJRGCRustGo
numeric_kernel732748724725
array_sort1060880527980
struct_particles10449819806030
string_ops516147179338
call_recursion595589694879
match_dispatch7885858471140
mixed_report523243369438

Angka di atas adalah putaran terbaik dalam milidetik, makin kecil makin baik. Dua jalan suite terpisah, masing-masing tiga putaran, diambil yang terbaik dari keenamnya, disematkan ke CPU 0, pada Intel Core i5-1235U dengan clang 21.1.8, rustc 1.97.1, dan go 1.26.5. Dua jalan, bukan satu, karena sekali jalan sempat menunjukkan JRG mengungguli C pada call_recursion dan jalan berikutnya membalikkannya — di lantai kebisingan mesin ini itu seri, dan satu jalan akan menerbitkan klaim yang salah.

Bacaan jujurnya

  • Pada kerja numerik murni, pemanggilan fungsi, dan struct by-value, JRG praktis setara C: selisihnya 1–2%, masih di bawah lantai kebisingan mesin ini.
  • Pada struct_particles ia hampir enam kali lebih cepat dari Go, dan tertinggal 6% dari C — selisih yang tepat berada di lantai kebisingan mesin ini, jadi dibaca sebagai setara.
  • Pada kerja yang banyak mengalokasi (string_ops dan mixed_report) ia 2,2 sampai 3,5 kali lebih lambat. Penyebabnya diketahui dan tercatat di bawah.
  • Pada array_sort, Rust dua kali lebih cepat dari semuanya. JRG membayar pemeriksaan batas indeks yang C tidak bayar. Itu harga sebuah jaminan, bukan kelalaian.
Ukuran biner, di-stripJRGCRustGo
numeric_kernel18,3 KB14,2 KB335 KB1,5 MB
!
Kenapa yang banyak alokasi lebih lambat

Backend LLVM tidak punya destruktor, jadi setiap string hasil runtime dibocorkan dengan sengaja. Alokatornya diganti jadi bump allocator, dan itu menghapus biaya-nya: string_ops turun dari 721 ke 509 ms dan RSS puncaknya dari 147,7 ke 57,0 MB. Yang tidak terhapus adalah masa hidup-nya.

Penyebabnya alokasi memori, bukan algoritmanya: satu jalan string_ops melakukan 4,8 juta alokasi. Angka itu menempel pada jumlah operasi string, bukan pada pertumbuhan buffer. Memperbaikinya butuh pelacakan kepemilikan supaya nilai sementara bisa dibebaskan. Itu pekerjaan desain, dan dicatat begitu alih-alih ditambal.

JRG masih eksperimental. Bagian ini bukan basa-basi kerendahhatian. Tiap butir di bawah adalah hal yang benar-benar akan Anda tabrak.

Backend LLVM: 11 dari 11

Sempat sembilan, dan yang menahannya dicatat sebagai satu hal: pengetikan dinamis. Diukur, penghalangnya sebelas — dan sepuluh di antaranya baru terlihat sesudah yang di depannya ditutup.

// Tiga bentuk pengetikan dinamis, semuanya jalan di ketiga mesin:
let d = {"nama": "budi", "umur": 17}   // dict bernilai campur tipe

var jejak = []                          // tingkat modul, tipe elemen berganti

var x = 1
x = "teks"
wok(x, typeof(x))                       // teks string
x = 2
wok(x + 1)                             // 3 — golongannya dibaca saat jalan

Ketiganya butuh nilai bertag, dan keputusan itu diambil — bukan dihindari. Aritmetika dan perbandingan atasnya dikirim berdasarkan golongan yang dibaca saat jalan, jadi x + 1 tetap berarti apa yang tertulis. Yang penting bukan tagnya, melainkan siapa yang membayarnya: sebuah var yang tidak pernah berganti tipe tetap telanjang, dan {"a": 1, "b": 2} tetap dict<int>. Bukan harapan — ketujuh kernel benchmark menghasilkan LLVM IR yang identik sebelum dan sesudah kecuali satu baris — deklarasi tipe %JrgAny, yang tidak menghasilkan satu instruksi pun. (Mula-mula ini dipublikasikan sebagai “byte-nya identik”; pengukurannya ternyata membandingkan berkas basi dengan dirinya sendiri, dan pengukuran ulang yang benar itulah yang menemukan sebuah regresi 1,8x yang sempat disembunyikannya.), jadi kode mereka memang tidak berubah sama sekali.

Untuk dict bernilai campur, tipe tiap kuncinya diingat ikut tipenya. Itu yang membuat bersarang["angka"][1] bisa diindeks sama sekali: tag menyebut golongan (slice), tidak pernah tipe elemen. Pembacaan dengan kunci yang dihitung saat jalan tetap bertag, dan pemakaian yang menuntut tipe elemen di situ ditolak keras — batas yang disebutkan, bukan yang disembunyikan.

Sebelas dari sebelas berarti paritas konformans, bukan paritas fitur. Yang masih belum ada di backend ini: pewarisan struct, modul antar-berkas, dan concurrency — yang terakhir ditolak terang-terangan, dan penolakan itu menanggung beban: bump allocator dan registri jawa:: bebas-kunci justru karena ia.

Jalan

Interpreter

11/11 konformans. Acuan semantik untuk dua mesin lain.

Jalan

Bytecode VM

11/11 konformans. Nol asimetri yang diketahui terhadap interpreter.

Jalan

LLVM

11/11 konformans, byte per byte. Yang belum ada: pewarisan struct, modul antar-berkas, concurrency.

Ditolak

Concurrency di LLVM

spawn, channel, dan select ditolak terang-terangan. Penolakan itu menanggung beban: bump allocator dan registri jawa:: bebas-kunci justru karena ia.

Beberapa lagi yang perlu diketahui

  • Anotasi tipe tidak ditegakkan. let x: int = "teks" diterima ketiga mesin. Yang diperiksa cuma keberadaan nama tipenya.
  • Trait bound tidak ditegakkan. Sama: namanya dicek, batasannya tidak. Pesannya mengatakan itu terus terang supaya "namanya lolos" tidak terbaca sebagai "batasannya berlaku".
  • Modul sebaris (mod) dan alias tipe (type) ditolak. Keduanya dulu diurai lalu dibuang tanpa pesan. Untuk modul, pakai berkas terpisah plus import. Bentuk itu bekerja penuh.
  • Tidak ada operator yang boleh berdiri sendiri di awal baris kecuali yang sudah didaftar. .method(), +, &&, $, dan else boleh; - dan * tidak, karena keduanya juga bisa berada di posisi prefix.
  • Dependency berjalan lewat path lokal, bukan lewat registry. bantu = { path = "../bantu" } di jrgm.toml bekerja penuh, transitif, di kedua mesin acuan: jrgm add --path, remove dan tree nyata, dan graf yang rusak ditolak sambil menyebut sebabnya — melingkar (rantainya ikut dicetak), path yang tidak berisi jrgm.toml, atau dua paket yang menyediakan modul bernama sama. Yang belum ada registry-nya: jrgm add tanpa --path, update dan publish menolak, karena sebuah nama telanjang memang tidak bisa dicari di mana pun — menuliskan barisnya toh cuma jadi kegagalan yang ditunda. Sampai fase PERKAKAS-1 keempatnya mencetak kalimat yang terbaca seperti laporan sukses lalu keluar dengan kode 0.
  • jrgm lint dan publish belum ada isinya, dan menolak sambil menyebut alasannya. jrgm test nyata: berkas ujinya di tests/*.jrg, sebuah fungsi dianggap test kalau namanya diawali test_ atau uji_, dan tidak perlu menulis main. Kerangka import "std/testing" tetap ada untuk sekumpulan cek yang mau dilaporkan sekaligus — di interpreter maupun VM, dari sumber yang sama yang ditanam di dalam biner. Backend LLVM tidak bisa memuatnya, dan itu bukan soal std/: ia tidak memuat modul antar-berkas sama sekali.
+
Aturan yang dipegang

Apa pun yang tidak bisa dievaluasi dengan bermakna harus menghasilkan diagnostik, tidak pernah 0, nil, false, atau nilai kosong yang kelihatan masuk akal. Sebagian besar pekerjaan di proyek ini justru mengejar tempat-tempat yang melanggar aturan itu.