Local variables in a compiler

143 views
Skip to first unread message

Robb Bates

unread,
Dec 3, 2025, 6:38:27 PM (13 days ago) Dec 3
to RC2014-Z80
I've been doing a bit of research and am having trouble figuring out how to have a compiler be able to use local variables popped off the stack and temp variables and then be able to return things via the stack but also be recursable (recursible?  Able to be recursed?)

I'm seeing all kinds of stuff about IX/IY and frame pointers, but can't quite figure out how they work.  Seems like each function needs a prologue and epilogue to shuffle the stack to discard consumed local and temp variables, which seems like a lot of redundant work and the return address keeps getting in the way.

I'm trying to use the old TOS-in-HL trick to avoid all the PUSHing and POPing for every inline primitive and function call.

Has anyone figured out how to deal with local variables in a sensible way?  I know this isn't something new.

Thanks,
Robb

Mark T

unread,
Dec 3, 2025, 8:23:35 PM (13 days ago) Dec 3
to RC2014-Z80
I think the calling routine pushes and pops arguments, then call puts the return address on the stack. 

Called routine reserves local space on the stack with ld hl, negative size, add hl,sp, ld sp, hl. Initialised variables could use ld and push.

End of called routine, ld hl,size, add hl,sp, ld sp,hl. Then return to the address on the stack.

Tying up hl for the duration of the function seems to me to be a waste of the most usefull register. IX or IY might be better and gives an offset to easily address local variables, within 256 bytes.

Ed Silky

unread,
Dec 3, 2025, 9:00:18 PM (13 days ago) Dec 3
to rc201...@googlegroups.com
I concur. The calling routine pushes parameters. The intro to a called routine pushes IX, points IX into the stack for the number of parameters, then parameters are accessed using IX+offset (the offsets being the appropriate amount to access the parameter). At the end, IX is popped and the routine returns. The post-routine-cleanup adjusts the stack to (effectively) remove the parameters that were pushed before the call.

The 'pre-call', 'call-intro', and 'post-call' code can be written as a macro, such that the source looks very clean, and the result is efficient. This structure supports recursion (as much as you have stack for).
-Ed

--
You received this message because you are subscribed to the Google Groups "RC2014-Z80" group.
To unsubscribe from this group and stop receiving emails from it, send an email to rc2014-z80+...@googlegroups.com.
To view this discussion, visit https://groups.google.com/d/msgid/rc2014-z80/9d10be8f-41a2-483c-87f5-975b01ef0354n%40googlegroups.com.

Ed Silky

unread,
Dec 3, 2025, 9:11:29 PM (13 days ago) Dec 3
to rc201...@googlegroups.com
If your question is more about local variables, the same mechanism is used. The local variables are negative offsets from IX, while the parameters are positive offsets. The return address lies between the two.

-Ed

Robb Bates

unread,
Dec 3, 2025, 10:40:49 PM (13 days ago) Dec 3
to RC2014-Z80
I think I have a grasp of how to use IX/IY to use local variables (I've heard them called temp) and arguments (which I've heard called local).
The question is how do you push something to the stack so that when you return from the function, it's there for the caller to use?

You have to somehow pop or save the return address, "consume" the arguments, push the return value and then push the return address back and RET. But saving the return address can't just be a single slot of you want to be able to reverse.

Robb

Phillip Stevens

unread,
Dec 4, 2025, 1:38:02 AM (13 days ago) Dec 4
to RC2014-Z80
On Thursday, 4 December 2025, Robb Bates wrote:
I think I have a grasp of how to use IX/IY to use local variables (I've heard them called temp) and arguments (which I've heard called local).
The question is how do you push something to the stack so that when you return from the function, it's there for the caller to use?

You have to somehow pop or save the return address, "consume" the arguments, push the return value and then push the return address back and RET. But saving the return address can't just be a single slot of you want to be able to reverse.

This might be something that you want to do in a small assembly program environment, but returning a value on the stack is not something that is commonly done. The best place to look is at C, where the only thing returned from a function is a single item, and this is either a value or an address of a value or structure, contained in CPU registers.

Which registers are used for passing parameters or returns depends on the compiler in use, and is called the ABI (Application Binary Interface). If you’re writing assembly, then you can be free to make your own choices.

Two very good 8085 / z80 compilers are found in z88dk. The sccz80 compiler uses DEHL to return up to a 32 bit value, or HL to return an address. The sdcc compiler (using the ABI 0) uses the same DEHL registers, but recently they’ve moved to use a new ABI 1 which achieves a very slight improvement in performance.

Both compilers can use optionally use callee conventions, where the callee is responsible for clearing the stack. Normally in C the caller is responsible for resetting the stack, and this might be a (dangerous) way for you to hack the C conventions and use a stack return. I say dangerous, because the required return values must be removed from the stack before changing the stack pointer otherwise there is a risk that the return values will be corrupted by an interrupt (like a from a serial interface or a timer).

To use local variables, the sccz80 compiler will use push (or adding a negative HL to SP) to create space and will internally track the stack and use HL or DE to access specific values stored on the stack. The sdcc compiler uses an index register to access the stack, and will move the stack pointer to protect local variables while they are in use.

For interest there are some precompiled C code (math functions written in C) in the am9511 floating point math library in z88dk, which demonstrates these two different ways of working. Here are two different compilations of atan2(x,y) for the 8085 and for the z80.
From this C.

Generally, if you want to get a number of values returned from a function you can:

1. define these as static or global variables (either in C or assembly) and therefore a static memory location is provided. But this doesn’t work for recursion. Global variables are good for keeping a setting or configuration across a lot of function calls.

2. define a local structure in a function in C (which will be put on the stack), and pass the address of the structure to your called function. The called function will then complete the values you require in your calling function, and then there doesn’t need to be an explicit return (except perhaps a success or failure value).

3. If you have less than 4 of 8 bit values, then you can make a union of a structure and long which will allow you to write individual 8 bit values within a single 32 bit return value. This is a bit unconventional, but it does work and is effective.

I would try to follow the C conventions on parameter passing, as greater thinkers than all of us thought this through fully to reach the conventions used today.

Cheers, P

Ed Silky

unread,
Dec 4, 2025, 2:12:53 AM (13 days ago) Dec 4
to rc201...@googlegroups.com
@Robb, Phillip provided an excellent answer, and I totally agree... Look at what C does first. 

The one thing that I'd like to point out (to help answer your question about pushing and popping) - You don't necessarily have to use the PUSH and POP instructions. You can get the value of the stack pointer, put it into IX or IY (or the other 16 bit registers) and access memory 'around' the stack using them. As Phillip mentioned, you have to be careful about your local (temp) variables, which you would put below the stack (as it was when your function was called). To do this, you want to save the current stack pointer and then move it down far enough to hold your variables (take care to always move the SP by multiples of 2). That way, interrupts, which will cause things to be pushed on the stack, won't corrupt your local variables.

I don't want to try to do a long explanation about ways to implement stack-based function calls, but hopefully that, and the C references that Phillip provided should give you some good ideas.

-Ed 

--
You received this message because you are subscribed to the Google Groups "RC2014-Z80" group.
To unsubscribe from this group and stop receiving emails from it, send an email to rc2014-z80+...@googlegroups.com.

Robb Bates

unread,
Dec 4, 2025, 11:09:20 AM (13 days ago) Dec 4
to RC2014-Z80
I think I got it.

When entering the method, IY holds the caller's frame pointer

[return] <-SP
[arguments]

So at the start of each method, we just
PUSH IY (to save the caller's frame pointer)
LD IY,0 (necessary since there isn't a LD IY,SP
ADD IY,SP (point to top of stack as the new method's frame pointer)

[caller's frame pointer] <-SP <-IY new frame pointer
[return address]
[arguments]
 
Access arguments by IY+n
PUSH dummy values for temps so SP stays valid
Access them by IY-n

 
Then at the end
 
LD SP,IY loads stack pointer with address of current frame pointer, which points at the caller's frame pointer
POP IY now IY holds caller's frame pointer
POP IX now IX holds the return address
; consume arguments and push return values here
JP (IX)

All temps are dumped. All arguments are consumed. All return values are pushed. HL, DE and BC aren't touched at all. SP points to the right place.

I think this works.  Punch holes in my theory.

Robb

Alan Cox

unread,
Dec 4, 2025, 12:18:39 PM (13 days ago) Dec 4
to rc201...@googlegroups.com
On Thu, 4 Dec 2025 at 16:09, Robb Bates <robb...@gmail.com> wrote:
I think I got it.

When entering the method, IY holds the caller's frame pointer

[return] <-SP
[arguments]

So at the start of each method, we just
PUSH IY (to save the caller's frame pointer)
LD IY,0 (necessary since there isn't a LD IY,SP
ADD IY,SP (point to top of stack as the new method's frame pointer)

[caller's frame pointer] <-SP <-IY new frame pointer
[return address]
[arguments]
 
Access arguments by IY+n
PUSH dummy values for temps so SP stays valid
Access them by IY-n

 
Then at the end
 
LD SP,IY loads stack pointer with address of current frame pointer, which points at the caller's frame pointer
POP IY now IY holds caller's frame pointer
POP IX now IX holds the return address
; consume arguments and push return values here
JP (IX)

Assuming you are doing C style calls and C style access then pretty much this. There are some optimizations though.

For the entry

PUSH IY
LD SP, -n     ; where n allows for the size of the local variable frame
ADD IY,SP   ; IY now points at the bottom of the new frame including locals
LD SP,IY      ; adjust the stack

On the return path you can do various things to adjust the stack - for small values repeatedly popping af is typically smallest and fastest if not you end up doing something like, although you want to use HL if you can (depending what values you need to pass back and how you can pass them back)

LD IY,-n
ADD IY,SP
LD SP,IY
POP IY
RET   (no point doing POP and JMP (IX) )

All the (IY+n) stuff though is 8bit and *really* slow so you want to keep everything in registers as much as possible. In many 16boit cases with more values than registers it's better to load and push the contents of memory locations on entry, use the fixed memory locations for calculation and then restore the old contents of the memory location on exit, than pay the hideous cost of Z80 indexed ops. On 8080 you really don't have much choice either.

For real CP/M programs (8080 code) you don't have any really easy way to do this. A lot of the compilers don't use frame pointers at all but instead use helpers that do

LD HL,offset
ADD HL,SP
LD E,(HL)
INC HL
LD D,(HL)
RET

or similar which is actually faster in many cases anyway (particularly when inlined)

FCC on Z80 uses all three methods depending upon code size and register usage. On 8080 it uses either inline or call helpers for the ADD HL,SP approach.

SDCC (and the version in Z88DK) does all sorts of magic and register usage analysis to try and keep stuff in registers instead, but as a result it won't run on a Z80 box only a modern PC.

If you are working in assembler then you can be a lot smarter than most compilers as you know the exact constraints. When you try and get fast recursive code on 8080 or Z80 you normally end up doing more like

func:   PUSH BC    ; save old working values if needed, also use as parameters
           PUSH HL
           LD HL,(tmp)   ; save working values we use recursively onto stack
           PUSH HL       ; etc for each
           ; Do stuff in BC/DE/HL result in DE and use tmp as if it was private
           CALL func  ; as needed
           ; whatever
           POP HL
           LD (tmp),HL   ; put old working values back
           POP HL
           POP BC
           RET

and it is commonly the case that it's much better to switch to a non-recursive version of the algorithm and use the stack purely as a data stack.

You'll also find that for 8080 compatible code it's often better to return a value in DE, whereas for Z80 HL is often more useful. That's partly because you've got no IX/IY to borrow for stack adjustment and partly because 8080 has no LD DE,(nnnn) or LD (nnnn),DE forms so you on 8080 you end up using XTHL SHLD nnnn XTHL stuff if you use HL for return values - but that varies sometimes.

Finally for Z80 there's a useful trick for single level recursion where you know it's only one level (eg SD card commands and extended commands) which is to use EXX and the alt registers for the recursive call as EXX is incredibly fast and nobody ever uses the alt registers for much 8)

> "Normally in C the caller is responsible for resetting the stack,"

Not really. The notion of a stack does not even exist in C as a language and in fact any vaguely modern processor mostly passes arguments in registers. ANSI C also has prototypes so the days of calling a function with the wrong number of arguments and getting freaky crashes are long over.

As an example this is how fcc compilers

int foo(int x)
{
    if (x)
        return x + foo(x - 1);
    return 0;
}

  .setcpu 8080
  .export _foo
.code
_foo:
  pop d
  pop h
  push h
  push d
  mov a,h
  ora l
;
  jz L1_e
  push h
  dcx h
  push h
  call _foo+0
  pop d
  pop d
  dad d
;
  ret
L1_e:
  lxi h,0
;
  ret


The pop/pop/push/push at the start is just a faster smaller way to get the first argument by popping and pushing the return address as well




Alan

Phillip Stevens

unread,
Dec 4, 2025, 5:34:02 PM (12 days ago) Dec 4
to RC2014-Z80
On Friday, 5 December 2025 at 01:18:39 UTC+8 Alan Cox wrote:

> "Normally in C the caller is responsible for resetting the stack,"

Not really. The notion of a stack does not even exist in C as a language and in fact any vaguely modern processor mostly passes arguments in registers. ANSI C also has prototypes so the days of calling a function with the wrong number of arguments and getting freaky crashes are long over.

Just to put into context that my point was contained within a paragraph specifically discussing the two compilers available within z88dk for 8080 and z80 based CPUs.

Both compilers can use optionally use callee conventions, where the callee is responsible for clearing the stack. Normally in C the caller is responsible for resetting the stack, and this might be a (dangerous) way for you to hack the C conventions and use a stack return. I say dangerous, because the required return values must be removed from the stack before changing the stack pointer otherwise there is a risk that the return values will be corrupted by an interrupt (like a from a serial interface or a timer).

To clarify, the normal method of passing parameters on these two (sccz80 and zsdcc) compilers is to use the stack because the 8080 and z80 do not have enough registers. Normally with these two compilers the caller is responsible for clearing down the stack, but using the __z88dk_callee decoration will allow a function so defined to reset its own stack, and this is usually the fastest way to balance the stack.

There is another option provided for the case where there is only one parameter using zsdcc, and where it will be passed in registers, and this is the decoration __z88dk_fastcall. In this case (for zsdcc ABI 0) the parameter will be in DEHL. The vanilla sdcc default has been revised (ABI 1), and now it will also use registers to pass parameters where they will fit. (Read the sdcc ABI 1 specification for more info, I’ve not dug too deep into that). sccz80 will always pass one parameter in DEHL registers.

Just to avoid doubt, as noted, both sccz80 and zsdcc (and sdcc) are cross compilers. They do not run on z80 hardware. They do run on most other hardware, including Intel and ARM. Each compiler takes a very different path to generate assembly, has its own strengths, and yet benchmarks show similar results. So the compiler can be chosen to suit the individual application, within the same development environment. https://github.com/z88dk/z88dk/wiki/Benchmarks

Cheers, P


 

7alken

unread,
Dec 10, 2025, 7:20:07 AM (7 days ago) Dec 10
to RC2014-Z80
hi, if this doesn't confuse you (mentioned caller/callee approach changes mostly on risc-v/m68k in relation to re-simplification for virtual weirdo thing) here is what machine discussed with me, crazy brainstorming few months ago... not Z80 related at all, just things nearby, to spark thinking, probably, or destroy it, ... it depends; ))
https://chatgpt.com/share/69395719-b424-8000-8000-b1de9d7787a8
https://chatgpt.com/share/68b70534-b0c0-8000-81f0-5d0eb6be18b3

(sadly, no time for sw nor hw now ... will see nearest future)
Petr
Reply all
Reply to author
Forward
0 new messages