nrdmn / llvm-project

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
http://llvm.org
Other
0 stars 1 forks source link

Implement necessary functions in STM8TargetLowering / Calling Convention #4

Open tyalie opened 6 months ago

tyalie commented 6 months ago

Okay. At least the following functions need to be implemented according to LLVMs TargetLowering:

Description from the source

/// This hook must be implemented to lower outgoing return values, described
/// by the Outs array, into the specified DAG. The implementation should
/// return the resulting token chain value.
virtual SDValue LowerReturn(SDValue /*Chain*/, CallingConv::ID /*CallConv*/,
                            bool /*isVarArg*/,
                            const SmallVectorImpl<ISD::OutputArg> & /*Outs*/,
                            const SmallVectorImpl<SDValue> & /*OutVals*/,
                            const SDLoc & /*dl*/,
                            SelectionDAG & /*DAG*/) const {
  llvm_unreachable("Not Implemented");
}

/// This hook must be implemented to lower the incoming (formal) arguments,
/// described by the Ins array, into the specified DAG. The implementation
/// should fill in the InVals array with legal-type argument values, and
/// return the resulting token chain value.
virtual SDValue LowerFormalArguments(
    SDValue /*Chain*/, CallingConv::ID /*CallConv*/, bool /*isVarArg*/,
    const SmallVectorImpl<ISD::InputArg> & /*Ins*/, const SDLoc & /*dl*/,
    SelectionDAG & /*DAG*/, SmallVectorImpl<SDValue> & /*InVals*/) const {
  llvm_unreachable("Not Implemented");
}

/// This hook must be implemented to lower calls into the specified
/// DAG. The outgoing arguments to the call are described by the Outs array,
/// and the values to be returned by the call are described by the Ins
/// array. The implementation should fill in the InVals array with legal-type
/// return values from the call, and return the resulting token chain value.
virtual SDValue
  LowerCall(CallLoweringInfo &/*CLI*/,
            SmallVectorImpl<SDValue> &/*InVals*/) const {
  llvm_unreachable("Not Implemented");
}
tyalie commented 6 months ago

[!WARNING] This might be incomplete, if there are inconsistencies, falsehoods or other, please comment below

In order to keep compatibility (and keep the already high mental load on us to a minimum), we will implement the SDCC calling convention for STM8 version 1. (see SDCC 4.4.1 manual - Section 4.5.1.1). It will be described here in more detail and with examples whenever possible.

SDCC calling convention

[!NOTE] First and foremost, SDCC strictly only considers the C calling convention, this is in opposition to LLVM which also supports calling conventions with multiple return values. As such we will not support the latter case for now.

The only registers (out of the three) that are often used are A and X. Y is used to communicate return parameters or temporary values, but that's about it. Your guess is as good as mine, but I assume it's due to the fact that most mnemonics operating on Y have the 0x90 opcode-prefix and are as such a byte longer.

Caller- and Callee-saved registers

As STM8 has less than a handful of registers, this is a very easy answer. As far as we know, the caller is always responsible for saving their registers, whilst there are no callee saved register.

Arguments

It is decided on whether arguments are passed on the stack, or using registers using the following flow chart

Image

SDCC is very strictly following that graph

Different input arguments and their assembly result > *Note*: As I'm using `void` as the return type the callee is responsible for cleaning up the stack. This is detailed in another section.
C Code SDCC Assembly
void fn1(uint8_t a) {
    volatile uint8_t z = a;
}
; only register a is used
_fn1:
        push    a
        ld      (0x01, sp), a
        pop     a
        ret
void fn2(uint8_t a, uint16_t b) {
    volatile uint8_t z = a + b;
}
; register a and x are used
_fn2:
        push    a
        pushw   x
        add     a, (2, sp)
        popw    x
        ld      (0x01, sp), a
        pop     a
        ret
void fn3(uint8_t a, uint8_t b) {
    volatile uint8_t z = a + b;
}
; register a is used for a, but b is on the stack
_fn3:
        push    a
        add     a, (0x04, sp)
        ld      (0x01, sp), a
        pop     a
        popw    x
        pop     a
        jp      (x)
void fn4(uint32_t a) {
    volatile uint8_t z = a;
}
; everything is on the stack
_fn4:
        push    a
        ld      a, (0x07, sp)
        ld      (0x01, sp), a
        ldw     x, (2, sp)
        addw    sp, #7
        jp      (x)

There is also the special case of function with variable arguments, these will have all the function arguments on the stack.

Return

The SDCC manual states:

Return values are in a (8 bit), x (16 bit), xyl (24 bit), xy (32 bit) or use a hidden extra pointer parameter pointing to the location (anything wider than 32 bit, and all struct /union)

The hidden pointer parameter will always be transferred on the stack.

In the case that the return parameters are on the stack, it will look like this

C codeSDCC ASM
uint64_t fn() { 
  return 0; 
}
void caller() {
    fn();
}
_fn:
        ldw     x, (0x03, sp)   ; get the pointer to the location
                ; where the return should be
        clrw    y
        ldw     (#6, x), y
        ldw     (#4, x), y
        ldw     (#2, x), y
        ldw     (x), y
        ret
_caller:
        sub     sp, #8
        ldw     x, sp
        addw    x, #1       ; SP points to the next free location,
                ; so SP + 1 is our data
        pushw   x       ; storing the pointer to the data on 
                ; the stack [SP -= 2]
        call    _fn
        addw    sp, #10
        ret

Stack cleanup

Who does the stack cleanup for the parameters depends on how the callee looks like. In general the caller is responsible, except in the following cases:

Examples for the stack cleanup
C code SDCC ASM code
void fn(uint32_t a) {}
void caller() {
    fn(0);
}
_fn:
        ldw     x, (1, sp)
        addw    sp, #6
        jp      (x)
_caller:
        clrw    x
        pushw   x
        clrw    x
        pushw   x
        call    _fn
        ret
uint8_t fn(uint32_t a) {}
void caller() {
    fn(0);
}
_fn:
        ldw     x, (1, sp)
        addw    sp, #6
        jp      (x)
_caller:
        clrw    x
        pushw   x
        clrw    x
        pushw   x
        call    _fn
        ret
uint32_t fn(uint32_t a) {}
void caller() {
    fn(0);
}
_fn:
        ret
_caller:
        clrw    x
        pushw   x
        clrw    x
        pushw   x
        call    _fn
        addw    sp, #4
        ret
float fn(float a) {}
void caller() {
    fn(0);
}
_fn:
        pushw   x
        ldw     x, (3, sp)
        ldw     (7, sp), x
        popw    x
        addw    sp, #4
        ret
_caller:
        clrw    x
        pushw   x
        clrw    x
        pushw   x
        call    _fn
        ret