Skip to content

Commit

Permalink
IR support for multi-value returns in normal subroutines, documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
irmen committed Jan 9, 2025
1 parent a6f9ed0 commit 66558f7
Show file tree
Hide file tree
Showing 11 changed files with 65 additions and 26 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ What does Prog8 provide?
- ``defer`` statement to help write concise and robust subroutine cleanup logic
- several specialized built-in functions such as ``lsb``, ``msb``, ``min``, ``max``, ``rol``, ``ror``
- various powerful built-in libraries to do I/O, number conversions, graphics and more
- subroutines can return more than one result value
- inline assembly allows you to have full control when every cycle or byte matters
- supports the sixteen 'virtual' 16-bit registers R0 - R15 from the Commander X16 (also available on other targets)
- encode strings and characters into petscii or screencodes or even other encodings
Expand Down
2 changes: 1 addition & 1 deletion codeGenCpu6502/src/prog8/codegen/cpu6502/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal fun IPtSubroutine.returnsWhatWhere(): List<Pair<RegisterOrStatusflag, D
return listOf(Pair(register, returntype))
}
else -> {
// TODO for multi-value results, put the first one in register(s) and only the rest elsewhere (like stack)???
// TODO for multi-value results, put the first one in register(s) and only the rest in the virtual registers?
throw AssemblyError("multi-value returns from a normal subroutine are not put into registers, this routine shouldn't have been called in this scenario")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val express

val result = mutableListOf<IRCodeChunkBase>()
val funcCall = expressionEval.translate(values)
require(funcCall.multipleResultRegs.size + funcCall.multipleResultFpRegs.size >= 2)
if (funcCall.multipleResultFpRegs.isNotEmpty())
TODO("deal with (multiple?) FP return registers")
val assignmentTargets = assignment.children.dropLast(1)
addToResult(result, funcCall, funcCall.resultReg, funcCall.resultFpReg)

val extsub = codeGen.symbolTable.lookup(values.name) as? StExtSub
if(extsub!=null) {
require(funcCall.multipleResultRegs.size + funcCall.multipleResultFpRegs.size >= 2)
if (funcCall.multipleResultFpRegs.isNotEmpty())
TODO("deal with (multiple?) FP return registers")
if (extsub.returns.size == assignmentTargets.size) {
// Targets and values match. Assign all the things. Skip 'void' targets.
extsub.returns.zip(assignmentTargets).zip(funcCall.multipleResultRegs).forEach {
Expand All @@ -41,7 +41,17 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val express
} else {
val normalsub = codeGen.symbolTable.lookup(values.name) as? StSub
if (normalsub != null) {
TODO()
// multi-value returns are passed throug cx16.R15 down to R0 (allows unencumbered use of many Rx registers if you don't return that many values)
val registersReverseOrder = Cx16VirtualRegisters.reversed()
normalsub.returns.zip(assignmentTargets).zip(registersReverseOrder).forEach {
val target = it.first.second as PtAssignTarget
if(!target.void) {
val assignSingle = PtAssignment(assignment.position)
assignSingle.add(target)
assignSingle.add(PtIdentifier("cx16.${it.second.toString().lowercase()}", it.first.first, assignment.position))
result += translateRegularAssign(assignSingle)
}
}
}
else throw AssemblyError("expected extsub or normal sub")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -645,15 +645,17 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
addInstr(result, IRInstruction(Opcode.CALL, labelSymbol = fcall.name,
fcallArgs = FunctionCallArgs(argRegisters, returnRegSpecs)), null)
return if(fcall.void)
ExpressionCodeResult(result, IRDataType.BYTE, -1, -1) // TODO void?
ExpressionCodeResult(result, IRDataType.BYTE, -1, -1) // TODO datatype void?
else if(returnRegSpecs.size==1) {
val returnRegSpec = returnRegSpecs.single()
if (fcall.type.isFloat)
ExpressionCodeResult(result, returnRegSpec.dt, -1, returnRegSpec.registerNum)
else
ExpressionCodeResult(result, returnRegSpec.dt, returnRegSpec.registerNum, -1)
} else {
TODO("multi-value return ; expression result")
// multi-value returns are passed throug cx16.R15 down to R0 (allows unencumbered use of many Rx registers if you don't return that many values)
// so the actual result of the expression here is 'void' (doesn't use IR virtual registers at all)
ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
}
}
is StExtSub -> {
Expand Down
13 changes: 12 additions & 1 deletion codeGenIntermediate/src/prog8/codegen/intermediate/IRCodeGen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1759,8 +1759,19 @@ class IRCodeGen(
private fun translate(ret: PtReturn): IRCodeChunks {
val result = mutableListOf<IRCodeChunkBase>()
if(ret.children.size>1) {
TODO("multi-value return")
// multi-value returns are passed throug cx16.R15 down to R0 (allows unencumbered use of many Rx registers if you don't return that many values)
val registersReverseOrder = Cx16VirtualRegisters.reversed()
for ((value, register) in ret.children.zip(registersReverseOrder)) {
val tr = expressionEval.translateExpression(value as PtExpression)
addToResult(result, tr, tr.resultReg, -1)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.STOREM, tr.dt, reg1=tr.resultReg, labelSymbol = "cx16.${register.toString().lowercase()}")
it += IRInstruction(Opcode.RETURN)
}
}
return result
}

val value = ret.children.singleOrNull()
if(value==null) {
addInstr(result, IRInstruction(Opcode.RETURN), null)
Expand Down
3 changes: 2 additions & 1 deletion compiler/test/TestSubroutines.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import prog8.ast.statements.*
import prog8.code.core.BaseDataType
import prog8.code.core.DataType
import prog8.code.target.C64Target
import prog8.code.target.VMTarget
import prog8.compiler.astprocessing.hasRtsInAsm
import prog8tests.helpers.ErrorReporterForTests
import prog8tests.helpers.compileText
Expand Down Expand Up @@ -309,6 +310,6 @@ main {
}
}"""
compileText(C64Target(), false, src, writeAssembly = true).shouldNotBeNull()
// compileText(VMTarget(), false, src, writeAssembly = true).shouldNotBeNull() TODO("multi-value return ; unittest")
compileText(VMTarget(), false, src, writeAssembly = true).shouldNotBeNull()
}
})
2 changes: 1 addition & 1 deletion docs/source/comparing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Subroutines
- There is no call stack for subroutine arguments: subroutine parameters are overwritten when called again. Thus recursion is not easily possible, but you can do it with manual stack manipulations.
There are a couple of example programs that show how to solve this in different ways, among which are fractal-tree.p8, maze.p8 and queens.p8
- 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.
- Subroutines 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
instead of defining the same sort of variables in every subroutine.
This reduces the memory needed for variables. A convenient way to do this is by using nested subroutines - these can easily access the
Expand Down
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ Features
- Encode strings and characters into petscii or screencodes or even other encodings, as desired (C64/Cx16)
- Automatic ROM/RAM bank switching on certain compiler targets when calling routines in other banks
- Identifiers can contain Unicode Letters, so ``knäckebröd``, ``приблизительно``, ``見せしめ`` and ``π`` are all valid identifiers.
- Subroutines can return more than one result value
- Advanced code optimizations to make the resulting program smaller and faster
- Programs can be restarted after exiting (i.e. run them multiple times without having to reload everything), due to automatic variable (re)initializations.
- Supports the sixteen 'virtual' 16-bit registers R0 to R15 as defined on the Commander X16. You can look at them as general purpose global variables. These are also available on the other compilation targets!
Expand Down
31 changes: 19 additions & 12 deletions docs/source/programming.rst
Original file line number Diff line number Diff line change
Expand Up @@ -778,8 +778,8 @@ for normal assignments (``aa = aa + xx``).
It is possible to "chain" assignments: ``x = y = z = 42``, this is just a shorthand
for the three individual assignments with the same value 42.

Only for certain subroutines that return multiple values it is possible to write a "multi assign" statement
with comma separated assignment targets, that assigns those multiple values to different targets in one statement.
For subroutines that return multiple values, you should write a "multi assign" statement
with comma separated assignment targets, to assigns those multiple values.
Details can be found here: :ref:`multiassign`.


Expand Down Expand Up @@ -1130,10 +1130,11 @@ Otherwise the compiler will warn you about discarding the result of the call.

Multiple return values
^^^^^^^^^^^^^^^^^^^^^^
Normal subroutines can only return zero or one return values.
However, the special ``asmsub`` routines (implemented in assembly code) or ``extsub`` routines
(referencing an external routine in ROM or elsewhere in RAM) can return more than one return value.
For example a status in the carry bit and a number in A, or a 16-bit value in A/Y registers and some more values in R0 and R1.
Subroutines can return more than one value.
For example, ``asmsub`` routines (implemented in assembly code) or ``extsub`` routines
(referencing an external routine in ROM or elsewhere in RAM) can return multiple values spread
across different registers, and even the CPU's status register flags for boolean values.
Normal subroutines can also return multiple values (restricted to booleans, bytes and word values).
In all of these cases, you have to "multi assign" all return values of the subroutine call to something.
You simply write the assignment targets as a comma separated list,
where the element's order corresponds to the order of the return values declared in the subroutine's signature.
Expand All @@ -1147,13 +1148,19 @@ So for instance::

asmsub multisub() -> uword @AY, bool @Pc, ubyte @X { ... }

.. sidebar:: Using just one of the values
.. sidebar:: usage of cx16.r0-cx16.r15

Sometimes it is easier to just have a single return value in the subroutine's signagure (even though it
actually may return multiple values): this avoids having to put ``void`` for all other values.
It also allows it to be called in expressions such as if-statements again.
Examples of these second 'convenience' definition are library routines such as ``cbm.STOP2`` and ``cbm.GETIN2``,
that only return a single value where the "official" versions ``STOP`` and ``GETIN`` always return multiple values.
Subroutines with multiple return values use the "virtual registers" to return those.
Using those virtual registers during the calculation of the values in the return statement should be avoided.
Otherwise you risk overwriting an earlier return value in the sequence.


**Using just one of the values:**
Sometimes it is easier to just have a single return value in a subroutine's signagure (even though it
actually may return multiple values): this avoids having to put ``void`` for all other values if you aren't really interested in those.
It also allows it to be called in expressions such as if-statements again.
Examples of these second 'convenience' definition are library routines such as ``cbm.STOP2`` and ``cbm.GETIN2``,
that only return a single value where the "official" versions ``STOP`` and ``GETIN`` always return multiple values.

**Skipping values:** Instead of using ``void`` to ignore the result of a subroutine call altogether,
you can also use it as a placeholder name in a multi-assignment. This skips assignment of the return value in that place.
Expand Down
8 changes: 8 additions & 0 deletions docs/source/technical.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ Regular subroutines
- A word value will be put in ``A`` + ``Y`` register pair (lsb in A, msb in Y).
- A float value will be put in the ``FAC1`` float 'register'.

- In case of *multiple* return values:

- for an ``asmsub`` or ``extsub`` the subroutine's signature specifies the output registers that contain the values explicitly,
just as for a single return value.
- for regular subroutines, the compiler will use the "virtual registers" cx16.r0-cx16.r15, from r15 down to r0, for the
result values left to right. This may change in a future compiler version.


**Builtin functions can be different:**
some builtin functions are special and won't exactly follow these rules.

Expand Down
6 changes: 2 additions & 4 deletions docs/source/todo.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
TODO
====

- implement IR support for the TODO("multi-value occurences in both codegens, to handle multi-value subroutine return values. Fix the unittest too.
- document new multi-value return feature (only bool/byte/word types supported, call convention)

- change library routines that now return 1 value + say, another in R0, to just return 2 values now that this is supported for normal subroutines too.
- rename "intermediate AST" into "simplified AST" (docs + classes in code)

- add paypal donation button as well?
Expand All @@ -29,7 +27,6 @@ Future Things and Ideas
Maybe propose a patch to 64tass itself that will treat .proc as .block ?
Once new codegen is written that is based on the IR, this point is mostly moot anyway as that will have its own dead code removal on the IR level.

- Allow normal subroutines to return multiple values as well (just as asmsubs already can)
- Change scoping rules for qualified symbols so that they don't always start from the root but behave like other programming languages (look in local scope first)
- something to reduce the need to use fully qualified names all the time. 'with' ? Or 'using <prefix>'?
- Improve register load order in subroutine call args assignments:
Expand Down Expand Up @@ -82,6 +79,7 @@ Libraries
Optimizations
-------------

- Multi-value returns of normal subroutines: use cpu register A or AY for the first one and only start using virtual registers for the rest.
- Optimize the IfExpression code generation to be more like regular if-else code. (both 6502 and IR) search for "TODO don't store condition as expression"
- VariableAllocator: can we think of a smarter strategy for allocating variables into zeropage, rather than first-come-first-served?
for instance, vars used inside loops first, then loopvars, then uwords used as pointers (or these first??), then the rest
Expand Down

0 comments on commit 66558f7

Please sign in to comment.