You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Wrong code at -O1/-O2/-O3 (-triple z80): IX frame pointer allocated as scratch, spill slots corrupted #50
While looking into getting llvm-z80 in as a z88dk compiler in the same way ez80clang is, by getting proper runtime library support, copilot noticed a miscompile of the test programs I am using for this, and triaged this description and hacked together a fix for me.
I don't know how you normally prefer these things, but I could create a PR that adds the testcase below if that would be helpful? I can also supply the hacked fix if interested.
copilot word salad
clang version 19.1.0 (CE-Programming/llvm-project ef28e9c54cd1333a6091ab2ffbd315b465fc5090), -triple z80.
At -O1/-O2/-O3 the IX frame-pointer register is also allocated as a scratch GPR, so (ix - N) spill slots are stored and reloaded with different, wrong bases. Wrong code. Correct at -O0, -Os, -Oz.
Repro (m2.c)
unsigned char*o= (unsigned char*)0xC000;
unsigned long __attribute__((noinline)) g(unsigned longa, unsigned longb) { returna*b; }
intmain(void) {
unsigned longx=g(2000UL, 10000UL); /* x = 20000000, spilled to a frame slot */*(unsigned long*)(o+0) =g(x, 3UL); /* 2nd call + store through global pointer o */*(unsigned long*)(o+4) =x+1UL; /* reload x; expected 20000001 */return0;
}
ez80-clang -cc1 -triple z80 -S -O2 m2.c -o m2.s
Miscompiled _main (-O2, abridged)
ld ix, 0
add ix, sp ; IX = frame pointer
push hl
push hl
ld ix, 0 ; BUG: IX reused as constant 0 for 32-bit arg high words -> FP destroyed
ld hl, 2000
ld de, 10000
push ix / push de / push ix / push hl
call _g
ld (ix - 4), l ; spill x relative to IX=0 -> writes 0xFFFC
ld (ix - 3), h
ld (ix - 2), e
ld (ix - 1), d
...
call _g
...
ld ix, (_o) ; IX reused again, now = 0xC000
ld (ix), l ... ; store g(x,3) result through o
ld l, (ix - 4) ; reload x from IX=0xC000 -> reads 0xBFFC (garbage)
ld h, (ix - 3)
ld e, (ix - 2)
ld d, (ix - 1)
ld bc, 1
ld iy, 0
call __ladd ; x(=0) + 1
...
ld sp, ix ; epilogue also assumes IX = FP
IX is loaded with 0 (arg-scratch) and later (_o) while the frame slot for x is addressed (ix - N). The spill is written at 0xFFFC and read back at 0xBFFC, so x reads as garbage.
Result
Running with o -> 0xC000: x + 1 stored at o+4 is 1, expected 20000001.
-O1/-O2/-O3 all emit this clobber (identical); runtime-confirmed at -O3. -O0 and -Oz are correct (IX kept as FP; o uses IY / __frameset).
Second repro: same defect fires when IX is chosen for a loop induction variable
The same -triple z80-O1/-O2/-O3 defect (IX frame-pointer register used as an allocatable GPR) also occurs when the register allocator assigns IX to a loop-carried variable. Here it corrupts (ix - N) spill slots inside a nested loop, so the program wild-jumps / hangs.
typedefunsigned shortu16;
unsigned charflags[600];
unsigned intbench(void) { /* Sieve of Eratosthenes on [2,599] */u16i, isq, k, count=598;
isq=4;
for (i=2; isq<600; ++i) {
if (!flags[i])
for (k=isq; k<600; k+=i) { count-= !flags[k]; flags[k] =1; }
isq+=i+i+1;
}
returncount; /* expected 108; runtime hangs */
}
ez80-clang -cc1 -triple z80 -S -O2 sm.c
_bench (abridged):
ld ix, 0
add ix, sp ; IX = frame pointer
...
ld (ix - 2), l ... ; spill count/isq/i to frame slots
ld (ix - 6), l ...
ld ix, 598 ; BUG: IX reassigned to the `count` loop variable
...
ld l, (ix - 4) ; frame slot read with IX=598 -> garbage address
...
add ix, bc ; IX mutated as an induction variable
...
ld l, (ix - 2) ; more frame reads against the corrupted IX
Runtime: hangs (never terminates). Same opt-level range as the first repro: broken at -O1/-O2/-O3, correct at -O0/-Os/-Oz. This is the same underlying issue — IX (the frame-pointer register) is in the allocatable set — so a single fix (reserve IX when it is the frame pointer) should cover both cases.
Ok good, closing this then.
A new toolchain release will use this fixed compiler eventually but one can just download it from the nightly release and put it in their toolchain install.
Activity
ravn commentedon Jul 10, 2026
Second repro: same defect fires when IX is chosen for a loop induction variable
The same
-triple z80-O1/-O2/-O3defect (IX frame-pointer register used as an allocatable GPR) also occurs when the register allocator assigns IX to a loop-carried variable. Here it corrupts(ix - N)spill slots inside a nested loop, so the program wild-jumps / hangs._bench(abridged):Runtime: hangs (never terminates). Same opt-level range as the first repro: broken at
-O1/-O2/-O3, correct at-O0/-Os/-Oz. This is the same underlying issue — IX (the frame-pointer register) is in the allocatable set — so a single fix (reserve IX when it is the frame pointer) should cover both cases.corpus: correct pi:ez80clang skip reason to upstream ez80-clang IX fr…
corpus: root-cause sieve/fannkuch:ez80clang as same upstream IX bug (#50
corpus: refresh results.html after full sweep (all ez80clang cells PASS)
AlvajoyAsante commentedon Aug 21, 2026
I ran into the same issue as well. I just wanted to bring this back to their attention. Are you able to make a PR?
adriweb commentedon Aug 21, 2026
Is this still happening with the latest nightly compiler? Lots of things have changed
AlvajoyAsante commentedon Aug 21, 2026
No I haven't tried the nightly build, I will go ahead and try it out and get back to you.
AlvajoyAsante commentedon Aug 21, 2026
The issue has been resolved in the nightly build. Results from my automated tests below:
IX-bug Repro Results
Detection: A non-SP
LD IX,instruction (excluding theLD IX,0 / ADD IX,SPframe-setup idiom) followed by live(IX - N)accesses in_main.-O0-O1-O2-O3-Os-OzThe baseline matches the known signature exactly: the bug reproduces at
-O1,-O2, and-O3, but not at-O0,-Os, or-Oz. This validates the detector.The bug is fixed in the nightly build at all optimization levels. I also visually verified the nightly
-O1and-O2output for_main:opointer is now placed in IY.(IX - 6)spill/reload ofxremains intact.Issue #50 is still open, and the fix commits do not reference it. However, the fix landed somewhere between
ef28e9cand30cc65d.adriweb commentedon Aug 21, 2026
Ok good, closing this then.
A new toolchain release will use this fixed compiler eventually but one can just download it from the nightly release and put it in their toolchain install.
ravn commentedon Aug 23, 2026
Great! Thanks 🙂