Tantangan Inline Assembly pada Glasgow Haskell Compiler (GHC)

Penggunaan inline assembly atau C FFI primitif di GHC umumnya diterapkan pada komputasi performa tinggi seperti kriptografi atau manipulasi vektor SIMD. Karena GHC tidak memiliki parser inline assembly bawaan di level sintaksis Haskell murni, pola standar yang umum digunakan adalah menyisipkan assembly via C wrapper atau menggunakan backend LLVM (-fllvm) yang dipadukan dengan modul C/Cmm.

Masalah utama muncul saat kode dijalankan lintas arsitektur. Instruksi instruksi register spesifik pada x86_64 tidak valid untuk aarch64, dan kesalahan deklarasi clobbered registers dapat merusak runtime system (RTS) GHC secara senyap (silent corruption). Membangun continuous integration (CI) pipeline yang memvalidasi target arsitektur dan memeriksa integritas clobber list sebelum kompilasi penuh menjadi kebutuhan kritis.

Strategi Linting: Mendeteksi Masalah Register Clobber

Sebelum menjalankan kompilasi biner GHC yang memakan resource, linting statis berbasis skrip dapat mendeteksi pelanggaran arsitektur dan deklarasi register assembly GCC/Clang-style yang umum dipakai pada FFI wrapper.

Contoh skrip linter Bash berikut memvalidasi keberadaan penanda arsitektur serta memeriksa apakah register kritis seperti stack pointer (rsp/sp) atau register reservasi LLVM diubah tanpa deklarasi clobber yang benar.

#!/usr/bin/env bash
set -euo pipefail

TARGET_ARCH="${1:-x86_64}"
SRC_DIR="${2:-cbits}"

echo "[LINT] Memeriksa inline assembly untuk target: ${TARGET_ARCH} di folder ${SRC_DIR}..."
EXIT_CODE=0

# Validasi guard macro arsitektur
for file in $(find "${SRC_DIR}" -type f -name "*.c" -o -name "*.h"); do
    if grep -q "__asm__" "$file"; then
        if [ "${TARGET_ARCH}" = "x86_64" ] && ! grep -q -E "(__x86_64__|__amd64__)" "$file"; then
            echo "[ERROR] $file: Ditemukan inline asm tanpa guard __x86_64__"
            EXIT_CODE=1
        fi
        if [ "${TARGET_ARCH}" = "aarch64" ] && ! grep -q -E "(__aarch64__|__arm64__)" "$file"; then
            echo "[ERROR] $file: Ditemukan inline asm tanpa guard __aarch64__"
            EXIT_CODE=1
        fi
    fi
done

# Validasi clobber register berbahaya (modifikasi rsp/sp langsung dilarang)
for file in $(find "${SRC_DIR}" -type f -name "*.c"); do
    if grep -Pzo '(?s)__asm__.*?\([\s\S]*?:[\s\S]*?:[\s\S]*?:[\s\S]*?"(rsp|esp|sp)"' "$file" > /dev/null; then
        echo "[ERROR] $file: Terdeteksi clobber langsung pada stack pointer! Gunakan frame pointer aman."
        EXIT_CODE=1
    fi
done

if [ $EXIT_CODE -eq 0 ]; then
    echo "[LINT] Validasi assembly berhasil tanpa pelanggaran fatal."
fi

exit $EXIT_CODE

Konfigurasi GitHub Actions Matrix Build

Untuk memvalidasi target x86_64 dan aarch64 secara paralel tanpa membuang waktu build kompilasi GHC, konfigurasi GitHub Actions membutuhkan runner native (Ubuntu standard untuk x86_64 dan Ubuntu ARM runner untuk aarch64) serta caching dependencies via actions/cache.

Workflow berikut menjalankan linting kustom sebelum memicu cabal build dengan flag --ghc-options="-fllvm".

name: GHC Inline Assembly Validation

on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  validate-asm:
    name: Build & Test (${{ matrix.arch }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - arch: x86_64
            os: ubuntu-22.04
            llvm_version: "15"
          - arch: aarch64
            os: ubuntu-24.04-arm
            llvm_version: "15"

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Install Toolchain (LLVM & Clang)
        run: |
          sudo apt-get update
          sudo apt-get install -y llvm-${{ matrix.llvm_version }} clang-${{ matrix.llvm_version }}
          echo "/usr/lib/llvm-${{ matrix.llvm_version }}/bin" >> $GITHUB_PATH

      - name: Setup Haskell GHC
        uses: haskell-actions/setup@v2
        with:
          ghc-version: '9.6.4'
          cabal-version: '3.10.2.0'

      - name: Run Assembly Linter
        run: |
          chmod +x ./scripts/lint-asm.sh
          ./scripts/lint-asm.sh ${{ matrix.arch }} cbits

      - name: Cache Cabal Store
        uses: actions/cache@v4
        with:
          path: |
            ~/.cabal/store
            dist-newstyle
          key: cabal-${{ matrix.arch }}-${{ hashFiles('**/*.cabal', 'cabal.project') }}
          restore-keys: |
            cabal-${{ matrix.arch }}-

      - name: Build with GHC LLVM Backend
        run: |
          cabal update
          cabal build --ghc-options="-fllvm -optlo-O3 -optlc-O3"

      - name: Run Test Suite
        run: |
          cabal test --test-show-details=streaming

Optimasi Caching dan Pertimbangan Teknis

Saat menggunakan LLVM backend di GHC, pastikan versi LLVM kompatibel secara presisi dengan rilis GHC yang digunakan. GHC 9.6 secara resmi mendukung LLVM versi 13 hingga 15. Penggunaan versi LLVM di luar batas yang didukung akan memicu error kompilasi pada pass opt atau llc.

  • Cache Key Granularity: Pisahkan cache Cabal berdasarkan matrix.arch. Binary artifacts antara x86_64 dan aarch64 tidak kompatibel; menyatukan cache key akan menyebabkan cache thrashing.
  • Clobber List Fallback: Register memory clobber wajib ditambahkan pada setiap block inline assembly yang memodifikasi pointer buffer Haskell agar compiler tidak melakukan reordering akses memori.
  • QEMU vs Native Runners: Hindari emulasi QEMU untuk arsitektur AArch64 bila runner native tersedia. GHC kompilasi dengan backend LLVM membutuhkan konsumsi memori dan CPU signifikan yang sering kali berujung pada timeout di lingkungan emulasi.