Menjalankan mesin emulasi Zilog Z80 pada backend worker sering menghadapi masalah akurasi instruksi siklus mikro. Salah satu kendala paling umum saat memvalidasi Z80 core terhadap test suite standar (seperti ZEXALL atau fuse-emulator tests) adalah kegagalan penentuan nilai register F (Flag register), khususnya bit ke-2: flag Parity/Overflow (P/V).

Gejala Bug: Kegagalan Test Suite Aritmatika 8-Bit

Pada pipeline CI backend worker yang menjalankan virtual machine Z80 berbasis Go/C++, eksekusi instruksi aritmatika 8-bit seperti ADD A, n, ADC A, n, dan SUB n menghasilkan assertion failure pada test suite deterministik. Sementara instruksi logika seperti AND atau OR lolos pengujian, operasi aritmatika bertanda gagal menghasilkan status flag yang valid.

Contoh kegagalan trace register pada instruksi penambahan signed:

Input:  A = 0x7F (127), B = 0x01 (1)
Opcode: ADD A, B
Hasil: A = 0x80 (-128)
Expected F: S=1, Z=0, H=1, P/V=1, N=0, C=0 (0x94)
Actual F:   S=1, Z=0, H=1, P/V=0, N=0, C=0 (0x90) -> FAIL! Flag P/V mismatch.

Root Cause: Kerancuan Parity Checking vs Signed Overflow

Arsitektur Zilog Z80 membagi peran bit 2 register F untuk dua fungsi yang saling eksklusif tergantung pada kelompok instruksinya:

  • Parity (P): Digunakan pada operasi logika (AND, OR, XOR). Bernilai 1 jika jumlah total bit bernilai 1 pada hasil operasi adalah genap, dan 0 jika ganjil.
  • Overflow (V): Digunakan pada operasi aritmatika 2's complement (ADD, ADC, SUB, SBC). Bernilai 1 jika hasil operasi melampaui batas signed range integer 8-bit (-128 hingga +127), dan 0 jika tidak terjadi overflow.

Root cause muncul karena backend engine secara keliru mengaplikasikan fungsi penghitung parity bitwise ke seluruh operasi ALU, mengabaikan fakta bahwa operasi aritmatika memerlukan deteksi signed overflow.

Secara matematis, signed overflow pada penjumlahan A + B = R terjadi jika kedua operand memiliki sign bit yang sama (keduanya positif atau keduanya negatif), tetapi sign bit hasil R berbeda:

V = ((A ^ R) & (B ^ R) & 0x80) != 0

Sedangkan pada pengurangan A - B = R, overflow terjadi jika operand memiliki sign bit berlawanan dan hasil R memiliki sign bit yang berbeda dari A:

V = ((A ^ B) & (A ^ R) & 0x80) != 0

Implementasi Perbaikan Bit-Manipulation (Go)

Berikut adalah perbandingan implementasi ALU sebelum dan sesudah perbaikan pada backend core emulator Z80.

Implementasi Rusak (Sebelum)

// Salah: Menghitung Parity untuk instruksi aritmatika
func (cpu *CPU) Add8Bit(val byte) {
    a := cpu.A
    res := uint16(a) + uint16(val)
    cpu.A = byte(res)

    // Flag P/V dihitung menggunakan tabel parity logika (salah untuk ADD)
    cpu.F = 0
    if (cpu.A & 0x80) != 0 { cpu.F |= FlagS }
    if cpu.A == 0          { cpu.F |= FlagZ }
    if (a & 0x0F) + (val & 0x0F) > 0x0F { cpu.F |= FlagH }
    if ParityTable[cpu.A]  { cpu.F |= FlagPV } // Root Cause
    if res > 0xFF          { cpu.F |= FlagC }
}

Implementasi Benar (Sesudah)

const (
    FlagS  byte = 1 << 7
    FlagZ  byte = 1 << 6
    FlagH  byte = 1 << 4
    FlagPV byte = 1 << 2
    FlagN  byte = 1 << 1
    FlagC  byte = 1 << 0
)

// Benar: Memisahkan signed overflow untuk aritmatika
func (cpu *CPU) Add8Bit(val byte) {
    a := cpu.A
    res := uint16(a) + uint16(val)
    r := byte(res)
    
    cpu.F = 0
    if (r & 0x80) != 0 { cpu.F |= FlagS }
    if r == 0          { cpu.F |= FlagZ }
    if (a & 0x0F) + (val & 0x0F) > 0x0F { cpu.F |= FlagH }
    
    // Signed overflow: (+) + (+) = (-) atau (-) + (-) = (+)
    if ((a ^ r) & (val ^ r) & 0x80) != 0 {
        cpu.F |= FlagPV
    }
    
    if res > 0xFF { cpu.F |= FlagC }
    cpu.A = r
}

Unit Test Deterministik Validasi Flag Z80

Validasi dilakukan menggunakan unit test berbasis tabel tanpa framework tambahan untuk memastikan edge case boundary signed integer tercover.

package main

import "testing"

type CPU struct {
    A byte
    F byte
}

func TestZ80_AddSignedOverflow(t *testing.T) {
    cases := []struct {
        name     string
        a, b     byte
        wantA    byte
        wantPV   bool
    }{
        {"Positive Overflow (127 + 1)", 0x7F, 0x01, 0x80, true},
        {"No Overflow (64 + 63)", 0x40, 0x3F, 0x7F, false},
        {"Negative Overflow (-128 + -1)", 0x80, 0xFF, 0x7F, true},
        {"Mixed Sign (-64 + 64)", 0xC0, 0x40, 0x00, false},
    }

    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            cpu := &CPU{A: tc.a}
            cpu.Add8Bit(tc.b)

            if cpu.A != tc.wantA {
                t.Fatalf("A mismatch: got 0x%02X, want 0x%02X", cpu.A, tc.wantA)
            }
            
            gotPV := (cpu.F & FlagPV) != 0
            if gotPV != tc.wantPV {
                t.Fatalf("PV flag mismatch: got %v, want %v (F: 0x%02X)", gotPV, tc.wantPV, cpu.F)
            }
        })
    }
}

Kesimpulan

Kesalahan emulasi Z80 pada register status umumnya bersumber dari duplikasi semantik satu flag fisik untuk dua konteks berbeda: parity pada aljabar boolean dan overflow pada aritmatika bertanda. Isolasi penanganan flag berdasarkan kelompok instruksi menyelesaikan kegagalan eksekusi test suite pada backend worker secara tuntas.