From 07158a6f1acf13395ef4e56644f70c0719324876 Mon Sep 17 00:00:00 2001 From: Irmen de Jong Date: Fri, 15 Nov 2024 20:56:56 +0100 Subject: [PATCH] improve manual about subroutine call convention --- .../src/prog8/code/core/BuiltinFunctions.kt | 7 +- .../codegen/cpu6502/FunctionCallAsmGen.kt | 1 + docs/source/comparing.rst | 1 + docs/source/technical.rst | 81 ++++++++++++------- docs/source/todo.rst | 4 +- examples/test.p8 | 50 +++++++++++- 6 files changed, 109 insertions(+), 35 deletions(-) diff --git a/codeCore/src/prog8/code/core/BuiltinFunctions.kt b/codeCore/src/prog8/code/core/BuiltinFunctions.kt index 7e2553c1c..6304fdfce 100644 --- a/codeCore/src/prog8/code/core/BuiltinFunctions.kt +++ b/codeCore/src/prog8/code/core/BuiltinFunctions.kt @@ -49,11 +49,14 @@ class FSignature(val pure: Boolean, // does it have side effects? return when { actualParamTypes.isEmpty() -> CallConvention(emptyList(), returns) 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]) { DataType.UBYTE, DataType.BYTE -> ParamConvention(paramType, RegisterOrPair.A, 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) else -> ParamConvention(paramType, null, false) } diff --git a/codeGenCpu6502/src/prog8/codegen/cpu6502/FunctionCallAsmGen.kt b/codeGenCpu6502/src/prog8/codegen/cpu6502/FunctionCallAsmGen.kt index a989593b2..a23bff4f6 100644 --- a/codeGenCpu6502/src/prog8/codegen/cpu6502/FunctionCallAsmGen.kt +++ b/codeGenCpu6502/src/prog8/codegen/cpu6502/FunctionCallAsmGen.kt @@ -15,6 +15,7 @@ internal class FunctionCallAsmGen(private val program: PtProgram, private val as // 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) = (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) diff --git a/docs/source/comparing.rst b/docs/source/comparing.rst index 0ef027d3a..e85a5f62a 100644 --- a/docs/source/comparing.rst +++ b/docs/source/comparing.rst @@ -58,6 +58,7 @@ Variables 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). - 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 diff --git a/docs/source/technical.rst b/docs/source/technical.rst index a016fc452..173b1c77e 100644 --- a/docs/source/technical.rst +++ b/docs/source/technical.rst @@ -137,42 +137,65 @@ Calling a subroutine requires three steps: #. 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, -that have their arguments solely passed into specific registers. -Sometimes even via a processor status flag such as the Carry flag. -Return values also via designated registers. -The processor status flag is preserved on returning so you can immediately act on that for instance -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): +- Each subroutine parameter is represented as a variable scoped to the subroutine. Prog8 doesn't have a call stack. +- The arguments passed in a subroutine call are evaluated by the caller, and then put into those variables by the caller. + The order of evaluation of subroutine call arguments *is unspecified* and should not be relied upon. +- The subroutine is invoked. +- The return value is not put into a variable, but the subroutine passes it back to the caller via cpu register(s): Byte values will be put in ``A`` . - Word values will be put in ``A`` + ``Y`` register pair. - Float values will be put in the ``FAC1`` float 'register' (BASIC allocated this somewhere in ram). + Boolean values will be put in ``A`` too, as 0 or 1. + 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 -doing something with that returnvalue. This can be on purpose if you're simply not interested in it. -Use the ``void`` keyword in front of the subroutine call to get rid of the warning in that case. +These are kernal (ROM) routines or low-level assembly routines, that get their arguments via specific registers. +Sometimes even via a processor status flag such as the Carry flag. +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 diff --git a/docs/source/todo.rst b/docs/source/todo.rst index 7a9657cf6..4dcd7184d 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -7,8 +7,10 @@ TODO 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 -- 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 '? - 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+ diff --git a/examples/test.p8 b/examples/test.p8 index 8b2c77b76..ffef922f9 100644 --- a/examples/test.p8 +++ b/examples/test.p8 @@ -1,7 +1,51 @@ +%import floats + main { sub start() { - ubyte @shared v1, v2 - if v1==0 and v2 & 3 !=0 - v1++ + thing() + thang() + 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 + }} } }