Skip to content

Commit

Permalink
allow multi-assign to skip any status register result
Browse files Browse the repository at this point in the history
  • Loading branch information
irmen committed Mar 29, 2024
1 parent 0c5e8ca commit 2a5afbc
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 116 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -84,24 +84,39 @@ internal class AssignmentAsmGen(private val program: PtProgram,
}
}

// because we can only handle integer results right now we can just zip() it all up
val (statusFlagResult, registersResults) = sub.returns.zip(assignment.children).partition { it.first.register.statusflag!=null }
if(statusFlagResult.isNotEmpty()) {
val (returns, target) = statusFlagResult.single()
if(returns.register.statusflag!=Statusflag.Pc)
TODO("other status flag for return value")

target as PtAssignTarget
if(registersResults.all { (it.second as PtAssignTarget).identifier!=null}) {
// all other results are just stored into identifiers directly so first handle those
// (simple store instructions that don't modify the carry flag)
assignRegisterResults(registersResults)
assignCarryResult(target, false)
return
val assignmentTargets = assignment.children.dropLast(1)
if(sub.returns.size==assignmentTargets.size) {
// because we can only handle integer results right now we can just zip() it all up
val (statusFlagResult, registersResults) = sub.returns.zip(assignmentTargets).partition { it.first.register.statusflag!=null }
if(statusFlagResult.isNotEmpty()) {
val (returns, target) = statusFlagResult.single()
if(returns.register.statusflag!=Statusflag.Pc)
TODO("other status flag for return value")

target as PtAssignTarget
if(registersResults.all { (it.second as PtAssignTarget).identifier!=null}) {
// all other results are just stored into identifiers directly so first handle those
// (simple store instructions that don't modify the carry flag)
assignRegisterResults(registersResults)
assignCarryResult(target, false)
return
}
assignCarryResult(target, needsToSaveA(registersResults))
}
assignCarryResult(target, needsToSaveA(registersResults))
assignRegisterResults(registersResults)
} else if (sub.returns.size>assignmentTargets.size) {
// Targets and values don't match. Skip status flag results, assign only the normal value results.
val targets = assignmentTargets.iterator()
sub.returns.forEach {
if(it.register.registerOrPair!=null) {
val target = targets.next() as PtAssignTarget
assignRegisterResults(listOf(it to target))
}
}
require(!targets.hasNext())
} else {
throw AssemblyError("number of values and targets don't match")
}
assignRegisterResults(registersResults)
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,30 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val express
require(funcCall.multipleResultRegs.size + funcCall.multipleResultFpRegs.size >= 2)
if(funcCall.multipleResultFpRegs.isNotEmpty())
TODO("deal with (multiple?) FP return registers")

// because we can only handle integer results right now we can just zip() it all up
val assignmentTargets = assignment.children.dropLast(1)
addToResult(result, funcCall, funcCall.resultReg, funcCall.resultFpReg)
sub.returns.zip(assignment.children).zip(funcCall.multipleResultRegs).forEach {
val regNumber = it.second
val returns = it.first.first
val target = it.first.second as PtAssignTarget
result += assignCpuRegister(returns, regNumber, target)
if(sub.returns.size==assignmentTargets.size) {
// Targets and values match. Assign all the things.
sub.returns.zip(assignmentTargets).zip(funcCall.multipleResultRegs).forEach {
val regNumber = it.second
val returns = it.first.first
val target = it.first.second as PtAssignTarget
result += assignCpuRegister(returns, regNumber, target)
}
} else if (sub.returns.size>assignmentTargets.size) {
// Targets and values don't match. Skip status flag results, assign only the normal value results.
val targets = assignmentTargets.iterator()
sub.returns.zip(funcCall.multipleResultRegs).forEach {
val returns = it.first
if(returns.register.registerOrPair!=null) {
val target = targets.next() as PtAssignTarget
val regNumber = it.second
result += assignCpuRegister(returns, regNumber, target)
}
}
require(!targets.hasNext())
} else {
throw AssemblyError("number of values and targets don't match")
}
return result
} else {
Expand Down
74 changes: 47 additions & 27 deletions compiler/src/prog8/compiler/astprocessing/AstChecker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -547,47 +547,67 @@ internal class AstChecker(private val program: Program,
}


// multi-assign: check the number of assign targets vs. the number of return values of the subroutine
// also check the types of the variables vs the types of each return value
val fcall = assignment.value as? IFunctionCall
val fcallTarget = fcall?.target?.targetSubroutine(program)
if(assignment.target.multi!=null) {
val multi = assignment.target.multi!!
if(fcall==null) {
errors.err("expected a function call with multiple return values", assignment.value.position)
} else {
if(fcallTarget==null) {
errors.err("expected a function call with multiple return values", assignment.value.position)
} else {
if(fcallTarget.returntypes.size!=multi.size) {
errors.err("expected ${multi.size} return values, have ${fcallTarget.returntypes.size}", fcall.position)
}
}
}

if(errors.noErrors()) {
// check the types...
fcallTarget!!.returntypes.zip(multi).withIndex().forEach { (index, p) ->
val (returnType, target) = p
val targetDt = target.inferType(program).getOr(DataType.UNDEFINED)
if(!(returnType isAssignableTo targetDt))
errors.err("can't assign returnvalue #${index+1} to corresponding target; ${returnType} vs $targetDt", target.position)
}
}

checkMultiAssignment(assignment, fcall, fcallTarget)
} else if(fcallTarget!=null) {
if(fcallTarget.returntypes.size!=1) {
// If there are 2 return values, one of them being a boolean in a status register, this is okay.
// In that case the normal value is assigned and the status bit is dealth with separately for example with if_cs
val (returnRegisters, _) = fcallTarget.asmReturnvaluesRegisters.partition { rr -> rr.registerOrPair != null }
if(returnRegisters.size>1)
errors.err("expected 1 return value, have ${fcallTarget.returntypes.size}", fcall.position)
if(returnRegisters.size>1) {
errors.err("multiple return values and too few assignment targets, need at least ${returnRegisters.size}", fcall.position)
}
}
}

super.visit(assignment)
}

private fun checkMultiAssignment(assignment: Assignment, fcall: IFunctionCall?, fcallTarget: Subroutine?) {
// multi-assign: check the number of assign targets vs. the number of return values of the subroutine
// also check the types of the variables vs the types of each return value
if(fcall==null || fcallTarget==null) {
errors.err("expected a function call with multiple return values", assignment.value.position)
return
}
val targets = assignment.target.multi!!
if(fcallTarget.returntypes.size<targets.size) {
errors.err("too many assignment targets, ${targets.size} targets for ${fcallTarget.returntypes.size} return values", fcall.position)
return
}
if(fcallTarget.returntypes.size>targets.size) {
// You can have LESS assign targets than the number of result values,
// as long as the result values contain booleans that are returned in cpu status flags (like Carry).
// These may be ignored in the assignment - only "true" values NEED to have a target.
val numberOfNormalValues = fcallTarget.asmReturnvaluesRegisters.count { it.registerOrPair!=null }
if(numberOfNormalValues != targets.size) {
errors.err("multiple return values and too few assignment targets, need at least $numberOfNormalValues", fcall.position)
return
}
// check the types of the 'normal' values that are being assigned
val returnTypesAndRegisters = fcallTarget.returntypes.zip(fcallTarget.asmReturnvaluesRegisters)
returnTypesAndRegisters.zip(targets).withIndex().forEach { (index, p) ->
val (returnType, register) = p.first
if(register.registerOrPair!=null) {
val target = p.second
val targetDt = target.inferType(program).getOr(DataType.UNDEFINED)
if (!(returnType isAssignableTo targetDt))
errors.err("can't assign returnvalue #${index + 1} to corresponding target; $returnType vs $targetDt", target.position)
}
}
} else {
// check all the assigment target types
fcallTarget.returntypes.zip(targets).withIndex().forEach { (index, p) ->
val (returnType, target) = p
val targetDt = target.inferType(program).getOr(DataType.UNDEFINED)
if (!(returnType isAssignableTo targetDt))
errors.err("can't assign returnvalue #${index + 1} to corresponding target; $returnType vs $targetDt", target.position)
}
}
}


override fun visit(assignTarget: AssignTarget) {
super.visit(assignTarget)
Expand Down
72 changes: 34 additions & 38 deletions compiler/test/ast/TestSubroutines.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package prog8tests.ast

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain
import prog8.ast.statements.Block
import prog8.ast.statements.Subroutine
Expand Down Expand Up @@ -110,9 +111,11 @@ main {
main {
sub start() {
bool @shared flag
ubyte @shared bytevar
cx16.r0L, flag = test2(12345, 5566, flag, -42)
cx16.r1, flag = test3()
cx16.r1, flag, bytevar = test3()
cx16.r1, bytevar = test3() ; omitting the status flag result should also work
}
asmsub test2(uword arg @AY, uword arg2 @R1, bool flag @Pc, byte value @X) -> ubyte @A, bool @Pc {
Expand All @@ -123,85 +126,78 @@ main {
}}
}
asmsub test3() -> uword @R1, bool @Pc {
asmsub test3() -> uword @R1, bool @Pc, ubyte @X {
%asm {{
lda #0
ldy #0
ldx #0
rts
}}
}
}"""
compileText(VMTarget(), false, src, writeAssembly = true) shouldNotBe null
val errors = ErrorReporterForTests()
val result = compileText(Cx16Target(), false, src, errors, true)!!
errors.errors.size shouldBe 0
val start = result.codegenAst!!.entrypoint()!!
start.children.size shouldBe 5
val a1_1 = start.children[2] as PtAssignment
val a1_2 = start.children[3] as PtAssignment
start.children.size shouldBe 8
val a1_1 = start.children[4] as PtAssignment
val a1_2 = start.children[5] as PtAssignment
val a1_3 = start.children[6] as PtAssignment
a1_1.multiTarget shouldBe true
a1_2.multiTarget shouldBe true
a1_3.multiTarget shouldBe true
a1_1.children.size shouldBe 3
a1_2.children.size shouldBe 4
a1_3.children.size shouldBe 3
(a1_1.children[0] as PtAssignTarget).identifier!!.name shouldBe("cx16.r0L")
(a1_1.children[1] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_flag")
(a1_2.children[0] as PtAssignTarget).identifier!!.name shouldBe("cx16.r1")
(a1_2.children[1] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_flag")

errors.clear()
val result2=compileText(VMTarget(), false, src, errors, true)!!
errors.errors.size shouldBe 0
val start2 = result2.codegenAst!!.entrypoint()!!
start2.children.size shouldBe 5
val a2_1 = start2.children[2] as PtAssignment
val a2_2 = start2.children[3] as PtAssignment
a2_1.multiTarget shouldBe true
a2_2.multiTarget shouldBe true
(a2_1.children[0] as PtAssignTarget).identifier!!.name shouldBe("cx16.r0L")
(a2_1.children[1] as PtAssignTarget).identifier!!.name shouldBe("main.start.flag")
(a2_2.children[0] as PtAssignTarget).identifier!!.name shouldBe("cx16.r1")
(a2_2.children[1] as PtAssignTarget).identifier!!.name shouldBe("main.start.flag")
(a1_2.children[2] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_bytevar")
(a1_3.children[0] as PtAssignTarget).identifier!!.name shouldBe("cx16.r1")
(a1_3.children[1] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_bytevar")
}

test("multi-assign from romsub") {
val src="""
main {
sub start() {
bool @shared flag
ubyte @shared bytevar
flag = test(42)
cx16.r0L, flag = test2(12345, 5566, flag, -42)
cx16.r1, flag = test3()
cx16.r1, flag, bytevar = test3()
cx16.r1, bytevar = test3() ; omitting the status flag result should also work
}
romsub ${'$'}8000 = test(ubyte arg @A) -> bool @Pc
romsub ${'$'}8002 = test2(uword arg @AY, uword arg2 @R1, bool flag @Pc, byte value @X) -> ubyte @A, bool @Pc
romsub ${'$'}8003 = test3() -> uword @R1, bool @Pc
romsub ${'$'}8003 = test3() -> uword @R1, bool @Pc, ubyte @X
}"""

compileText(VMTarget(), false, src, writeAssembly = true) shouldNotBe null
val errors = ErrorReporterForTests()
val result = compileText(Cx16Target(), false, src, errors, true)!!
errors.errors.size shouldBe 0
val start = result.codegenAst!!.entrypoint()!!
start.children.size shouldBe 5
val a1_1 = start.children[2] as PtAssignment
val a1_2 = start.children[3] as PtAssignment
start.children.size shouldBe 9
val a1_1 = start.children[5] as PtAssignment
val a1_2 = start.children[6] as PtAssignment
val a1_3 = start.children[7] as PtAssignment
a1_1.multiTarget shouldBe true
a1_2.multiTarget shouldBe true
a1_3.multiTarget shouldBe true
a1_1.children.size shouldBe 3
a1_2.children.size shouldBe 4
a1_3.children.size shouldBe 3
(a1_1.children[0] as PtAssignTarget).identifier!!.name shouldBe("cx16.r0L")
(a1_1.children[1] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_flag")
(a1_2.children[0] as PtAssignTarget).identifier!!.name shouldBe("cx16.r1")
(a1_2.children[1] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_flag")

errors.clear()
val result2=compileText(VMTarget(), false, src, errors, true)!!
errors.errors.size shouldBe 0
val start2 = result2.codegenAst!!.entrypoint()!!
start2.children.size shouldBe 5
val a2_1 = start2.children[2] as PtAssignment
val a2_2 = start2.children[3] as PtAssignment
a2_1.multiTarget shouldBe true
a2_2.multiTarget shouldBe true
(a2_1.children[0] as PtAssignTarget).identifier!!.name shouldBe("cx16.r0L")
(a2_1.children[1] as PtAssignTarget).identifier!!.name shouldBe("main.start.flag")
(a2_2.children[0] as PtAssignTarget).identifier!!.name shouldBe("cx16.r1")
(a2_2.children[1] as PtAssignTarget).identifier!!.name shouldBe("main.start.flag")
(a1_2.children[2] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_bytevar")
(a1_3.children[0] as PtAssignTarget).identifier!!.name shouldBe("cx16.r1")
(a1_3.children[1] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_bytevar")
}
})
17 changes: 7 additions & 10 deletions docs/source/syntaxreference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -684,16 +684,13 @@ are all assigned to individual assignment targets. You simply write them as a co

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

**There is also a special rule:** if there's just one return value in a register, and one or more others that are returned
as bits in the status register (such as the Carry bit), the compiler *also* allows you to call the subroutine and just assign a *single* return value.
It will then store the result value in a variable if required, and *try to keep the status register untouched
after the call* so you can often use a conditional branch statement for that. But the latter is tricky,
make sure you check the generated assembly code.

.. note::
For asmsubs or romsubs that return a boolean status flag in a cpu status register such as the Carry flag,
it is always more efficient to use a conditional branch like `if_cs` to act on that value, than storing
it in a variable and then adding an `if flag...` statement afterwards.
**There is also a special rule:** you are allowed to omit assignments of the boolean values returned in status registers such as the carry flag.
So in the case of multisub() above, you could also write `wordvar, bytevar = multisub()` and leave out the carry flag.
The compiler will try to assign the normal numeric values and leave the status flags untouched, which then allows you to
use a conditional branch such as `if_cs` to do something with it. This is always more efficient
than storing it in a variable and then adding an `if flag...` statement afterwards.
It can sometimes be tricky to keep the status flags that are returned from the subroutine intact though,
if the assign targets are not simple variables. In such cases, make sure you check the generated assembly code to see if it all works out.


Subroutine definitions
Expand Down
2 changes: 1 addition & 1 deletion docs/source/todo.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
TODO
====

make it possible to omit the Status Register values in multi-assigns regardless of the number of return values (is now 1 value)
check souce code of examples and library, for void calls that could now be turned into multi-assign calls.

...

Expand Down
Loading

0 comments on commit 2a5afbc

Please sign in to comment.