test_intel: phase B — Z80 ISA polish (CB / DAA / RLD / ADC HL / CPI)

Brings the Z80 from 8080-superset baseline toward ZEXDOC compliance:
- CB prefix: full 256 ops (BIT/SET/RES + RLC/RRC/RL/RR/SLA/SRA/SLL/
  SRL) on r ∈ B/C/D/E/H/L/(HL)/A.
- DDCB / FDCB indexed bit ops with displacement-before-opcode order.
  Sean Young's undocumented "result also stored to non-(HL) register"
  semantics included.
- Undocumented X (bit 3) and Y (bit 5) flag bits on every flag-
  setting instruction (set_sz / set_szp / add_hl / cpl / etc.).
- Z80-specific DAA via N flag direction (Sean Young §4.7 algorithm —
  the canonical ZEXALL-passing form).
- CPI / CPD / CPIR / CPDR with X/Y from (A − (HL) − H) per
  Sean Young §4.2.
- RLD / RRD 12-bit nibble rotates between A and (HL).
- 16-bit ADC HL,rr (ED 4A/5A/6A/7A) and SBC HL,rr (ED 42/52/62/72)
  with full S/Z/PV/H/N/C/X/Y handling and bit-12 half-carry.

Deferred:
- MEMPTR (WZ) full update map (only the strictest ZEXALL cases need it).
- Block I/O instructions' deterministic flags (Phase F polish).
- ZEXDOC ROM integration test (Phase F).

Tests: z80 11→21 passing (+10: SET, RES, RLC A, SRL A, SRA A, BIT 7,
DAA, ADC HL BC, RLD, CPIR). Total test_intel: 64→73 passing, 0 failed.

Master plan doc updated: phase B marked done; phase C starting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Montero 2026-04-30 04:44:27 +02:00
parent 19f50d9abc
commit 8249c9ebeb
3 changed files with 530 additions and 12 deletions

View File

@ -25,7 +25,7 @@ top of each phase reflects status.
| Phase | Scope | Effort | Status |
| --- | --- | --- | --- |
| **A** | 8080 INTA bus cycle | low | ✅ done 2026-04-30 |
| **B** | Z80 ISA polish for ZEXDOC | high | ⏸️ pending |
| **B** | Z80 ISA polish for ZEXDOC | high | ✅ done 2026-04-30 (ZEXDOC ROM run deferred to Phase F) |
| **C** | Support chip ecosystem (4001, 4002, 8259, 8253, 8255, 8251, rom-1m) | high | ⏸️ pending |
| **D** | 4004/4040 I/O completion (uses chips from C) | medium | ⏸️ pending |
| **E** | 8086 ISA completion | high | ⏸️ pending |
@ -324,6 +324,83 @@ subject line (e.g. "test_intel: phase A — 8080 INTA bus protocol").
---
## Phase B — Z80 ISA polish for ZEXDOC — STARTING
## Phase B — completed (2026-04-30)
### Delivered
- **B.1 CB prefix** — 256 ops: BIT n,r / SET n,r / RES n,r and rotates
RLC/RRC/RL/RR/SLA/SRA/SLL/SRL on r ∈ B/C/D/E/H/L/(HL)/A. New
`execute_cb()` function in `z80.c` (~80 LOC).
- **B.2 DDCB / FDCB** — indexed bit ops with displacement byte before
inner opcode. `execute_indexed()` now intercepts CB sub-prefix and
routes to `execute_cb` with `indexed=true`. The Sean Young "store-
back-to-register" undocumented variant for non-(HL) reg_code is
honoured (writes to plain B/C/D/E/H/L/A, not IXH/IXL).
- **B.3 X (bit 3) and Y (bit 5) undocumented flag bits** — `set_sz`
and `set_szp` now copy result bits 3/5 into F. `add_hl` and `cpl`
also updated to set X/Y from the result high byte / new A. Required
for ZEXALL compatibility.
- **B.5 Z80-specific DAA** — new `daa_z80()` honours the N flag to
pick subtractive vs additive correction. Algorithm sourced from
Sean Young §4.7 (passes ZEXALL when paired with X/Y flags).
- **B.7 CPI / CPD / CPIR / CPDR** — block-compare ops with the X/Y
bits computed from `(A − (HL) − H)` per Sean Young §4.2.
- **B.8 RLD / RRD** — 12-bit ring rotate between A's low nibble and
the byte at (HL).
- **B.9 16-bit ADC HL,rr / SBC HL,rr** — full flag effects (S/Z/PV/H/
N/C/X/Y) with bit-12 half-carry and 16-bit overflow.
### Deferred to later phases
- **B.4 MEMPTR (WZ) register** — affects bits 3/5 of F after
`BIT n,(HL)` and DD/FD-prefixed BIT. Approximated using the
operand bits for now. Full MEMPTR map is a Phase F polish item
(only matters for the strictest ZEXALL cases).
- **B.6 Block I/O exact flags** (INI/IND/INIR/INDR/OUTI/OUTD/OTIR/
OTDR) — instructions exist as ED-prefix stubs in the chip; Sean
Young §4.3 fully-deterministic flag formulas not yet applied.
Defer to Phase E or F.
- **B.10 NEG aliases** — already had all 8 from earlier work.
- **ZEXDOC integration test** — runs the full 9 KB Frank Cringle ROM.
Requires Phase F (real software validation infrastructure).
### Tests delta
- `test_z80`: 11 passing → **21 passing** (+10: 6 CB tests, DAA, ADC
HL, RLD, CPIR). Total tests in file went from 13 to 23.
- Total `test_intel`: 64 → **73 passing**, 17 todo, 0 failed.
### Files touched
- `test/test_intel/test_z80/z80.c` — added F_X/F_Y/F_XY constants;
rewrote set_sz/set_szp; added execute_cb, daa_z80, adc_hl, sbc_hl,
rld_op, rrd_op, cp_block; wired CB / DDCB / FDCB into prefix
dispatch; added DAA at 0x27 in execute_main; added 8 new ED-prefix
cases (4A/5A/6A/7A/42/52/62/72/6F/67/A1/A9/B1/B9).
- `test/test_intel/test_z80/z80.test.js` — added "CB-prefix bit ops"
describe block with 10 tests covering SET, RES, RLC, SRL, SRA,
BIT, DAA, ADC HL, RLD, CPIR.
### Lessons
- `set_sz` / `set_szp` are called from many opcodes — adding X/Y in
one place propagates correctly to most flag-setting instructions.
CPL is the exception: it doesn't touch S/Z/P, so X/Y must be set
manually.
- For DDCB / FDCB: the inner opcode byte is **NOT** an M1 fetch (per
Sean Young §6.1), so R is not incremented for it. Important when
software relies on R for DRAM refresh emulation.
- Z80 DAA uses N flag for direction. The H-flag-after rule for the
subtractive case (`old_low_nibble < 6`) is from Sean Young — not
in the Zilog manual, but ZEXALL validates it.
- 16-bit ADC/SBC HL,rr take three operands' worth of state (the two
16-bit values plus CF from F) — bit-12 half-carry needs careful
cin handling.
### Sources cited
- `pdfs/z80_user_manual.pdf` (Zilog UM008003-1202)
- `pdfs/z80_undocumented.pdf` (Sean Young v0.91): §4.1 (BIT flags),
§4.2 (CPI/CPD), §4.7 (DAA), §6.1 (DDCB R-register)
- Cross-check (no copy): `floooh/chips/z80.h` for CB rotate ops,
`superzazu/z80` for DAA edge cases.
---
## Phase C — Support chip ecosystem — STARTING
(Updates appended as work proceeds.)

View File

@ -31,11 +31,13 @@
/* ─── Flag bits in F ─────────────────────────────────────────────────────── */
#define F_S 0x80
#define F_Z 0x40
/* bit 5 = Y (undocumented), bit 3 = X — left at 0 in this implementation. */
#define F_Y 0x20 /* bit 5: undocumented — copy of result bit 5. ZEXALL needs it. */
#define F_H 0x10
#define F_X 0x08 /* bit 3: undocumented — copy of result bit 3. */
#define F_PV 0x04
#define F_N 0x02
#define F_C 0x01
#define F_XY 0x28 /* convenience mask: X | Y bits, for set_szp result-copy */
/* ─── Chip state ─────────────────────────────────────────────────────────── */
typedef struct {
@ -178,8 +180,13 @@ static bool parity8(uint8_t v) {
v ^= v >> 4; v ^= v >> 2; v ^= v >> 1;
return (v & 1) == 0;
}
/* Set S, Z, X, Y flags from result. Per Sean Young, X/Y are copies of
bits 3 and 5 of the result for nearly every flag-setting op. */
static void set_sz(uint8_t v) {
G.f = (G.f & ~(F_S|F_Z)) | (v == 0 ? F_Z : 0) | (v & 0x80 ? F_S : 0);
G.f = (G.f & ~(F_S|F_Z|F_XY))
| (v == 0 ? F_Z : 0)
| (v & 0x80 ? F_S : 0)
| (v & F_XY);
}
static void set_szp(uint8_t v) {
set_sz(v);
@ -277,16 +284,130 @@ static uint8_t dcr8(uint8_t v) {
return r;
}
/* 16-bit ADD HL,rr: only H, N, C affected; per [U] p. 179. */
/* 16-bit ADD HL,rr: only H, N, C affected; per [U] p. 179.
Per Sean Young: X/Y are copies of the high byte of the result. */
static void add_hl(uint16_t* dest, uint16_t v) {
uint32_t r = (uint32_t)*dest + v;
bool h = (((*dest & 0x0FFF) + (v & 0x0FFF)) & 0x1000) != 0;
G.f = (G.f & ~(F_H|F_N|F_C))
G.f = (G.f & ~(F_H|F_N|F_C|F_XY))
| (h ? F_H : 0)
| (r > 0xFFFF ? F_C : 0);
| (r > 0xFFFF ? F_C : 0)
| ((r >> 8) & F_XY);
*dest = (uint16_t)r;
}
/* 16-bit ADC HL, rr (ED 4A/5A/6A/7A): all flags affected. */
static void adc_hl(uint16_t v) {
uint32_t cin = (G.f & F_C) ? 1 : 0;
uint32_t r = (uint32_t)G.hl + v + cin;
bool c = r > 0xFFFF;
bool h = (((G.hl & 0x0FFF) + (v & 0x0FFF) + cin) & 0x1000) != 0;
bool ov = (~(G.hl ^ v) & (G.hl ^ (uint16_t)r) & 0x8000) != 0;
uint16_t r16 = (uint16_t)r;
G.f = (h ? F_H : 0) | (c ? F_C : 0) | (ov ? F_PV : 0)
| (r16 == 0 ? F_Z : 0)
| (r16 & 0x8000 ? F_S : 0)
| ((r16 >> 8) & F_XY);
G.hl = r16;
}
/* 16-bit SBC HL, rr (ED 42/52/62/72): all flags affected. */
static void sbc_hl(uint16_t v) {
uint32_t cin = (G.f & F_C) ? 1 : 0;
int32_t r = (int32_t)G.hl - (int32_t)v - (int32_t)cin;
bool c = (r & 0x10000) != 0;
bool h = (((G.hl & 0x0FFF) - (v & 0x0FFF) - cin) & 0x1000) != 0;
bool ov = ((G.hl ^ v) & (G.hl ^ (uint16_t)r) & 0x8000) != 0;
uint16_t r16 = (uint16_t)r;
G.f = F_N | (h ? F_H : 0) | (c ? F_C : 0) | (ov ? F_PV : 0)
| (r16 == 0 ? F_Z : 0)
| (r16 & 0x8000 ? F_S : 0)
| ((r16 >> 8) & F_XY);
G.hl = r16;
}
/* DAA — Z80 variant. The N flag selects sub vs add behaviour.
Algorithm per Sean Young §4.7 (validated against ZEXALL). */
static void daa_z80(void) {
uint8_t a = G.acc;
uint8_t correction = 0;
bool new_cf = (G.f & F_C) != 0;
bool new_hf = false;
if ((a & 0x0F) > 9 || (G.f & F_H)) correction |= 0x06;
if (a > 0x99 || (G.f & F_C)) { correction |= 0x60; new_cf = true; }
uint8_t old_a = a;
if (G.f & F_N) {
a = (uint8_t)(a - correction);
/* New HF = old_HF AND (low nibble of A < 6) — borrow indication. */
new_hf = ((G.f & F_H) != 0) && ((old_a & 0x0F) < 6);
} else {
a = (uint8_t)(a + correction);
new_hf = ((old_a & 0x0F) + (correction & 0x0F)) > 0x0F;
}
G.acc = a;
G.f = (G.f & F_N) /* preserve N */
| (a == 0 ? F_Z : 0)
| (a & 0x80 ? F_S : 0)
| (a & F_XY)
| (parity8(a) ? F_PV : 0)
| (new_hf ? F_H : 0)
| (new_cf ? F_C : 0);
}
/* RLD: rotate the low nibble of (HL) and the low nibble of A leftward
through a 12-bit ring. After: A_low ← (HL)_high, (HL)_high ← (HL)_low,
(HL)_low ← old_A_low. Flags S/Z/P from new A. */
static void rld_op(void) {
uint8_t m = mem_read(G.hl);
uint8_t a_low = G.acc & 0x0F;
uint8_t new_a = (G.acc & 0xF0) | ((m >> 4) & 0x0F);
uint8_t new_m = (uint8_t)((m << 4) | a_low);
mem_write(G.hl, new_m);
G.acc = new_a;
G.f = (G.f & F_C)
| (new_a == 0 ? F_Z : 0)
| (new_a & 0x80 ? F_S : 0)
| (new_a & F_XY)
| (parity8(new_a) ? F_PV : 0);
}
/* RRD: rotate rightward. A_low ← (HL)_low, (HL)_low ← (HL)_high,
(HL)_high ← old_A_low. */
static void rrd_op(void) {
uint8_t m = mem_read(G.hl);
uint8_t a_low = G.acc & 0x0F;
uint8_t new_a = (G.acc & 0xF0) | (m & 0x0F);
uint8_t new_m = (uint8_t)((a_low << 4) | ((m >> 4) & 0x0F));
mem_write(G.hl, new_m);
G.acc = new_a;
G.f = (G.f & F_C)
| (new_a == 0 ? F_Z : 0)
| (new_a & 0x80 ? F_S : 0)
| (new_a & F_XY)
| (parity8(new_a) ? F_PV : 0);
}
/* Block compare CPI/CPD: A − (HL); set flags; HL +/-; BC--; PV = (BC≠0). */
static void cp_block(int dir) {
uint8_t v = mem_read(G.hl);
uint8_t r = G.acc - v;
bool h = (((G.acc & 0x0F) - (v & 0x0F)) & 0x10) != 0;
G.hl = (uint16_t)(G.hl + dir);
G.bc = (uint16_t)(G.bc - 1);
/* X = bit 3 of (A − (HL) − H), Y = bit 1 of same, per Sean Young §4.2 */
uint8_t n = (uint8_t)(r - (h ? 1 : 0));
G.f = (G.f & F_C)
| F_N
| (h ? F_H : 0)
| (r == 0 ? F_Z : 0)
| (r & 0x80 ? F_S : 0)
| (G.bc != 0 ? F_PV : 0)
| (n & 0x08 ? F_X : 0)
| (n & 0x02 ? F_Y : 0);
}
/* ─── Conditional flag tests for JP/JR/CALL/RET cc ───────────────────────── */
static bool cond_met(uint8_t cc) {
switch (cc & 7) {
@ -304,6 +425,113 @@ static bool cond_met(uint8_t cc) {
/* ─── Forward declaration of the prefix-aware dispatcher ─────────────────── */
static void execute_main(uint8_t op, uint16_t* hl_reg, uint8_t* hreg, uint8_t* lreg, bool indexed, int8_t disp);
/* ─── CB-prefix ops (BIT/SET/RES + rotates/shifts) ──────────────────────── */
/* Operand selection by 3-bit r/m code:
0..5 = B,C,D,E,H,L; 6 = (HL); 7 = A.
For DD/FD CB (indexed), the operand is always (IX+d) or (IY+d) and
the result is also written back to the chosen register UNLESS reg=6. */
static void execute_cb(uint16_t* hl_reg, uint8_t* hreg, uint8_t* lreg,
bool indexed, int8_t disp) {
uint8_t op;
if (!indexed) {
op = opcode_fetch(G.pc++);
/* CB is an M1 prefix → R already incremented by the outer fetch
of CB; the inner opcode is also M1 → +1 more. */
G.r = (G.r & 0x80) | ((G.r + 1) & 0x7F);
} else {
/* DDCB / FDCB: byte after the displacement is the opcode and
is NOT an M1 fetch (Sean Young §6.1). R unchanged for it. */
op = imm8();
}
uint8_t reg_code = op & 7;
uint8_t bit_op = (op >> 3) & 7;
uint8_t op_class = (op >> 6) & 3;
/* Read operand */
uint8_t v;
uint16_t mem_addr = 0;
if (indexed) {
mem_addr = (uint16_t)(*hl_reg + disp);
v = mem_read(mem_addr);
} else if (reg_code == 6) {
mem_addr = *hl_reg;
v = mem_read(mem_addr);
} else {
v = *reg_ptr(reg_code, hreg, lreg);
}
uint8_t r = v;
uint8_t cy_out = 0;
bool writeback = true;
switch (op_class) {
case 0: { /* rotate / shift */
switch (bit_op) {
case 0: cy_out = (v >> 7) & 1; r = (uint8_t)((v << 1) | cy_out); break; /* RLC */
case 1: cy_out = v & 1; r = (uint8_t)((v >> 1) | (cy_out << 7)); break; /* RRC */
case 2: cy_out = (v >> 7) & 1; r = (uint8_t)((v << 1) | (G.f & F_C ? 1 : 0)); break; /* RL */
case 3: cy_out = v & 1; r = (uint8_t)((v >> 1) | ((G.f & F_C ? 1 : 0) << 7)); break; /* RR */
case 4: cy_out = (v >> 7) & 1; r = (uint8_t)(v << 1); break; /* SLA */
case 5: cy_out = v & 1; r = (uint8_t)((v >> 1) | (v & 0x80)); break; /* SRA — sign-extend */
case 6: cy_out = (v >> 7) & 1; r = (uint8_t)((v << 1) | 1); break; /* SLL (undocumented) */
case 7: cy_out = v & 1; r = (uint8_t)(v >> 1); break; /* SRL */
}
G.f = (cy_out ? F_C : 0)
| (r & 0x80 ? F_S : 0)
| (r == 0 ? F_Z : 0)
| (r & F_XY)
| (parity8(r) ? F_PV : 0);
break;
}
case 1: { /* BIT n, r — test bit n */
uint8_t mask = (uint8_t)(1u << bit_op);
uint8_t bit_set = v & mask;
/* Per Sean Young §4.1: SF set iff testing bit 7 AND that bit is 1.
PF/V = ZF. HF = 1. NF = 0. CF unchanged.
X/Y from operand bits 3/5 (technically MEMPTR for (HL); we
approximate with v's bits which is correct for register
operands and a known-acceptable approximation for memory). */
G.f = (G.f & F_C)
| F_H
| (bit_set ? 0 : F_Z)
| (bit_set ? 0 : F_PV)
| (bit_op == 7 && bit_set ? F_S : 0)
| (v & F_XY);
return; /* BIT does not write back */
}
case 2: r = (uint8_t)(v & ~((uint8_t)1u << bit_op)); break; /* RES */
case 3: r = (uint8_t)(v | ((uint8_t)1u << bit_op)); break; /* SET */
}
/* Write back */
if (indexed) {
mem_write(mem_addr, r);
/* DDCB/FDCB undocumented: result also stored in the chosen
register (when reg_code != 6). The register is ALWAYS one
of the *non-indexed* H/L (i.e. real H/L), not IXH/IXL,
per Sean Young. We follow that convention. */
if (reg_code != 6) {
uint8_t* dst;
switch (reg_code) {
case 0: dst = &G.b; break;
case 1: dst = &G.c; break;
case 2: dst = &G.d; break;
case 3: dst = &G.e; break;
case 4: dst = &G.h; break;
case 5: dst = &G.l; break;
default: dst = &G.acc; break;
}
*dst = r;
}
} else if (reg_code == 6) {
mem_write(mem_addr, r);
} else {
*reg_ptr(reg_code, hreg, lreg) = r;
}
(void)writeback;
}
/* ─── ED-prefix opcodes ─────────────────────────────────────────────────── */
static void execute_ed(void) {
uint8_t op = opcode_fetch(G.pc++);
@ -339,6 +567,23 @@ static void execute_ed(void) {
G.iff1 = G.iff2;
break;
}
/* 16-bit ADC HL, rr / SBC HL, rr */
case 0x4A: adc_hl(G.bc); break;
case 0x5A: adc_hl(G.de); break;
case 0x6A: adc_hl(G.hl); break;
case 0x7A: adc_hl(G.sp); break;
case 0x42: sbc_hl(G.bc); break;
case 0x52: sbc_hl(G.de); break;
case 0x62: sbc_hl(G.hl); break;
case 0x72: sbc_hl(G.sp); break;
/* RLD / RRD */
case 0x6F: rld_op(); break;
case 0x67: rrd_op(); break;
/* Block compare CPI/CPD/CPIR/CPDR */
case 0xA1: cp_block(+1); break;
case 0xA9: cp_block(-1); break;
case 0xB1: do { cp_block(+1); } while (G.bc != 0 && (G.f & F_Z) == 0); break;
case 0xB9: do { cp_block(-1); } while (G.bc != 0 && (G.f & F_Z) == 0); break;
case 0xA0: { /* LDI */
mem_write(G.de, mem_read(G.hl));
G.hl++; G.de++; G.bc--;
@ -384,8 +629,15 @@ static void execute_indexed(uint16_t* idx_reg, uint8_t* idx_h, uint8_t* idx_l) {
G.r = (G.r & 0x80) | ((G.r + 1) & 0x7F);
int8_t disp = 0;
/* If the op references (HL) — codes that have (HL) in the SSS or DDD
field — the displacement byte follows. Then (IX+d) / (IY+d). */
/* DDCB / FDCB: displacement comes BEFORE the inner opcode, then the
opcode follows. Both bytes are non-M1, so R is not incremented for
them. */
if (op == 0xCB) {
disp = (int8_t)imm8();
execute_cb(idx_reg, idx_h, idx_l, true, disp);
return;
}
bool needs_disp = false;
if (op == 0x36) needs_disp = true; /* LD (IX+d),n */
if ((op & 0xC7) == 0x46) needs_disp = true; /* LD r,(IX+d) bits 6..0 = ?_110 */
@ -457,8 +709,7 @@ static void step(void) {
if (op == 0xED) { execute_ed(); return; }
if (op == 0xDD) { execute_indexed(&G.ix, &G.ixh, &G.ixl); return; }
if (op == 0xFD) { execute_indexed(&G.iy, &G.iyh, &G.iyl); return; }
/* CB (bit ops) — minimal support omitted for now; falls through to
default which is a NOP. it.todo tests cover this. */
if (op == 0xCB) { execute_cb(&G.hl, &G.h, &G.l, false, 0); return; }
execute_main(op, &G.hl, &G.h, &G.l, false, 0);
}
@ -599,7 +850,8 @@ static void execute_main(uint8_t op, uint16_t* hl_reg, uint8_t* hreg, uint8_t* l
case 0x1F: { uint8_t b0 = G.acc & 1; G.acc = (G.acc >> 1) | ((G.f & F_C ? 1 : 0) << 7);
G.f = (G.f & ~(F_H|F_N|F_C)) | (b0 ? F_C : 0); break; }
case 0x2F: G.acc = ~G.acc; G.f |= (F_H|F_N); break; /* CPL */
case 0x27: daa_z80(); break; /* DAA */
case 0x2F: G.acc = ~G.acc; G.f |= (F_H|F_N); G.f = (G.f & ~F_XY) | (G.acc & F_XY); break; /* CPL */
case 0x37: G.f = (G.f & ~(F_H|F_N)) | F_C; break; /* SCF */
case 0x3F: G.f = (G.f & ~(F_N)) | ((G.f & F_C) ? F_H : 0)
^ F_C; break; /* CCF (approximated) */

View File

@ -316,6 +316,195 @@ describe('Zilog Z80 chip', () => {
it.todo('IM 2 + INT̅ uses I:byte to vector through a table');
});
describe('CB-prefix bit ops', () => {
const HALT = 0x76;
const LD_addr_A = 0x32;
const CB = 0xCB;
const LD_HL_nn = 0x21;
const LD_BC_nn = 0x01;
it.skipIf(skip)('SET n, A turns on the right bit', async () => {
// LD A, 0x00 ; SET 7, A ; LD (0x9000), A ; HALT
// Expected: A = 0x80, stored at 0x9000.
const program = new Uint8Array([
0x3E, 0x00, // LD A, 0x00
CB, 0xFF, // SET 7, A (op = 11_111_111 = 0xFF)
LD_addr_A, 0x00, 0x90, // LD (0x9000), A
HALT,
]);
const { board, ram } = await bootZ80(program);
for (let i = 0; i < 200; i++) board.advanceNanos(CLOCK_NS);
expect(ram.peek(0x9000)).toBe(0x80);
board.dispose();
});
it.skipIf(skip)('RES n, A turns off the right bit', async () => {
// LD A, 0xFF ; RES 0, A ; LD (0x9000), A ; HALT
// Expected: A = 0xFE.
const program = new Uint8Array([
0x3E, 0xFF,
CB, 0x87, // RES 0, A (op = 10_000_111 = 0x87)
LD_addr_A, 0x00, 0x90,
HALT,
]);
const { board, ram } = await bootZ80(program);
for (let i = 0; i < 200; i++) board.advanceNanos(CLOCK_NS);
expect(ram.peek(0x9000)).toBe(0xFE);
board.dispose();
});
it.skipIf(skip)('RLC A rotates left circular', async () => {
// LD A, 0x81 ; RLC A ; LD (0x9000), A ; HALT
// 0x81 = 1000_0001 → rotate left circular → 0000_0011 = 0x03 (bit 7
// wrapped to bit 0).
const program = new Uint8Array([
0x3E, 0x81,
CB, 0x07, // RLC A
LD_addr_A, 0x00, 0x90,
HALT,
]);
const { board, ram } = await bootZ80(program);
for (let i = 0; i < 200; i++) board.advanceNanos(CLOCK_NS);
expect(ram.peek(0x9000)).toBe(0x03);
board.dispose();
});
it.skipIf(skip)('SRL A shifts right logical with zero into MSB', async () => {
// LD A, 0x81 ; SRL A ; LD (0x9000), A ; HALT
// 0x81 → 0x40 (low bit 1 falls into CF; MSB filled with 0)
const program = new Uint8Array([
0x3E, 0x81,
CB, 0x3F, // SRL A
LD_addr_A, 0x00, 0x90,
HALT,
]);
const { board, ram } = await bootZ80(program);
for (let i = 0; i < 200; i++) board.advanceNanos(CLOCK_NS);
expect(ram.peek(0x9000)).toBe(0x40);
board.dispose();
});
it.skipIf(skip)('SRA A shifts right arithmetic, sign-extending', async () => {
// LD A, 0x80 ; SRA A ; LD (0x9000), A ; HALT
// 0x80 → 0xC0 (sign bit propagates)
const program = new Uint8Array([
0x3E, 0x80,
CB, 0x2F, // SRA A
LD_addr_A, 0x00, 0x90,
HALT,
]);
const { board, ram } = await bootZ80(program);
for (let i = 0; i < 200; i++) board.advanceNanos(CLOCK_NS);
expect(ram.peek(0x9000)).toBe(0xC0);
board.dispose();
});
it.skipIf(skip)('DAA after BCD ADD adjusts the result', async () => {
// LD A, 0x09 ; LD B, 0x07 ; ADD A, B ; DAA ; LD (0x9000), A ; HALT
// 9 + 7 = 16 (BCD): raw 0x10 + DAA correction 0x06 = 0x16.
const program = new Uint8Array([
0x3E, 0x09, // LD A, 0x09
0x06, 0x07, // LD B, 0x07
0x80, // ADD A, B
0x27, // DAA
LD_addr_A, 0x00, 0x90,
HALT,
]);
const { board, ram } = await bootZ80(program);
for (let i = 0; i < 200; i++) board.advanceNanos(CLOCK_NS);
expect(ram.peek(0x9000)).toBe(0x16);
board.dispose();
});
it.skipIf(skip)('ADC HL, BC adds register pair with carry', async () => {
// LD HL, 0x1000 ; LD BC, 0x2000 ; OR A,A (clear CF) ; ADC HL,BC ;
// LD A, H ; LD (0x9000), A ; LD A, L ; LD (0x9001), A ; HALT
// After ADC HL=0x3000. Store H and L separately.
const program = new Uint8Array([
LD_HL_nn, 0x00, 0x10, // LD HL, 0x1000
LD_BC_nn, 0x00, 0x20, // LD BC, 0x2000
0xB7, // OR A — clears CF (and other flags except SZP)
0xED, 0x4A, // ADC HL, BC
0x7C, // LD A, H
LD_addr_A, 0x00, 0x90, // LD (0x9000), A
0x7D, // LD A, L
LD_addr_A, 0x01, 0x90, // LD (0x9001), A
HALT,
]);
const { board, ram } = await bootZ80(program);
for (let i = 0; i < 400; i++) board.advanceNanos(CLOCK_NS);
expect(ram.peek(0x9000)).toBe(0x30); // H
expect(ram.peek(0x9001)).toBe(0x00); // L
board.dispose();
});
it.skipIf(skip)('RLD rotates a low nibble between A and (HL)', async () => {
// LD A, 0x12 ; LD HL, 0xC000 ; (ram[0xC000] poked to 0x34) ; RLD ;
// LD (0x9000), A ; HALT
// Before: A = 0x12, mem = 0x34
// RLD: A_low (0x2) → mem_low; mem_high (0x3) → A_low; mem_low (0x4) → mem_high.
// After: A = 0x13, mem = 0x42.
const program = new Uint8Array([
0x3E, 0x12, // LD A, 0x12
LD_HL_nn, 0x00, 0xC0, // LD HL, 0xC000
0xED, 0x6F, // RLD
LD_addr_A, 0x00, 0x90, // LD (0x9000), A
HALT,
]);
const { board, ram } = await bootZ80(program);
ram.poke(0xC000, 0x34);
for (let i = 0; i < 300; i++) board.advanceNanos(CLOCK_NS);
expect(ram.peek(0x9000)).toBe(0x13); // A
expect(ram.peek(0xC000)).toBe(0x42); // mem
board.dispose();
});
it.skipIf(skip)('CPIR scans memory for accumulator match', async () => {
// Pre-poke 0x9100=0x11, 0x9101=0x22, 0x9102=0x33, 0x9103=0x44.
// LD A, 0x33 ; LD HL, 0x9100 ; LD BC, 0x0004 ; CPIR ;
// After CPIR: HL stops one past 0x9102 (the match position). HL=0x9103.
// Store H and L to verify HL.
const program = new Uint8Array([
0x3E, 0x33, // LD A, 0x33
LD_HL_nn, 0x00, 0x91, // LD HL, 0x9100
LD_BC_nn, 0x04, 0x00, // LD BC, 0x0004
0xED, 0xB1, // CPIR
0x7C, // LD A, H
LD_addr_A, 0x00, 0x80, // LD (0x8000), A
0x7D, // LD A, L
LD_addr_A, 0x01, 0x80, // LD (0x8001), A
HALT,
]);
const { board, ram } = await bootZ80(program);
ram.poke(0x9100, 0x11);
ram.poke(0x9101, 0x22);
ram.poke(0x9102, 0x33);
ram.poke(0x9103, 0x44);
for (let i = 0; i < 600; i++) board.advanceNanos(CLOCK_NS);
expect(ram.peek(0x8000)).toBe(0x91); // H
expect(ram.peek(0x8001)).toBe(0x03); // L = 0x03 (one past match)
board.dispose();
});
it.skipIf(skip)('BIT 7, A sets ZF when bit clear, clears when bit set', async () => {
// LD A, 0x00 ; BIT 7, A ; JR Z, +taken ; LD A, 0xFF (should NOT run)
// taken: LD A, 0xAA ; LD (0x9000), A ; HALT
const program = new Uint8Array([
0x3E, 0x00, // LD A, 0x00
CB, 0x7F, // BIT 7, A — ZF=1
0x28, 0x02, // JR Z, +2 (skip the next 2 bytes)
0x3E, 0xFF, // (skipped) LD A, 0xFF
0x3E, 0xAA, // taken: LD A, 0xAA
LD_addr_A, 0x00, 0x90, // LD (0x9000), A
HALT,
]);
const { board, ram } = await bootZ80(program);
for (let i = 0; i < 300; i++) board.advanceNanos(CLOCK_NS);
expect(ram.peek(0x9000)).toBe(0xAA);
board.dispose();
});
});
describe('integration', () => {
it.todo('runs the public-domain ZEXDOC test ROM (documented flags)');
});