LLVM IR tidak mengubah setiap kondisi aritmetika atau pointer yang tidak valid menjadi undefined behavior seketika. Banyak instruksi justru menghasilkan nilai poison. Nilai itu dapat mengalir ke instruksi berikutnya, sehingga optimizer tetap dapat bertumpu pada janji seperti “penjumlahan ini tidak overflow” tanpa menjadikan pelanggaran janji tersebut sebagai undefined behavior tepat pada instruksi asalnya.

Perbedaan ini merupakan bagian dari semantik LLVM, bukan detail implementasi optimizer. Frontend yang menghasilkan nsw, nuw, inbounds, noundef, atau constraint terkait sedang menyatakan fakta yang boleh dipercaya oleh pass berikutnya.

Poison adalah undefined behavior yang ditunda

Perhatikan penjumlahan signed berikut:

define i32 @next(i32 %x) {
entry:
  %y = add nsw i32 %x, 1
  ret i32 %y
}

Flag nsw menyatakan bahwa penjumlahan signed tidak mengalami wrap. Jika %x bernilai 2147483647, hasil matematisnya tidak dapat direpresentasikan oleh i32. LLVM tidak mewajibkan instruksi add itu sendiri langsung memicu undefined behavior. Hasilnya menjadi poison.

Pemisahan ini mendukung transformasi spekulatif. Optimizer dapat memindahkan atau mengevaluasi instruksi pada jalur yang hasilnya tidak pernah dipakai. Poison membiarkan hasil yang tidak valid tetap ada sambil menunda konsekuensi yang lebih kuat sampai operasi berikutnya memerlukan nilai yang dapat digunakan.

Pola yang sama tidak terbatas pada add nsw. Flag no-wrap serta sejumlah constraint pointer atau metadata dapat membuat instruksi menghasilkan poison ketika kondisi yang dideklarasikan tidak terpenuhi.

Poison merambat melalui operasi biasa yang bergantung padanya

Sebagian besar instruksi biasa yang menerima poison akan menghasilkan poison juga:

%a = add nsw i32 %x, 1
%b = mul i32 %a, 4
%c = icmp sgt i32 %b, 100

Jika %a poison, %b juga poison, lalu %c ikut poison. Bahkan operasi yang tampak menghapus input tidak otomatis menghapus poison:

%a = add nsw i32 %x, 1
%z = and i32 %a, 0

Untuk integer konkret, %z bernilai nol. Jika %a poison, %z tetap poison. Menggantinya tanpa syarat dengan nol akan membuang konsekuensi semantik dari kontrak nsw yang telah dilanggar.

Karena itu, instruksi yang akhirnya membuat eksekusi undefined dapat berada jauh dari instruksi yang pertama kali menghasilkan poison.

Pemakaian sensitif mengubah poison menjadi undefined behavior seketika

LLVM mendefinisikan beberapa posisi operand yang tidak dapat mempertahankan poison sebagai keadaan tertunda. Kondisi branch adalah salah satunya:

define i32 @classify(i32 %x) {
entry:
  %y = add nsw i32 %x, 1
  %positive = icmp sgt i32 %y, 0
  br i1 %positive, label %yes, label %no

yes:
  ret i32 1

no:
  ret i32 0
}

Jika %y poison, %positive juga poison. Memakai nilai tersebut sebagai kondisi br menghasilkan undefined behavior.

Dereference pointer membentuk batas lain:

%p = getelementptr inbounds i32, ptr %base, i64 %index
%v = load i32, ptr %p

GEP dengan inbounds membawa constraint atas perhitungan pointer. Pelanggaran terhadap constraint itu dapat menghasilkan poison. Menghitung pointer saja bukan akses memori, tetapi memakai pointer poison sebagai operand pointer pada load menghasilkan undefined behavior.

Lokasi kegagalan yang terlihat dapat berupa branch, load, store, call, division, atau operasi sensitif lain meskipun pelanggaran kontrak awal terjadi lebih jauh sebelumnya.

select sengaja bergantung pada jalur yang dipilih

Poison tidak menodai setiap instruksi dengan cara yang sama. select merupakan pengecualian penting:

%r = select i1 %cond, i32 %a, i32 %b

Jika %cond terdefinisi dengan baik, poison pada nilai yang tidak dipilih tidak membuat %r menjadi poison. Nilai yang dipilih menentukan hasil. Namun poison pada kondisi itu sendiri tetap membuat select menghasilkan poison.

Perbedaan ini penting ketika optimisasi mengganti control flow dengan select atau memindahkan komputasi di sekitarnya. Kebenaran transformasi bergantung pada semantik poison, bukan hanya pada nilai konkret yang tampak pada eksekusi biasa.

freeze mengubah ketidakpastian tertunda menjadi satu nilai stabil

LLVM menyediakan freeze ketika sebuah transformasi memerlukan nilai yang well-defined meskipun inputnya mungkin undef atau poison:

%raw = add nsw i32 %x, 1
%safe = freeze i32 %raw
%cmp = icmp sgt i32 %safe, 0
br i1 %cmp, label %yes, label %no

Jika %raw sudah berupa nilai normal, freeze mengembalikannya tanpa perubahan. Jika %raw poison atau undef, freeze menghasilkan nilai arbitrer dengan tipe yang sama. Aturan pentingnya adalah setiap penggunaan hasil freeze yang sama selalu melihat nilai pilihan yang sama.

freeze bukan perbaikan umum untuk kontrak frontend yang salah. Menambahkannya di mana-mana akan melemahkan fakta yang berguna dan dapat menghalangi optimisasi yang valid. Perannya lebih sempit: sebuah transformasi dapat secara eksplisit menghentikan deferred undefined behavior agar tidak merambat ke pemakaian yang membutuhkan nilai stabil.

undef dan poison tidak dapat dipertukarkan

LLVM juga memiliki undef, tetapi semantiknya berbeda. Nilai undef dapat memberikan bit pattern arbitrer, dan penggunaan yang berbeda dapat melihat nilai yang berbeda. Dokumentasi LLVM saat ini mendeprekasi penggunaan umum undef dan terutama mempertahankannya untuk kasus yang masih memerlukannya, seperti memori yang belum diinisialisasi.

Poison lebih kuat. Nilai ini mencatat pelanggaran constraint semantik dan merambat melalui sebagian besar instruksi yang bergantung padanya. Properti yang lebih kuat tersebut memungkinkan optimisasi yang tidak dapat dibenarkan hanya dengan undef.

freeze membuat perbedaannya terlihat:

%u = freeze i32 undef
%a = add i32 %u, %u

Kedua penggunaan %u melihat nilai yang sama karena hasil yang telah di-freeze stabil. Tanpa freeze, penggunaan transitif yang melibatkan undef tidak secara umum diwajibkan menghasilkan nilai yang sama.

Flag frontend adalah kontrak semantik

Banyak bug poison berasal sebelum optimisasi. Frontend menghasilkan sebuah flag karena kondisinya biasanya benar, bukan karena semantik source language menjaminnya.

%sum = add nsw i32 %a, %b

Ini bukan sekadar cara yang lebih cepat untuk menulis penjumlahan integer. Instruksi tersebut menyatakan bahwa signed overflow tidak terjadi. Jika source language mendefinisikan wrapping, atau frontend belum membuktikan kondisi no-overflow, penambahan nsw mengubah semantik IR.

Disiplin yang sama berlaku pada inbounds untuk getelementptr. Tanpa inbounds, GEP dapat menghitung alamat di luar object tanpa melakukan dereference. Menambahkan inbounds membawa constraint yang lebih kuat, dan pelanggaran terhadapnya dapat menghasilkan poison.

Pass optimisasi berhak mempercayai properti tersebut. Transformasi lanjutan yang terlihat mengejutkan dapat benar menurut kontrak IR yang dideklarasikan meskipun annotation dari frontend keliru.

Poison dengan demikian memisahkan invariant yang rusak dari lokasi kegagalan akhirnya. Pemisahan ini memberi LLVM ruang untuk melakukan optimisasi agresif, tetapi juga membuat pembentukan IR harus presisi: flag dan attribute adalah kontrak, poison membawa pelanggarannya ke depan, dan freeze menjadi batas eksplisit untuk mengubah ketidakpastian tertunda menjadi satu nilai stabil.