improve manual about subroutine call convention

This commit is contained in:
Irmen de Jong 2024-11-15 20:56:56 +01:00
parent 957c42bc1d
commit 07158a6f1a
6 changed files with 109 additions and 35 deletions

View File

@ -49,11 +49,14 @@ class FSignature(val pure: Boolean, // does it have side effects?
return when { return when {
actualParamTypes.isEmpty() -> CallConvention(emptyList(), returns) actualParamTypes.isEmpty() -> CallConvention(emptyList(), returns)
actualParamTypes.size==1 -> { actualParamTypes.size==1 -> {
// one parameter goes via register/registerpair // One parameter goes via register/registerpair.
// this avoids repeated code for every caller to store the value in the subroutine's argument variable.
// (that store is still done, but only coded once at the start at the subroutine itself rather than at every call site).
// TODO can we start using the X register as well for a third byte or word+byte combo
val paramConv = when(val paramType = actualParamTypes[0]) { val paramConv = when(val paramType = actualParamTypes[0]) {
DataType.UBYTE, DataType.BYTE -> ParamConvention(paramType, RegisterOrPair.A, false) DataType.UBYTE, DataType.BYTE -> ParamConvention(paramType, RegisterOrPair.A, false)
DataType.UWORD, DataType.WORD -> ParamConvention(paramType, RegisterOrPair.AY, false) DataType.UWORD, DataType.WORD -> ParamConvention(paramType, RegisterOrPair.AY, false)
DataType.FLOAT -> ParamConvention(paramType, RegisterOrPair.AY, false) DataType.FLOAT -> ParamConvention(paramType, RegisterOrPair.AY, false) // TODO is this correct? shouldn't it be FAC1?
in PassByReferenceDatatypes -> ParamConvention(paramType, RegisterOrPair.AY, false) in PassByReferenceDatatypes -> ParamConvention(paramType, RegisterOrPair.AY, false)
else -> ParamConvention(paramType, null, false) else -> ParamConvention(paramType, null, false)
} }

View File

@ -15,6 +15,7 @@ internal class FunctionCallAsmGen(private val program: PtProgram, private val as
// just ignore any result values from the function call. // just ignore any result values from the function call.
} }
// TODO tweak subroutine call convention to also make use of X register to pass a third byte?
internal fun optimizeIntArgsViaRegisters(sub: PtSub) = internal fun optimizeIntArgsViaRegisters(sub: PtSub) =
(sub.parameters.size==1 && sub.parameters[0].type in IntegerDatatypesWithBoolean) (sub.parameters.size==1 && sub.parameters[0].type in IntegerDatatypesWithBoolean)
|| (sub.parameters.size==2 && sub.parameters[0].type in ByteDatatypesWithBoolean && sub.parameters[1].type in ByteDatatypesWithBoolean) || (sub.parameters.size==2 && sub.parameters[0].type in ByteDatatypesWithBoolean && sub.parameters[1].type in ByteDatatypesWithBoolean)

View File

@ -58,6 +58,7 @@ Variables
Subroutines Subroutines
----------- -----------
- There is no call stack. Subroutine parameters are overwritten when called again (recursion is not easily possible, but you can do it with manual stack manipulations).
- There is no function overloading (except for a couple of builtin functions). - There is no function overloading (except for a couple of builtin functions).
- Some subroutine types can return multiple return values, and you can multi-assign those in a single statement. - Some subroutine types can return multiple return values, and you can multi-assign those in a single statement.
- Because every declared variable allocates some memory, it might be beneficial to share the same variables over different subroutines - Because every declared variable allocates some memory, it might be beneficial to share the same variables over different subroutines

View File

@ -137,42 +137,65 @@ Calling a subroutine requires three steps:
#. preparing the return value (if any) and returning that from the call. #. preparing the return value (if any) and returning that from the call.
``asmsub`` routines Regular subroutines
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
These are usually declarations of Kernal (ROM) routines or low-level assembly only routines, - Each subroutine parameter is represented as a variable scoped to the subroutine. Prog8 doesn't have a call stack.
that have their arguments solely passed into specific registers. - The arguments passed in a subroutine call are evaluated by the caller, and then put into those variables by the caller.
Sometimes even via a processor status flag such as the Carry flag. The order of evaluation of subroutine call arguments *is unspecified* and should not be relied upon.
Return values also via designated registers. - The subroutine is invoked.
The processor status flag is preserved on returning so you can immediately act on that for instance - The return value is not put into a variable, but the subroutine passes it back to the caller via cpu register(s):
via a special branch instruction such as ``if_z`` or ``if_cs`` etc.
regular subroutines
^^^^^^^^^^^^^^^^^^^
- subroutine parameters are just variables scoped to the subroutine.
- the arguments passed in a call are evaluated and then copied into those variables.
Using variables for this sometimes can seem inefficient but it's required to allow subroutines to work locally
with their parameters and allow them to modify them as required, without changing the
variables used in the call's arguments. If you want to get rid of this overhead you'll
have to make an ``asmsub`` routine in assembly instead.
- the order of evaluation of subroutine call arguments *is unspecified* and should not be relied upon.
- the return value is passed back to the caller via cpu register(s):
Byte values will be put in ``A`` . Byte values will be put in ``A`` .
Word values will be put in ``A`` + ``Y`` register pair. Boolean values will be put in ``A`` too, as 0 or 1.
Float values will be put in the ``FAC1`` float 'register' (BASIC allocated this somewhere in ram). Word values will be put in ``A`` + ``Y`` register pair (lsb in A, msb in Y).
Float values will be put in the ``FAC1`` float 'register'.
**Builtin functions can be different:**
some builtin functions are special and won't exactly follow these rules.
**Some arguments will be passed in registers:**
For single byte and word arguments, the values are simply loaded in cpu registers by the caller before calling the subroutine.
*The subroutine itself will take care of putting the values into the parameter variables.* This saves on code size because
otherwise all callers would have to store the values in those variables themselves. The rules for this are as follows:
Single byte parameter: ``sub foo(ubyte bar) { ... }``
gets bar in the accumulator A, *subroutine* stores it into parameter variable
Two byte parameters: ``sub foo(ubyte bar, ubyte baz) { ... }``
gets bar in the accumulator A, and baz in Y, *subroutine* stores it into parameter variable
Single word parameter: ``sub foo(uword bar) { ... }``
gets bar in the register pair A + Y (lsb in A, msb in Y), *subroutine* stores it into parameter variable
Other: ``sub foo(ubyte bar, ubyte baz, ubyte zoo) { ... }``
register values indeterminate, values all get stored in the parameter variables *by the caller*
Calls to builtin functions are treated in a special way:
Generally if they have a single argument it's passed in a register or register pair.
Multiple arguments are passed like a normal subroutine, into variables.
Some builtin functions have a fully custom implementation.
``asmsub`` and ``extsub`` routines
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The compiler will warn about routines that are called and that return a value, if you're not These are kernal (ROM) routines or low-level assembly routines, that get their arguments via specific registers.
doing something with that returnvalue. This can be on purpose if you're simply not interested in it. Sometimes even via a processor status flag such as the Carry flag.
Use the ``void`` keyword in front of the subroutine call to get rid of the warning in that case. Note that word values can be put in a "CPU register pair" such as AY (meaning A+Y registers) but also
in one of the 16 'virtual' 16 bit registers introduced by the Commander X16, R0-R15.
Float values can be put in the FAC1 or FAC2 floating point 'registers'.
The return values also get returned via designated registers, or via processor status flags again.
This means that after calling such a routine you can immediately act on the status
via a special branch instruction such as ``if_z`` or ``if_cs`` etc.
The register/status flag usage is fully specified in the asmsub or extsub signature defintion
for both the parameters and the return values::
extsub $2000 = extfunction(ubyte arg1 @A, uword arg2 @XY, uword arg3 @R0,
float frac @FAC1, bool flag @Pc) -> ubyte @Y, bool @Pz
asmsub function(ubyte arg1 @A, uword arg2 @XY, uword arg3 @R0,
float frac @FAC1, bool flag @Pc) -> ubyte @Y, bool @Pz {
%asm {{
...
...
}}
}
Compiler Internals Compiler Internals

View File

@ -7,8 +7,10 @@ TODO
Future Things and Ideas Future Things and Ideas
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
- adjust builtin function call convention to also use X register? see class FSignature.
- adjust funcion call convention for regular subroutines too? Can it reuse the CallConvention class here? can it also start using the X register? see optimizeIntArgsViaRegisters()
- implement const long to store a 32 bit signed integer value. (constants should be able to be long?) -> const_long branch - implement const long to store a 32 bit signed integer value. (constants should be able to be long?) -> const_long branch
- get rid of the BuiltinFunctionCall (and PtBuiltinFunctionCall) ast nodes distinction, just use 1 node type, they're mixed up now already anyways. - get rid of the BuiltinFunctionCall (and PtBuiltinFunctionCall) ast nodes distinction, just use 1 node type, they're mixed up now already anyways. -> remove-BFC-node branch
- something to reduce the need to use fully qualified names all the time. 'with' ? Or 'using <prefix>'? - something to reduce the need to use fully qualified names all the time. 'with' ? Or 'using <prefix>'?
- Why are blocks without an addr moved BEHIND a block with an address? That's done in the StatementReorderer. - Why are blocks without an addr moved BEHIND a block with an address? That's done in the StatementReorderer.
- on the C64: make the floating point routines @banked so that basic can be permanently banked out even if you use floats? But this will crash when the call is done from program code at $a000+ - on the C64: make the floating point routines @banked so that basic can be permanently banked out even if you use floats? But this will crash when the call is done from program code at $a000+

View File

@ -1,7 +1,51 @@
%import floats
main { main {
sub start() { sub start() {
ubyte @shared v1, v2 thing()
if v1==0 and v2 & 3 !=0 thang()
v1++ bool status
cx16.r0L, status = extfunction(42, 11223, 999, 1.22, true)
cx16.r0L, status = function(42, 11223, 999, 1.22, true)
func1(42)
func2(9999)
func3(42,9999)
func4(42,9999,12345)
}
sub thing() -> bool {
cx16.r0++
return true
}
sub thang() -> float {
}
sub func1(ubyte arg) {
cx16.r0L +=arg
}
sub func2(uword arg) {
cx16.r0 += arg
}
sub func3(ubyte arg1, uword arg2) {
cx16.r0 += arg2
cx16.r0L =+ arg1
}
sub func4(ubyte arg1, uword arg2, uword arg3) {
cx16.r0L =+ arg1
cx16.r0 += arg2
cx16.r1 += arg3
}
extsub $2000 = extfunction(ubyte arg1 @A, uword arg2 @XY, uword arg3 @R0, float frac @FAC1, bool flag @Pc) -> ubyte @Y, bool @Pz
asmsub function(ubyte arg1 @A, uword arg2 @XY, uword arg3 @R0, float frac @FAC1, bool flag @Pc) -> ubyte @Y, bool @Pz {
%asm {{
rts
}}
} }
} }