Mengintegrasikan LLVM sebagai backend dynamic expression engine memungkinkan evaluasi filter query, kalkulasi finansial, atau transformasi data berjalan mendekati performa native C/C++. Masalah muncul saat pipeline optimasi level default (misal -O2 atau -O3) diterapkan langsung pada LLVM Intermediate Representation (IR) yang dihasilkan compiler frontend custom. Pass optimasi agresif seperti instcombine dan loop-vectorize sering memicu silent data corruption atau SIGSEGV karena mengasumsikan properti pointer standar yang tidak terpenuhi oleh engine host.

Gejala: SIGSEGV Intermiten dan Nilai Kalkulasi Tak Valid

Pada implementasi expression engine backend, ekspresi dinamis umumnya di-compile menjadi fungsi callable beralamat runtime via LLVM ORC JIT. Error paling sering termanifestasi sebagai:

  • Runtime Crash (SIGSEGV): Terjadi ketika pass optimasi mengasumsikan pointer input selalu non-null dan dereferensial (misalnya akibat penandaan inbounds pada getelementptr), memicu speculative load sebelum pengecekan kondisi null.
  • Silent Memory Corruption: Terjadi ketika loop vectorizer mengeksekusi SIMD instructions pada buffer array dinamis tanpa mengetahui bahwa pointer input dan pointer output memiliki potensi overlap (aliasing hazard).

Reproduksi dan Isolasi Root Cause dengan Toolchain LLVM

Langkah awal debugging backend JIT adalah men-dump IR yang bermasalah langsung sebelum pipeline pass berjalan. Dump module ke disk menggunakan API LLVM:

std::error_code ec;
llvm::raw_fd_ostream out("failing_expr.ll", ec, llvm::sys::fs::OF_None);
module->print(out, nullptr);

1. Isolasi Modul Menggunakan opt dan llc

Verifikasi apakah bug berasal dari IR transformation (pass opt) atau backend code generation (llc). Jalankan pass optimasi standar secara manual:

opt -S -passes='default<O2>' failing_expr.ll -o optimized.ll
llc -O0 optimized.ll -o output.s
clang output.s test_runner.c -o test_runner
./test_runner # Jika crash, bug berada pada level IR pass opt

2. Minimalisasi IR dengan llvm-reduce

File IR hasil dump dynamic engine sering berukuran ribuan baris. Gunakan llvm-reduce dengan script penentu status keluar (exit code 1 jika crash berulang) untuk memangkas instruksi tidak relevan:

#!/bin/bash
# check.sh: Menguji ada tidaknya crash
opt -S -passes='instcombine' $1 -o test.ll 2>/dev/null || exit 0
llc test.ll -o test.s 2>/dev/null
clang test.s runner.c -o runner 2>/dev/null
./runner > /dev/null 2>&1
# Return 1 jika segfault (kode 139)
if [ $? -eq 139 ]; then exit 1; else exit 0; fi

Jalankan reduksi:

llvm-reduce --test=check.sh failing_expr.ll

Analisis Kasus: Null Assumption dan InstCombine

Contoh umum crash terjadi ketika frontend engine meng-generate instruksi getelementptr inbounds untuk mengecek data array di balik pointer opsional (nullable context struct). Berikut adalah IR yang memicu speculative load:

; IR Bermasalah: frontend menandai inbounds pada pointer yang bisa bernilai null
define double @eval_expression(ptr %ctx, ptr %row) {
entry:
  %is_null = icmp eq ptr %ctx, null
  br i1 %is_null, label %fallback, label %compute

compute:
  ; 'inbounds' memberitahu LLVM bahwa pointer ini valid, mengizinkan reordering
  %field_ptr = getelementptr inbounds double, ptr %ctx, i64 2
  %val = load double, ptr %field_ptr, align 8
  ret double %val

fallback:
  ret double 0.0
}

Pass instcombine memindahkan instruksi load melewati branch %is_null ke blok entry karena berasumsi akses memory tersebut sah dan tidak memiliki side-effect. Akibatnya, saat runtime menerima ctx = nullptr, terjadi dereferensi null pointer langsung sebelum pengecekan logika.

Langkah Perbaikan pada Backend JIT

1. Koreksi Pointer Semantics dan Memory Layout

Hapus flag inbounds dari pembuatan GEP jika pointer dasar belum terbukti non-null pada blok bersangkutan. Jika dereferensi null diizinkan menghasilkan poison value tanpa trapping instruction, gunakan metadata non-temporal atau ubah urutan blok kontrol dasar.

; IR yang Diperbaiki: Hapus inbounds dan gunakan explicit branch barrier
define double @eval_expression(ptr %ctx, ptr %row) {
entry:
  %is_null = icmp eq ptr %ctx, null
  br i1 %is_null, label %fallback, label %compute

compute:
  ; GEP tanpa inbounds mencegah compiler mengasumsikan non-null sebelum eksekusi
  %field_ptr = getelementptr double, ptr %ctx, i64 2
  %val = load double, ptr %field_ptr, align 8, !dereferenceable !0
  ret double %val

fallback:
  ret double 0.0
}

!0 = !{i64 8}

2. Penambahan Metadata Aliasing (TBAA & noalias)

Jika problem berupa silent data corruption pada ekspresi bertipe loop akumulasi, berikan batasan aliasing eksplisit menggunakan metadata noalias scope agar vectorizer tidak salah reorder.

define void @vector_eval(ptr noalias %dst, ptr noalias %src, i64 %len) {
  ; LLVM loop-vectorize dapat bekerja tanpa runtime checks redundan
  ; dan aman dari memory overlap hazard
}

3. Kustomisasi Optimization Pipeline

Jangan menerapkan PassBuilder::buildPerModuleDefaultPipeline(OptimizationLevel::O3) secara mentah. Untuk expression engine backend, matikan loop vectorization dan memory sinking agresif jika layout memory dinamis tidak mendukungnya:

llvm::LoopAnalysisManager LAM;
llvm::FunctionAnalysisManager FAM;
llvm::CGSCCAnalysisManager CGAM;
llvm::ModuleAnalysisManager MAM;

llvm::PassBuilder PB;
PB.registerModuleAnalyses(MAM);
PB.registerFunctionAnalyses(FAM);
PB.crossRegisterProxies(LAM, FAM, CGAM, MAM);

llvm::FunctionPassManager FPM;
// Gunakan pass hemat risiko untuk dynamic expression engine
FPM.addPass(llvm::SROAPass());
FPM.addPass(llvm::EarlyCSEPass());
FPM.addPass(llvm::SimplifyCFGPass());
FPM.addPass(llvm::InstCombinePass()); // Aman setelah koreksi 'inbounds'
FPM.addPass(llvm::ReassociatePass());
FPM.addPass(llvm::GVNPass());
FPM.addPass(llvm::SCCPPass());

Tips Produksi: Selalu aktifkan llvm::VerifierPass sebelum dan sesudah eksekusi pipeline pass custom untuk memverifikasi integritas tipe, structural constraints, dan SSA form.