Skip to content

Commit 4e73a21

Browse files
oiseeclaude
andcommitted
docs: Z80 unit test proposal + div8/mul8 bug report
- docs/z80_unit_test_proposal.md: mzx test CLI design, gen_mzx_tests.py, carry sensitivity audit, CI integration - Documents confirmed bugs: div8 k=5 (251/256 wrong), mul8 RLA carry issue - Thanks: Joaquín Ferrero for finding these Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a52aa25 commit 4e73a21

1 file changed

Lines changed: 178 additions & 0 deletions

File tree

docs/z80_unit_test_proposal.md

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# Z80 Library Unit Test Design Proposal
2+
3+
**Date:** 2026-04-01
4+
**Status:** draft / proposal for mze/mzx integration
5+
6+
---
7+
8+
## Problem Statement
9+
10+
The mul8/mul16/div8 library sequences have discovered bugs:
11+
12+
1. **`div8_optimal.json` k=5**: ops implement `(n×105)>>10 ≈ n/10`, not `n/5`. **251/256 inputs wrong** (e.g. n=137 → got 14, want 27).
13+
2. **k=10**: correctly implements `(n>>1)×103>>9 ≈ n/10`. ✓
14+
3. **mul8 carry dependency**: sequences using `RLA`/`RLCA` include the incoming carry flag in the result. The GPU searcher (`z80_mulopt_fast.cu`) tested only with `B=0, CY=0` — results are incorrect if called with `CY≠0`.
15+
16+
The root cause: GPU brute-force verified sequences in isolation with clean initial state. Real code can call these routines with arbitrary `B` and `CY`.
17+
18+
---
19+
20+
## Proposed Test CLI: `mzx --test`
21+
22+
Extend `mzx` with a deterministic test mode:
23+
24+
```bash
25+
# Single test: set registers, run entry point, check expected
26+
mzx --run mul8_library.bin --entry mul_15 \
27+
--set a=10 --set b=0 --set f=0 \
28+
--exp a=150
29+
# exit 0 = pass, exit 1 = fail
30+
31+
# Exhaustive sweep: all A=0..255
32+
mzx --run mul8_library.bin --entry mul_15 \
33+
--set b=0 --set f=0 \
34+
--sweep a=0..255 \
35+
--exp "a == (a_in * 15) & 0xFF"
36+
# exit 0 if all 256 pass
37+
38+
# Full carry test: A=0..255 × CY=0,1 (512 inputs)
39+
mzx --run mul8_library.bin --entry mul_15 \
40+
--set b=0 \
41+
--sweep a=0..255,f=0..1 \
42+
--exp "a == (a_in * 15) & 0xFF"
43+
```
44+
45+
### Register naming
46+
47+
| Flag | Meaning |
48+
|------|---------|
49+
| `--set r=V` | Set register `r` to value `V` before run |
50+
| `--sweep r=lo..hi` | Iterate register over range |
51+
| `--exp r=V` | Assert register `r` equals `V` after halt |
52+
| `--exp "expr"` | Evaluate expression (uses `a_in`, `b_in`, etc. for pre-call values) |
53+
| `--entry label` | Jump to label (resolved from `.sym` file or label in binary) |
54+
| `--json` | Output JSON report instead of exit code |
55+
56+
---
57+
58+
## Test Generation from JSON Tables
59+
60+
Script: `scripts/gen_mzx_tests.py`
61+
62+
```python
63+
# Generate test cases from mulopt8_clobber.json
64+
for k in range(2, 256):
65+
entry = mul8_table[k]
66+
# Test: exhaustive A=0..255, B=0, CY=0
67+
tests.append({
68+
"entry": f"mul_{k}",
69+
"sweep": {"a": range(256)},
70+
"set": {"b": 0, "f": 0}, # CY=0
71+
"exp": "a == (a_in * k) & 0xFF"
72+
})
73+
# Test: carry sensitivity (if ops contain RLA/RLCA → must pass with CY=1 too)
74+
if any("RLA" in op or "RLCA" in op for op in entry["ops"]):
75+
tests.append({
76+
"entry": f"mul_{k}",
77+
"note": "carry_sensitive",
78+
"sweep": {"a": range(256), "f": [0, 1]}, # CY=0 and CY=1
79+
"set": {"b": 0},
80+
"exp": "a == (a_in * k) & 0xFF"
81+
})
82+
```
83+
84+
---
85+
86+
## Carry Sensitivity Audit
87+
88+
Sequences using `RLA` or `RLCA` are carry-sensitive and must either:
89+
90+
**Option A** (safe): Replace `RLCA``ADD A,A`, `RLA``ADD A,A` in the generator
91+
- `ADD A,A` = `RLCA` but ignores incoming CY (4T, same cost)
92+
- `SUB B` = `SBC A,B` but ignores incoming CY (4T, same cost)
93+
94+
**Option B** (documented): Mark sequences as `"carry_in": "must_be_0"` in JSON, emit `AND A` or `OR A` (resets CY, 4T) as preamble in library generator
95+
96+
Recommendation: **Option A** — fix the ops in the JSON tables. The optimizer found these sequences assuming CY=0 anyway; replacing RLA→ADD A,A gives identical results under that assumption and is safe for all CY.
97+
98+
Scan from current tables:
99+
```bash
100+
python3 -c "
101+
import json
102+
data = json.load(open('data/mulopt8_clobber.json'))
103+
for k,entry in data.items():
104+
ops = entry.get('ops', [])
105+
if any('RLA' in op for op in ops):
106+
print(f'k={k}: carry-sensitive ops: {[o for o in ops if \"RL\" in o]}')
107+
"
108+
```
109+
110+
---
111+
112+
## div8 k=5 Fix
113+
114+
The k=5 entry computes `(n×105)>>10 ≈ n/10` (wrong). Correct formula:
115+
116+
```
117+
n/5 ≈ (n × 205) >> 10 [205/1024 = 0.2002...]
118+
```
119+
120+
Or equivalently (preshift approach, same T-states):
121+
```
122+
n/5 ≈ (n × 103) >> 9 [103/512 = 0.2012...] -- slightly less accurate
123+
```
124+
125+
Re-run GPU search for k=5:
126+
```bash
127+
cuda/z80_divmod_fast --div 5 --max-len 12 --json
128+
```
129+
130+
Verify fix in 1 line:
131+
```python
132+
assert all((205 * n) >> 10 == n // 5 for n in range(256)) # True
133+
```
134+
135+
---
136+
137+
## CI Integration
138+
139+
```makefile
140+
# Makefile target
141+
test-mul8:
142+
python3 scripts/gen_mzx_tests.py --table data/mulopt8_clobber.json --out /tmp/mul8_tests.json
143+
mzx --run data/mul8_library.bin --batch /tmp/mul8_tests.json --json > /tmp/mul8_results.json
144+
python3 scripts/check_results.py /tmp/mul8_results.json
145+
146+
test-div8:
147+
python3 scripts/gen_mzx_tests.py --table data/div8_optimal.json --out /tmp/div8_tests.json
148+
mzx --run data/div8_library.bin --batch /tmp/div8_tests.json --json > /tmp/div8_results.json
149+
python3 scripts/check_results.py /tmp/div8_results.json
150+
```
151+
152+
---
153+
154+
## Summary of Bugs Found (April 1, 2026)
155+
156+
| File | Bug | Severity |
157+
|------|-----|----------|
158+
| `div8_optimal.json` k=5 | ops implement n/10 not n/5, 251/256 wrong | **CRITICAL** |
159+
| `z80_mulopt_fast.cu` L158-159 | tests only B=0, CY=0; carry-sensitive ops pass incorrectly | HIGH |
160+
| `mul8_library.asm` (RLA-using sequences) | incorrect if called with CY≠0 | MEDIUM |
161+
162+
Thanks: Joaquín Ferrero for finding and reporting these. Filed as issues on z80-optimizer.
163+
164+
---
165+
166+
## Validation Approach (until mzx test mode exists)
167+
168+
Quick Python validator using our own Z80 simulator (`pkg/cpu`):
169+
```bash
170+
# Go test: exhaustive verify all 254 constants, all 256 inputs, B=0..255, CY=0..1
171+
CGO_ENABLED=0 ~/go/bin/go1.24.3 test ./pkg/mulopt/ -run TestMul8Exhaustive -v
172+
```
173+
174+
Or standalone:
175+
```bash
176+
# Use existing z80opt verify-jsonl infrastructure
177+
python3 scripts/verify_library.py --lib data/mulopt8_clobber.json --all-inputs --all-carry
178+
```

0 commit comments

Comments
 (0)