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 assignmentTargets = assignment.children.dropLast(1)
val (statusFlagResult, registersResults) = sub.returns.zip(assignment.children).partition { it.first.register.statusflag!=null } if(sub.returns.size==assignmentTargets.size) {
if(statusFlagResult.isNotEmpty()) { // because we can only handle integer results right now we can just zip() it all up
val (returns, target) = statusFlagResult.single() val (statusFlagResult, registersResults) = sub.returns.zip(assignmentTargets).partition { it.first.register.statusflag!=null }
if(returns.register.statusflag!=Statusflag.Pc) if(statusFlagResult.isNotEmpty()) {
TODO("other status flag for return value") val (returns, target) = statusFlagResult.single()
if(returns.register.statusflag!=Statusflag.Pc)
TODO("other status flag for return value")
target as PtAssignTarget target as PtAssignTarget
if(registersResults.all { (it.second as PtAssignTarget).identifier!=null}) { if(registersResults.all { (it.second as PtAssignTarget).identifier!=null}) {
// all other results are just stored into identifiers directly so first handle those // all other results are just stored into identifiers directly so first handle those
// (simple store instructions that don't modify the carry flag) // (simple store instructions that don't modify the carry flag)
assignRegisterResults(registersResults) assignRegisterResults(registersResults)
assignCarryResult(target, false) assignCarryResult(target, false)
return 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) require(funcCall.multipleResultRegs.size + funcCall.multipleResultFpRegs.size >= 2)
if(funcCall.multipleResultFpRegs.isNotEmpty()) if(funcCall.multipleResultFpRegs.isNotEmpty())
TODO("deal with (multiple?) FP return registers") TODO("deal with (multiple?) FP return registers")
val assignmentTargets = assignment.children.dropLast(1)
// because we can only handle integer results right now we can just zip() it all up
addToResult(result, funcCall, funcCall.resultReg, funcCall.resultFpReg) addToResult(result, funcCall, funcCall.resultReg, funcCall.resultFpReg)
sub.returns.zip(assignment.children).zip(funcCall.multipleResultRegs).forEach { if(sub.returns.size==assignmentTargets.size) {
val regNumber = it.second // Targets and values match. Assign all the things.
val returns = it.first.first sub.returns.zip(assignmentTargets).zip(funcCall.multipleResultRegs).forEach {
val target = it.first.second as PtAssignTarget val regNumber = it.second
result += assignCpuRegister(returns, regNumber, target) 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 return result
} else { } 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 fcall = assignment.value as? IFunctionCall
val fcallTarget = fcall?.target?.targetSubroutine(program) val fcallTarget = fcall?.target?.targetSubroutine(program)
if(assignment.target.multi!=null) { if(assignment.target.multi!=null) {
val multi = assignment.target.multi!! checkMultiAssignment(assignment, fcall, fcallTarget)
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)
}
}
} else if(fcallTarget!=null) { } else if(fcallTarget!=null) {
if(fcallTarget.returntypes.size!=1) { if(fcallTarget.returntypes.size!=1) {
// If there are 2 return values, one of them being a boolean in a status register, this is okay. // 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 // 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 } val (returnRegisters, _) = fcallTarget.asmReturnvaluesRegisters.partition { rr -> rr.registerOrPair != null }
if(returnRegisters.size>1) if(returnRegisters.size>1) {
errors.err("expected 1 return value, have ${fcallTarget.returntypes.size}", fcall.position) errors.err("multiple return values and too few assignment targets, need at least ${returnRegisters.size}", fcall.position)
}
} }
} }
super.visit(assignment) 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) { override fun visit(assignTarget: AssignTarget) {
super.visit(assignTarget) super.visit(assignTarget)

View File

@@ -2,6 +2,7 @@ package prog8tests.ast
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldContain
import prog8.ast.statements.Block import prog8.ast.statements.Block
import prog8.ast.statements.Subroutine import prog8.ast.statements.Subroutine
@@ -110,9 +111,11 @@ main {
main { main {
sub start() { sub start() {
bool @shared flag bool @shared flag
ubyte @shared bytevar
cx16.r0L, flag = test2(12345, 5566, flag, -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
} }
asmsub test2(uword arg @AY, uword arg2 @R1, bool flag @Pc, byte value @X) -> ubyte @A, bool @Pc { 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 {{ %asm {{
lda #0 lda #0
ldy #0 ldy #0
ldx #0
rts rts
}} }}
} }
}""" }"""
compileText(VMTarget(), false, src, writeAssembly = true) shouldNotBe null
val errors = ErrorReporterForTests() val errors = ErrorReporterForTests()
val result = compileText(Cx16Target(), false, src, errors, true)!! val result = compileText(Cx16Target(), false, src, errors, true)!!
errors.errors.size shouldBe 0 errors.errors.size shouldBe 0
val start = result.codegenAst!!.entrypoint()!! val start = result.codegenAst!!.entrypoint()!!
start.children.size shouldBe 5 start.children.size shouldBe 8
val a1_1 = start.children[2] as PtAssignment val a1_1 = start.children[4] as PtAssignment
val a1_2 = start.children[3] as PtAssignment val a1_2 = start.children[5] as PtAssignment
val a1_3 = start.children[6] as PtAssignment
a1_1.multiTarget shouldBe true a1_1.multiTarget shouldBe true
a1_2.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[0] as PtAssignTarget).identifier!!.name shouldBe("cx16.r0L")
(a1_1.children[1] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_flag") (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[0] as PtAssignTarget).identifier!!.name shouldBe("cx16.r1")
(a1_2.children[1] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_flag") (a1_2.children[1] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_flag")
(a1_2.children[2] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_bytevar")
errors.clear() (a1_3.children[0] as PtAssignTarget).identifier!!.name shouldBe("cx16.r1")
val result2=compileText(VMTarget(), false, src, errors, true)!! (a1_3.children[1] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_bytevar")
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")
} }
test("multi-assign from romsub") { test("multi-assign from romsub") {
@@ -165,43 +164,40 @@ main {
main { main {
sub start() { sub start() {
bool @shared flag bool @shared flag
ubyte @shared bytevar
flag = test(42) flag = test(42)
cx16.r0L, flag = test2(12345, 5566, flag, -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 ${'$'}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 ${'$'}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 errors = ErrorReporterForTests()
val result = compileText(Cx16Target(), false, src, errors, true)!! val result = compileText(Cx16Target(), false, src, errors, true)!!
errors.errors.size shouldBe 0 errors.errors.size shouldBe 0
val start = result.codegenAst!!.entrypoint()!! val start = result.codegenAst!!.entrypoint()!!
start.children.size shouldBe 5 start.children.size shouldBe 9
val a1_1 = start.children[2] as PtAssignment val a1_1 = start.children[5] as PtAssignment
val a1_2 = start.children[3] as PtAssignment val a1_2 = start.children[6] as PtAssignment
val a1_3 = start.children[7] as PtAssignment
a1_1.multiTarget shouldBe true a1_1.multiTarget shouldBe true
a1_2.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[0] as PtAssignTarget).identifier!!.name shouldBe("cx16.r0L")
(a1_1.children[1] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_flag") (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[0] as PtAssignTarget).identifier!!.name shouldBe("cx16.r1")
(a1_2.children[1] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_flag") (a1_2.children[1] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_flag")
(a1_2.children[2] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_bytevar")
errors.clear() (a1_3.children[0] as PtAssignTarget).identifier!!.name shouldBe("cx16.r1")
val result2=compileText(VMTarget(), false, src, errors, true)!! (a1_3.children[1] as PtAssignTarget).identifier!!.name shouldBe("p8b_main.p8s_start.p8v_bytevar")
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")
} }
}) })

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 { ... } 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 **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.
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. So in the case of multisub() above, you could also write `wordvar, bytevar = multisub()` and leave out the carry flag.
It will then store the result value in a variable if required, and *try to keep the status register untouched The compiler will try to assign the normal numeric values and leave the status flags untouched, which then allows you to
after the call* so you can often use a conditional branch statement for that. But the latter is tricky, use a conditional branch such as `if_cs` to do something with it. This is always more efficient
make sure you check the generated assembly code. 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,
.. note:: 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.
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.
Subroutine definitions Subroutine definitions

View File

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