TypeScript di atas LLVM

Bagaimana Perry menurunkan bahasa yang dirancang untuk engine JIT menjadi LLVM IR — monomorphization, NaN-boxing, inline lowering — dan mengapa ia meninggalkan Cranelift.

Mengapa LLVM untuk TypeScript?

Kompiler ahead-of-time hidup dalam rezim yang berbeda dari JIT. JIT mengompilasi sambil pengguna menunggu, sehingga latensi kompilasi menjadi kendalanya. Kompiler AOT seperti Perry mengompilasi sekali — di mesin developer atau di CI — dan binary-nya dieksekusi jutaan kali setelahnya. Asimetri itulah yang membuat optimizer berat justru terbayar dengan sendirinya.

LLVM membawa dua dekade pekerjaan middle-end: loop vectorization, loop-invariant code motion, global value numbering, sparse conditional constant propagation, aggressive inlining, alias analysis. Tugas Perry adalah menyerahkan IR ke mesin itu agar benar-benar bisa dioptimasi — dan di sinilah informasi tipe TypeScript berperan.

Pipeline lowering

Source di-parse dengan SWC, lalu diturunkan menjadi high-level IR (HIR) bertipe, tempat keputusan-keputusan penting terjadi sebelum LLVM sempat melihat kodenya:

  • Monomorphization. Fungsi dan kelas generic dispesialisasi per instansiasi konkret, strategi yang sama seperti yang digunakan Rust dan C++. Stack<number> dan Stack<string> menjadi dua fungsi independen yang sepenuhnya bertipe — sehingga optimizer bekerja dengan tipe konkret, bukan blob dispatch generic, dan generic tidak memakan biaya sama sekali saat runtime.
  • Static dispatch. Ketika tipe receiver diketahui pada waktu kompilasi, pemanggilan metode dikompilasi menjadi pemanggilan langsung yang bisa di-inline oleh LLVM, bukan pencarian hash-table.
  • Direct field access. Field objek diselesaikan menjadi indeks waktu kompilasi, sehingga pembacaan properti adalah pemuatan dengan offset tetap — bukan pencarian dictionary.

NaN-boxing dan inline lowering

Ketika nilai bersifat dinamis, Perry menggunakan NaN-boxing: setiap nilai adalah word 64-bit. Double disimpan langsung; objek, string, boolean, null, dan undefined dikodekan ke dalam pola bit yang tidak terpakai dari IEEE 754 quiet NaN. Angka bersifat zero-cost — tanpa boxing, tanpa alokasi untuk aritmatika.

Tangkapannya adalah operasi pada nilai non-angka membutuhkan urutan bit unpack-operate-repack. Jika urutan itu hidup sebagai pemanggilan ke runtime yang dikompilasi terpisah, LLVM melihatnya sebagai black box buram dan tidak bisa mengoptimasi lintas batasnya. Karena itu Perry menghasilkan operasi hot — pembacaan properti, dispatch metode, alokasi objek — sebagai LLVM IR inline yang bisa digabung dan disederhanakan oleh optimizer. Alokasi objek, misalnya, dikompilasi menjadi alokasi bump thread-local inline:

LLVM IR — alokasi bump inline
%off_ptr = getelementptr i8, ptr %state, i64 8
%offset  = load i64, ptr %off_ptr        ; current bump offset
%new_off = add i64 %offset, 96           ; headers + 8 fields
%sz_ptr  = getelementptr i8, ptr %state, i64 16
%size    = load i64, ptr %sz_ptr         ; block capacity
%fits    = icmp ule i64 %new_off, %size
br i1 %fits, label %fast, label %slow

Mengapa bukan Cranelift?

Backend pertama Perry adalah Cranelift — codegen di balik wasmtime, dibangun untuk kompilasi yang cepat dan dapat diprediksi. Itu adalah titik awal yang tepat, dan Cranelift tetap menjadi pilihan yang sangat baik untuk JIT dan runtime yang di-sandbox. Dua hal memaksa peralihan ini:

  • Batas atas optimizer. Cranelift memang sengaja dirancang sebagai kompiler single-tier yang cepat: “kode yang cukup baik, dengan cepat,” yang merupakan trade-off yang tepat untuk JIT tapi salah untuk kompiler AOT yang nilai jualnya adalah performa native puncak.
  • arm64_32. Apple Watch menggunakan ABI (instruksi 64-bit, pointer 32-bit) yang tidak didukung Cranelift. Agar watchOS bisa menjadi target, LLVM diperlukan — dan mempertahankan dua backend berarti dua set bug, test, dan baseline performa.

Migrasi ini tidak gratis: rilis pertama yang hanya menggunakan LLVM membuat beberapa benchmark mengalami regresi hingga 70x karena operasi hot awalnya melewati pemanggilan helper runtime yang buram. Proses pemulihan — inline lowering, bump allocator di atas, batas inlining yang lebih baik — membawa backend ini melampaui angka Cranelift, dan pada akhirnya Perry mengalahkan Node.js di setiap benchmark dalam suite-nya, dengan kelipatan 1,7x hingga 24,6x dengan dua hasil seri (April 2026). Post-mortem lengkapnya layak dibaca: Dari Cranelift ke LLVM.

Menyelami lebih dalam

Halaman struktur internal kompiler membahas NaN-boxing, monomorphization, dan static dispatch lebih detail. Di blog, Mengoptimalkan Semuanya menelusuri pekerjaan optimasi rilis demi rilis, dan GC generasional, lazy JSON, dan benchmark yang tahan pemeriksaan menjelaskan cara kerja metodologi benchmark (RUNS=11, median + p95). Untuk gambaran yang lebih besar, mulai dari overview kompiler TypeScript native.

Lihat outputnya sendiri

perry compile main.ts — kode mesin native, tanpa engine yang menyertai.