Zig dapat mengeksekusi sebagian kode pada waktu kompilasi (compile time), yaitu sebelum biner program dihasilkan. Kemampuan ini disebut comptime dan menjadi fondasi pemrograman generik, refleksi tipe, serta pembentukan tabel konstanta pada jalur Zig. Topik ini membahas deklarasi mana yang dievaluasi oleh kompilator, semantik tipe comptime_int, dan cara membuktikan bahwa sebuah nilai sudah selesai dihitung sebelum program mulai berjalan. Hasil akhirnya adalah kemampuan memindahkan perhitungan tertentu ke waktu kompilasi secara sengaja, bukan secara kebetulan.
Konsep#
Deklarasi tingkat container dievaluasi saat kompilasi#
Aturan dasarnya terletak pada lokasi deklarasi. Semua konstanta yang ditulis di tingkat container (container), yaitu di luar fungsi mana pun, dievaluasi sepenuhnya oleh kompilator. Panggilan fungsi di dalam deklarasi tersebut ikut berlangsung di dalam kompilator selama seluruh argumennya diketahui pada waktu kompilasi. Blok comptime di tubuh fungsi memberikan perlakuan yang sama pada pernyataan yang ditempatinya.
Program berikut menghitung faktorial 6 pada waktu kompilasi. Konstanta f6 berada di tingkat container, sehingga perulangan di dalam fungsi faktorial dijalankan oleh kompilator, bukan oleh CPU. Blok comptime kemudian memeriksa nilai faktorial 5 dengan std.debug.assert, dan pemeriksaan itu juga terjadi sebelum program ada.
const std = @import("std");
fn faktorial(n: u64) u64 {
var hasil: u64 = 1;
var i: u64 = 2;
while (i <= n) : (i += 1) {
hasil *= i;
}
return hasil;
}
// deklarasi tingkat container selalu dievaluasi saat kompilasi
const f6 = faktorial(6);
pub fn main() void {
std.debug.print("faktorial 6 = {d}\n", .{f6});
comptime {
// asersi ini diperiksa oleh kompilator, bukan oleh program
std.debug.assert(faktorial(5) == 120);
}
std.debug.print("tipe literal 720 = {s}\n", .{@typeName(@TypeOf(720))});
}faktorial 6 = 720
tipe literal 720 = comptime_intBaris const f6 = faktorial(6) adalah bukti evaluasi waktu kompilasi yang paling mudah dilihat. Ubah panggilannya menjadi faktorial(21), lalu kompilasi berhenti dengan pesan error: overflow of integer type 'u64' beserta catatan called at comptime here. Kegagalan itu tidak mungkin terjadi pada program yang berjalan, karena nilai tidak muat pada u64 sudah tertangkap sebelum biner dibuat. Baris terakhir melengkapi bukti tersebut dari sisi tipe, yaitu @TypeOf(720) melaporkan bahwa literal 720 bertipe comptime_int, sebuah tipe yang hanya ada pada waktu kompilasi.
# jalankan program dari direktori topik
zig run faktorial-comptime.zigfaktorial 6 = 720
tipe literal 720 = comptime_intTipe comptime_int dan presisi arbitrer#
Setiap literal integer tanpa anotasi tipe bertipe comptime_int. Tipe ini hanya hidup pada waktu kompilasi, lebarnya tidak dibatasi, dan aritmetikanya tidak pernah meluap. Sifat itu sudah tersentuh pada pembahasan operator pembungkusan untuk menjelaskan mengapa literal tidak pernah membungkus. Topik ini memandang sifat yang sama dari sisi lain, yaitu presisi arbitrer (arbitrary precision) yang memungkinkan perhitungan berukuran besar tetap akurat.
Fungsi pada program berikut bekerja sepenuhnya dengan comptime_int. Parameternya bertipe comptime_int, sehingga hanya menerima konstanta waktu kompilasi, dan hasil perhitungannya juga comptime_int. Faktorial 25 dihitung tanpa kehilangan satu digit pun, meskipun nilainya jauh melampaui kapasitas u64 yang hanya sekitar 1,8 × 10^19.
const std = @import("std");
// comptime_int hanya menerima konstanta waktu kompilasi
fn faktorialBesar(n: comptime_int) comptime_int {
var hasil: comptime_int = 1;
var i: comptime_int = 2;
while (i <= n) : (i += 1) {
hasil *= i;
}
return hasil;
}
pub fn main() void {
const f25 = faktorialBesar(25);
std.debug.print("25! = {d}\n", .{f25});
std.debug.print("tipe hasil = {s}\n", .{@typeName(@TypeOf(f25))});
const muat: u128 = f25;
std.debug.print("25! pada u128 = {d}\n", .{muat});
}25! = 15511210043330985984000000
tipe hasil = comptime_int
25! pada u128 = 15511210043330985984000000Konversi comptime_int ke tipe konkret berlangsung implisit selama nilainya muat, seperti terlihat pada const muat: u128 = f25. Apabila nilai tidak muat, kompilator menolaknya dengan pesan yang menyebut angka lengkapnya.
// tidak-dijalankan
const tidak_muat: u64 = f25; // 25! melebihi kapasitas u64Pesan errornya adalah type 'u64' cannot represent integer value '15511210043330985984000000'. Penolakan semacam ini terjadi saat kompilasi, sehingga kekeliruan lebar tipe tidak pernah sampai ke program yang berjalan. Turunkan nilai perhitungan, perbesar tipe sasarannya, atau pertahankan hasil sebagai comptime_int selama nilai itu masih dipakai pada waktu kompilasi.
Contoh Lengkap#
Menentukan kapan kode berjalan#
Kompilator mengeksekusi kode pada tiga tempat, yaitu deklarasi tingkat container, blok comptime, dan ekspresi yang diberi kata kunci comptime. Selain ketiganya, kode berjalan pada waktu berjalan (runtime) dan dieksekusi oleh CPU. Karena lokasi panggilan menentukan jalannya eksekusi, panggilan fungsi yang sama dapat menghasilkan kegagalan pada waktu yang berbeda.
// tidak-dijalankan
// panggilan pada deklarasi tingkat container dievaluasi kompilator
const f21 = faktorial(21); // error kompilasi, overflow terdeteksi lebih dulu
pub fn main() void {
// panggilan lokal dieksekusi CPU saat program berjalan
const g21 = faktorial(21); // panic integer overflow saat program berjalan
}Kedua baris pada fragmen menghitung nilai yang sama, tetapi kegagalannya muncul di tempat yang berbeda. Baris pertama menghentikan kompilasi dengan pesan error, sedangkan baris kedua lolos kompilasi kemudian menghentikan program dengan panic integer overflow pada mode Debug. Diagram berikut merangkum dua jalur eksekusi tersebut untuk pemanggilan fungsi yang sama.
Pemindahan perhitungan ke waktu kompilasi dilakukan dengan tiga kriteria, yaitu seluruh masukan berupa konstanta, hasilnya diketahui tidak berubah, dan pemrosesannya tidak menyentuh I/O. Kriteria ketiga penting karena kode pada waktu kompilasi tidak memiliki akses ke terminal, berkas, maupun memori runtime. Perhitungan yang lolos ketiga kriteria, misalnya konversi satuan dengan faktor tetap, layak diikat sebagai konstanta tingkat container.
Kesalahan umum#
Kesalahan pada topik ini berpusat pada asumsi tentang kode mana yang benar-benar dieksekusi kompilator. Tiga kasus berikut paling sering muncul.
- Menganggap pengikatan
constlokal sebagai pemicu evaluasi waktu kompilasi. Deklarasiconst g21 = faktorial(21)di dalammaintetap berjalan pada CPU, sehingga overflow baru muncul sebagai panic saat program berjalan. Pindahkan deklarasi ke tingkat container atau tambahkan kata kuncicomptimepada ekspresinya agar evaluasi berpindah ke kompilator. - Meletakkan operasi I/O di dalam blok
comptime. Pemanggilanstd.debug.printdi dalam blok ditolak dengan pesanunable to resolve comptime value. Struktur yang benar adalah menghitung nilai di dalam blok, menyimpannya pada konstanta, kemudian mencetak konstanta tersebut di luar blok. - Memaksa konversi
comptime_intke tipe yang tidak muat. Kompilator menolak dengan pesan yang menyebut nilai lengkapnya, misalnyatype 'u64' cannot represent integer value .... Pilih lebar tipe yang lebih besar atau tulis ulang perhitungan agar hasilnya berada di dalam rentang tipe sasaran.
Ringkasan#
- Konstanta di tingkat container selalu dievaluasi saat kompilasi, dan blok
comptimedi tubuh fungsi memberikan perlakuan yang sama pada pernyataannya. - Literal integer bertipe
comptime_intdengan lebar tak terbatas, sehingga aritmetikanya tidak pernah meluap dan konversinya implisit selama nilai muat pada tipe sasaran. - Lokasi panggilan menentukan waktu eksekusi, yaitu deklarasi container berjalan di dalam kompilator, sedangkan panggilan lokal berjalan pada CPU.
- Kode waktu kompilasi tidak boleh menyentuh I/O, sehingga pemeriksaan nilai dilakukan dengan
std.debug.assertdi dalam blokcomptime.
Referensi#
- comptime — Zig Language Reference
— Aturan evaluasi waktu kompilasi beserta semantik
comptime_int. - Introducing-the-Compile-Time-Concept — Zig Language Reference — Pengantar resmi konsep waktu kompilasi pada bahasa ini.