Bahasa pemrograman concatenative (seperti Forth, Factor, atau DSL berbasis stack untuk game jam) memproses data melalui evaluasi implisit pada parameter stack. Ketiadaan variabel lokal eksplisit mempercepat eksekusi, tetapi menciptakan risiko struktural: stack underflow dan stack imbalance pada percabangan kondisional. Kegagalan ini sering kali lolos dari unit test konvensional jika jalur cabang tertentu tidak tereksekusi pada runtime.

Solusi deterministik untuk masalah ini adalah memvalidasi stack effect signature secara statis pada fase linting, mengeksekusi headless smoke test untuk memvalidasi game loop, dan memastikan binary tidak melampaui batasan ukuran (budget) yang ditentukan.

1. Setup Toolchain dan Caching di GitHub Actions

Compiler atau interpreter bahasa concatenative sering kali berukuran kecil dan didistribusikan sebagai binary mandiri atau modul C/Rust sederhana. Hindari kompilasi ulang toolchain pada setiap commit dengan menerapkan strategi hash-based caching menggunakan actions/cache.

- name: Cache Concatenative Toolchain
  id: cache-toolchain
  uses: actions/cache@v4
  with:
    path: ~/.local/bin/stacklang
    key: ${{ runner.os }}-toolchain-${{ hashFiles('tools/toolchain.lock') }}

- name: Install Toolchain
  if: steps.cache-toolchain.outputs.cache-hit != 'true'
  run: |
    mkdir -p ~/.local/bin
    curl -sSL https://releases.example.org/stacklang-linux-x86_64 -o ~/.local/bin/stacklang
    chmod +x ~/.local/bin/stacklang
    echo "$HOME/.local/bin" >> $GITHUB_PATH

2. Otomasi Linting Stack-Effect Signature

Dalam paradigma concatenative, setiap kata (word) memiliki kontrak formal yang disebut stack effect comment dengan format ( input -- output ). Contoh: : drop-dup ( a b -- a a ) drop dup ;.

Jika cabang kondisional if ... else ... then menghasilkan jumlah elemen stack yang berbeda antara jalur benar dan salah, status stack menjadi non-deterministik. Linting statis menelusuri definisi kata, menghitung delta konsumsi/produksi stack setiap instruksi primitif, dan menggagalkan build jika terjadi ketidaksesuaian delta.

Script validasi statis minimal berikut (tools/check_stack.py) memverifikasi keseimbangan delta stack pada file sumber:

#!/usr/bin/env python3
import re
import sys

# Mapping primitif dasar ke delta stack (pop, push)
PRIMITIVES = {
    "dup": (-1, 2),    # ( a -- a a )
    "drop": (-1, 0),   # ( a -- )
    "swap": (-2, 2),   # ( a b -- b a )
    "rot": (-3, 3),    # ( a b c -- b c a )
    "+": (-2, 1),      # ( a b -- c )
    "-": (-2, 1),
    "*": (-2, 1),
    "@": (-1, 1),      # Fetch ( addr -- val )
    "!": (-2, 0),      # Store ( val addr -- )
}

def calculate_effect(signature):
    match = re.search(r"\(\s*(.*?)\s*--\s*(.*?)\s*\)", signature)
    if not match:
        return None
    inputs = len(match.group(1).split()) if match.group(1).strip() else 0
    outputs = len(match.group(2).split()) if match.group(2).strip() else 0
    return outputs - inputs

def verify_word(name, signature, body):
    expected_delta = calculate_effect(signature)
    if expected_delta is None:
        print(f"ERROR: {name} tidak memiliki stack comment valid.")
        return False

    current_depth = 0
    tokens = body.split()
    for token in tokens:
        if token.isdigit():
            current_depth += 1  # Literal push
        elif token in PRIMITIVES:
            consumed, produced = PRIMITIVES[token]
            if current_depth + consumed < 0:
                print(f"ERROR di '{name}': Stack underflow saat memanggil '{token}'")
                return False
            current_depth += consumed + produced

    if current_depth != expected_delta:
        print(f"ERROR di '{name}': Ketidaksesuaian stack delta. Diharapkan {expected_delta}, hasil: {current_depth}")
        return False
    return True

def main():
    has_error = False
    with open(sys.argv[1], "r") as f:
        content = f.read()

    # Parsing kata: : nama ( in -- out ) body ;
    words = re.findall(r":\s*(\S+)\s*(\([^\)]+\))(.*?);", content, re.DOTALL)
    for name, sig, body in words:
        if not verify_word(name, sig, body):
            has_error = True

    if has_error:
        sys.exit(1)
    print("Semua stack effect signature valid.")

if __name__ == "__main__":
    main()

3. Eksekusi Headless Smoke Test untuk Game Loop

Aplikasi grafis atau game concatenative biasanya menggunakan library C seperti Raylib atau SDL2 melalui FFI. Menjalankan binary ini langsung di GitHub Actions runner akan memicu kegagalan inisialisasi context visual (misal: GLFW: Failed to initialize display).

Gunakan virtual display server xvfb atau dummy driver bawaan subsistem grafik. Untuk mencegah runner berhenti tanpa batas waktu akibat game loop, berikan parameter tick batas atau gunakan utilitas timeout Linux.

- name: Install Display Dependencies
  run: |
    sudo apt-get update && sudo apt-get install -y xvfb libgl1-mesa-dri

- name: Run Headless Smoke Test
  run: |
    # Jalankan game selama 180 frame atau batasi waktu ke 5 detik
    xvfb-run -a --server-args="-screen 0 640x480x24" \
      timeout --preserve-status 5s ./bin/game --smoke-test || STATUS=$?
    
    # Status 124 adalah exit code normal dari perintah timeout
    if [ -n "$STATUS" ] && [ "$STATUS" -ne 124 ]; then
      echo "Smoke test crash dengan exit code $STATUS"
      exit $STATUS
    fi

4. Integrasi Pipeline CI dan Validasi Binary Budget

Ekosistem game jam sering membatasi ukuran file terkompilasi (misal: 64KB atau 1MB). Pipeline CI harus memverifikasi batas ukuran ini secara otomatis setelah proses stripping binary selesai.

Berikut konfigurasi lengkap workflow .github/workflows/ci.yml:

name: Concatenative Engine CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build-and-verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set Up Python (for static analysis)
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Static Stack Effect Lint
        run: python3 tools/check_stack.py src/main.stack

      - name: Build Engine Binary
        run: |
          make build
          strip --strip-all bin/game

      - name: Headless Smoke Test
        run: |
          sudo apt-get update && sudo apt-get install -y xvfb libgl1-mesa-dri
          xvfb-run -a timeout --preserve-status 5s ./bin/game --headless-ticks 180

      - name: Verify Binary Size Budget
        run: |
          MAX_BYTES=65536 # Batas 64 KB
          ACTUAL_BYTES=$(stat -c%s bin/game)
          echo "Ukuran Binary: $ACTUAL_BYTES bytes (Batas: $MAX_BYTES bytes)"
          if [ "$ACTUAL_BYTES" -gt "$MAX_BYTES" ]; then
            echo "ERROR: Ukuran binary melebihi budget!"
            exit 1
          fi

      - name: Archive Artifact
        uses: actions/upload-artifact@v4
        with:
          name: game-linux-x86_64
          path: bin/game
          if-no-files-found: error

Debugging dan Jebakan Umum

  • Branch Discrepancy: Kata kondisional seperti if drop else dup then memiliki efek stack dinamis bergantung pada nilai runtime. Linter statis harus memaksakan agar kedua cabang memiliki efek stack yang identik.
  • Xvfb Resource Leaks: Penggunaan xvfb-run -a secara otomatis memilih nomor display bebas (display number auto-allocation), menghindari bentrokan proses jika runner mengeksekusi job paralel.
  • Floating Point Stack: Forth standar memisahkan float stack dari data stack utama. Jika runtime memisahkan stack ini, pastikan linter memvalidasi kedua stack secara independen.