Skip to content

Wrong code at -O1/-O2/-O3 (-triple z80): IX frame pointer allocated as scratch, spill slots corrupted #50

Closed

Description

@ravn

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 long a, unsigned long b) { return a * b; }
int main(void) {
    unsigned long x = 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 */
    return 0;
}
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).

Activity

  1. ravn commented on Jul 10, 2026

    @ravn
    Author

    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.

    typedef unsigned short u16;
    unsigned char flags[600];
    unsigned int bench(void) {           /* Sieve of Eratosthenes on [2,599] */
        u16 i, 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;
        }
        return count;                    /* 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.

  2. AlvajoyAsante commented on Aug 21, 2026

    @AlvajoyAsante

    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?

  3. adriweb commented on Aug 21, 2026

    @adriweb
    Member

    Is this still happening with the latest nightly compiler? Lots of things have changed

  4. AlvajoyAsante commented on Aug 21, 2026

    @AlvajoyAsante

    No I haven't tried the nightly build, I will go ahead and try it out and get back to you.

  5. AlvajoyAsante commented on Aug 21, 2026

    @AlvajoyAsante

    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 the LD IX,0 / ADD IX,SP frame-setup idiom) followed by live (IX - N) accesses in _main.

    Opt level Installed 19.1.0 Nightly 22.1.8
    -O0 No No
    -O1 Yes No
    -O2 Yes No
    -O3 Yes No
    -Os No No
    -Oz No No

    The 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 -O1 and -O2 output for _main:

    • IX is initialized once from SP and is never modified afterward.
    • The o pointer is now placed in IY.
    • The (IX - 6) spill/reload of x remains intact.

    Issue #50 is still open, and the fix commits do not reference it. However, the fix landed somewhere between ef28e9c and 30cc65d.

  6. adriweb commented on Aug 21, 2026

    @adriweb
    Member

    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.

  7. ravn commented on Aug 23, 2026

    @ravn
    Author

    Great! Thanks 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions