allow multi-assign to skip any status register result

This commit is contained in:
Irmen de Jong 2024-03-29 16:58:13 +01:00
parent 0c5e8ca199
commit 3e34a3ef72
7 changed files with 151 additions and 115 deletions

View File

@ -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")
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
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)
}

View File

@ -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 {

View File

@ -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)

View File

@ -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
@ -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 {
@ -123,41 +126,37 @@ 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") {
@ -165,43 +164,40 @@ main {
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")
}
})

View File

@ -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

View File

@ -1,7 +1,10 @@
TODO
====
make it possible to omit the Status Register values in multi-assigns regardless of the number of return values (is now 1 value)
try to replace the variable number of assignment targets by allowing placeholder '_' target
check souce code of examples and library, for void calls that could now be turned into multi-assign calls.
...

View File

@ -1,41 +1,30 @@
%import textio
%import test_stack
%zeropage basicsafe
%option no_sysinit
main {
sub start() {
bool @shared flag
ubyte @shared bytevar
uword @shared wordvar
; cx16.r1=9999
; flag = test(42)
; cx16.r0L, flag = test2(12345, 5566, flag, -42)
; cx16.r1, flag = test3()
wordvar, bytevar, flag = test4()
wordvar, bytevar, flag = test4()
wordvar, bytevar = test4()
if_cs
txt.print("true! ")
else
txt.print("false! ")
txt.print_uwhex(wordvar, true)
txt.spc()
txt.print_bool(flag)
txt.spc()
txt.print_ub(bytevar)
txt.nl()
}
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
asmsub test4() -> uword @AY, ubyte @X, bool @Pc {
%asm {{
lda #<$11ee
ldy #>$11ee
ldx #42
sec
clc
rts
}}
}