6502 codegen for multi-assigns

This commit is contained in:
Irmen de Jong 2025-01-09 00:37:39 +01:00
parent 7268a8736f
commit a6f9ed07e7
8 changed files with 292 additions and 206 deletions

View File

@ -1067,7 +1067,11 @@ $repeatLabel""")
}
}
else if(ret.children.size>1) {
TODO("multi-value return ; choose call convention: everything on stack?")
// 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()
ret.children.zip(registersReverseOrder).forEach { (value, register) ->
assignExpressionToRegister(value as PtExpression, register)
}
}
out(" rts")
}

View File

@ -243,9 +243,7 @@ internal sealed class AsmAssignmentBase(val source: AsmAssignSource,
val position: Position) {
init {
targets.forEach { target ->
if (target.register !in arrayOf(RegisterOrPair.XY, RegisterOrPair.AX, RegisterOrPair.AY))
require(!source.datatype.isUndefined) { "must not be placeholder/undefined datatype at $position" }
if (!source.datatype.isArray && !target.datatype.isArray)
if (!source.datatype.isArray && !source.datatype.isUndefined && !target.datatype.isArray && !target.datatype.isUndefined)
require(memsizer.memorySize(source.datatype, null) <= memsizer.memorySize(target.datatype, null)) {
"source dt size must be less or equal to target dt size at $position srcdt=${source.datatype} targetdt=${target.datatype}"
}

View File

@ -459,194 +459,9 @@ internal class AssignmentAsmGen(
is PtArrayIndexer -> throw AssemblyError("source kind should have been array")
is PtMemoryByte -> throw AssemblyError("source kind should have been memory")
is PtTypeCast -> assignTypeCastedValue(assign.target, value.type, value.value, value)
is PtFunctionCall -> {
val symbol = asmgen.symbolTable.lookup(value.name)
val sub = symbol!!.astNode as IPtSubroutine
asmgen.translateFunctionCall(value)
if(sub is PtSub && sub.returns.size>1) {
TODO("multi-value returns ; handle functioncall result")
} else {
val returnValue = sub.returnsWhatWhere().singleOrNull { it.first.registerOrPair!=null } ?: sub.returnsWhatWhere().single { it.first.statusflag!=null }
when {
returnValue.second.isString -> {
val targetDt = assign.target.datatype
when {
targetDt.isUnsignedWord -> {
// assign the address of the string result value
assignRegisterpairWord(assign.target, RegisterOrPair.AY)
}
targetDt.isString || targetDt.isUnsignedByteArray || targetDt.isByteArray -> {
throw AssemblyError("stringvalue assignment should have been replaced by a call to strcpy")
}
else -> throw AssemblyError("weird target dt")
}
}
returnValue.second.isFloat -> {
// float result from function sits in FAC1
assignFAC1float(assign.target)
}
else -> {
// do NOT restore X register before assigning the result values first
when (returnValue.first.registerOrPair) {
RegisterOrPair.A -> assignRegisterByte(assign.target, CpuRegister.A, returnValue.second.isSigned, true)
RegisterOrPair.X -> assignRegisterByte(assign.target, CpuRegister.X, returnValue.second.isSigned, true)
RegisterOrPair.Y -> assignRegisterByte(assign.target, CpuRegister.Y, returnValue.second.isSigned, true)
RegisterOrPair.AX -> assignVirtualRegister(assign.target, RegisterOrPair.AX)
RegisterOrPair.AY -> assignVirtualRegister(assign.target, RegisterOrPair.AY)
RegisterOrPair.XY -> assignVirtualRegister(assign.target, RegisterOrPair.XY)
RegisterOrPair.R0 -> assignVirtualRegister(assign.target, RegisterOrPair.R0)
RegisterOrPair.R1 -> assignVirtualRegister(assign.target, RegisterOrPair.R1)
RegisterOrPair.R2 -> assignVirtualRegister(assign.target, RegisterOrPair.R2)
RegisterOrPair.R3 -> assignVirtualRegister(assign.target, RegisterOrPair.R3)
RegisterOrPair.R4 -> assignVirtualRegister(assign.target, RegisterOrPair.R4)
RegisterOrPair.R5 -> assignVirtualRegister(assign.target, RegisterOrPair.R5)
RegisterOrPair.R6 -> assignVirtualRegister(assign.target, RegisterOrPair.R6)
RegisterOrPair.R7 -> assignVirtualRegister(assign.target, RegisterOrPair.R7)
RegisterOrPair.R8 -> assignVirtualRegister(assign.target, RegisterOrPair.R8)
RegisterOrPair.R9 -> assignVirtualRegister(assign.target, RegisterOrPair.R9)
RegisterOrPair.R10 -> assignVirtualRegister(assign.target, RegisterOrPair.R10)
RegisterOrPair.R11 -> assignVirtualRegister(assign.target, RegisterOrPair.R11)
RegisterOrPair.R12 -> assignVirtualRegister(assign.target, RegisterOrPair.R12)
RegisterOrPair.R13 -> assignVirtualRegister(assign.target, RegisterOrPair.R13)
RegisterOrPair.R14 -> assignVirtualRegister(assign.target, RegisterOrPair.R14)
RegisterOrPair.R15 -> assignVirtualRegister(assign.target, RegisterOrPair.R15)
else -> {
val sflag = returnValue.first.statusflag
if(sflag!=null)
assignStatusFlagByte(assign.target, sflag)
else
throw AssemblyError("should be just one register byte result value")
}
}
}
}
}
}
is PtBuiltinFunctionCall -> {
val returnDt = asmgen.translateBuiltinFunctionCallExpression(value, assign.target.register)
if(assign.target.register==null) {
// still need to assign the result to the target variable/etc.
when {
returnDt?.isByteOrBool==true -> assignRegisterByte(assign.target, CpuRegister.A, returnDt.isSigned, false) // function's byte result is in A
returnDt?.isWord==true -> assignRegisterpairWord(assign.target, RegisterOrPair.AY) // function's word result is in AY
returnDt==BaseDataType.STR -> {
val targetDt = assign.target.datatype
when {
targetDt.isString -> {
asmgen.out("""
tax
lda #<${assign.target.asmVarname}
sta P8ZP_SCRATCH_W1
lda #>${assign.target.asmVarname}
sta P8ZP_SCRATCH_W1+1
txa
jsr prog8_lib.strcpy""")
}
targetDt.isUnsignedWord -> assignRegisterpairWord(assign.target, RegisterOrPair.AY)
else -> throw AssemblyError("str return value type mismatch with target")
}
}
returnDt==BaseDataType.FLOAT -> {
// float result from function sits in FAC1
assignFAC1float(assign.target)
}
else -> throw AssemblyError("weird result type")
}
}
}
is PtPrefix -> {
if(assign.target.array==null) {
if(assign.source.datatype isAssignableTo assign.target.datatype || (assign.source.datatype.isBool && assign.target.datatype.isByte)) {
if(assign.source.datatype.isIntegerOrBool) {
val signed = assign.source.datatype.isSigned
if(assign.source.datatype.isByteOrBool) {
assignExpressionToRegister(value.value, RegisterOrPair.A, signed)
when(value.operator) {
"+" -> {}
"-" -> {
if(asmgen.isTargetCpu(CpuType.CPU65c02))
asmgen.out(" eor #255 | ina")
else
asmgen.out(" eor #255 | clc | adc #1")
}
"~" -> asmgen.out(" eor #255")
"not" -> asmgen.out(" eor #1")
else -> throw AssemblyError("invalid prefix operator")
}
assignRegisterByte(assign.target, CpuRegister.A, signed, false)
} else {
assignExpressionToRegister(value.value, RegisterOrPair.AY, signed)
when(value.operator) {
"+" -> {}
"-" -> {
asmgen.out("""
sec
eor #255
adc #0
tax
tya
eor #255
adc #0
tay
txa""")
}
"~" -> asmgen.out(" tax | tya | eor #255 | tay | txa | eor #255")
"not" -> throw AssemblyError("not shouldn't exist for an integer")
else -> throw AssemblyError("invalid prefix operator")
}
assignRegisterpairWord(assign.target, RegisterOrPair.AY)
}
} else {
// First assign the value to the target then apply the operator in place on the target.
// This saves a temporary variable
translateNormalAssignment(
AsmAssignment(
AsmAssignSource.fromAstSource(value.value, program, asmgen),
assign.targets, program.memsizer, assign.position
), scope
)
when (value.operator) {
"+" -> {}
"-" -> inplaceNegate(assign, true, scope)
"~" -> inplaceInvert(assign, scope)
"not" -> inplaceInvert(assign, scope)
else -> throw AssemblyError("invalid prefix operator")
}
}
} else {
// use a temporary variable
val tempvar = if(value.type.isByteOrBool) "P8ZP_SCRATCH_B1" else "P8ZP_SCRATCH_W1"
assignExpressionToVariable(value.value, tempvar, value.type)
when (value.operator) {
"+" -> {}
"-", "~" -> {
val assignTempvar = AsmAssignment(
AsmAssignSource(SourceStorageKind.VARIABLE, program, asmgen, value.type, variableAsmName = tempvar),
listOf(AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, value.type, scope, assign.position, variableAsmName = tempvar)),
program.memsizer, assign.position)
if(value.operator=="-")
inplaceNegate(assignTempvar, true, scope)
else
inplaceInvert(assignTempvar, scope)
}
"not" -> {
val assignTempvar = AsmAssignment(
AsmAssignSource(SourceStorageKind.VARIABLE, program, asmgen, value.type, variableAsmName = tempvar),
listOf(AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, value.type, scope, assign.position, variableAsmName = tempvar)),
program.memsizer, assign.position)
inplaceInvert(assignTempvar, scope)
}
else -> throw AssemblyError("invalid prefix operator")
}
if(value.type.isByteOrBool)
assignVariableByte(assign.target, tempvar)
else
assignVariableWord(assign.target, tempvar, value.type)
}
} else {
assignPrefixedExpressionToArrayElt(assign, scope)
}
}
is PtFunctionCall -> assignFunctionCall(assign, value)
is PtBuiltinFunctionCall -> assignBuiltinFunctionCall(assign.target, value)
is PtPrefix -> assignPrefixExpr(assign, value, scope)
is PtContainmentCheck -> {
containmentCheckIntoA(value)
assignRegisterByte(assign.target, CpuRegister.A, false, true)
@ -663,6 +478,203 @@ internal class AssignmentAsmGen(
}
}
private fun assignPrefixExpr(assign: AsmAssignment, value: PtPrefix, scope: IPtSubroutine?) {
if(assign.target.array==null) {
if(assign.source.datatype isAssignableTo assign.target.datatype || (assign.source.datatype.isBool && assign.target.datatype.isByte)) {
if(assign.source.datatype.isIntegerOrBool) {
val signed = assign.source.datatype.isSigned
if(assign.source.datatype.isByteOrBool) {
assignExpressionToRegister(value.value, RegisterOrPair.A, signed)
when(value.operator) {
"+" -> {}
"-" -> {
if(asmgen.isTargetCpu(CpuType.CPU65c02))
asmgen.out(" eor #255 | ina")
else
asmgen.out(" eor #255 | clc | adc #1")
}
"~" -> asmgen.out(" eor #255")
"not" -> asmgen.out(" eor #1")
else -> throw AssemblyError("invalid prefix operator")
}
assignRegisterByte(assign.target, CpuRegister.A, signed, false)
} else {
assignExpressionToRegister(value.value, RegisterOrPair.AY, signed)
when(value.operator) {
"+" -> {}
"-" -> {
asmgen.out("""
sec
eor #255
adc #0
tax
tya
eor #255
adc #0
tay
txa""")
}
"~" -> asmgen.out(" tax | tya | eor #255 | tay | txa | eor #255")
"not" -> throw AssemblyError("not shouldn't exist for an integer")
else -> throw AssemblyError("invalid prefix operator")
}
assignRegisterpairWord(assign.target, RegisterOrPair.AY)
}
} else {
// First assign the value to the target then apply the operator in place on the target.
// This saves a temporary variable
translateNormalAssignment(
AsmAssignment(
AsmAssignSource.fromAstSource(value.value, program, asmgen),
assign.targets, program.memsizer, assign.position
), scope
)
when (value.operator) {
"+" -> {}
"-" -> inplaceNegate(assign, true, scope)
"~" -> inplaceInvert(assign, scope)
"not" -> inplaceInvert(assign, scope)
else -> throw AssemblyError("invalid prefix operator")
}
}
} else {
// use a temporary variable
val tempvar = if(value.type.isByteOrBool) "P8ZP_SCRATCH_B1" else "P8ZP_SCRATCH_W1"
assignExpressionToVariable(value.value, tempvar, value.type)
when (value.operator) {
"+" -> {}
"-", "~" -> {
val assignTempvar = AsmAssignment(
AsmAssignSource(SourceStorageKind.VARIABLE, program, asmgen, value.type, variableAsmName = tempvar),
listOf(AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, value.type, scope, assign.position, variableAsmName = tempvar)),
program.memsizer, assign.position)
if(value.operator=="-")
inplaceNegate(assignTempvar, true, scope)
else
inplaceInvert(assignTempvar, scope)
}
"not" -> {
val assignTempvar = AsmAssignment(
AsmAssignSource(SourceStorageKind.VARIABLE, program, asmgen, value.type, variableAsmName = tempvar),
listOf(AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, value.type, scope, assign.position, variableAsmName = tempvar)),
program.memsizer, assign.position)
inplaceInvert(assignTempvar, scope)
}
else -> throw AssemblyError("invalid prefix operator")
}
if(value.type.isByteOrBool)
assignVariableByte(assign.target, tempvar)
else
assignVariableWord(assign.target, tempvar, value.type)
}
} else {
assignPrefixedExpressionToArrayElt(assign, scope)
}
}
private fun assignBuiltinFunctionCall(target: AsmAssignTarget, value: PtBuiltinFunctionCall) {
val returnDt = asmgen.translateBuiltinFunctionCallExpression(value, target.register)
if(target.register==null) {
// still need to assign the result to the target variable/etc.
when {
returnDt?.isByteOrBool==true -> assignRegisterByte(target, CpuRegister.A, returnDt.isSigned, false) // function's byte result is in A
returnDt?.isWord==true -> assignRegisterpairWord(target, RegisterOrPair.AY) // function's word result is in AY
returnDt==BaseDataType.STR -> {
val targetDt = target.datatype
when {
targetDt.isString -> {
asmgen.out("""
tax
lda #<${target.asmVarname}
sta P8ZP_SCRATCH_W1
lda #>${target.asmVarname}
sta P8ZP_SCRATCH_W1+1
txa
jsr prog8_lib.strcpy""")
}
targetDt.isUnsignedWord -> assignRegisterpairWord(target, RegisterOrPair.AY)
else -> throw AssemblyError("str return value type mismatch with target")
}
}
returnDt==BaseDataType.FLOAT -> {
// float result from function sits in FAC1
assignFAC1float(target)
}
else -> throw AssemblyError("weird result type")
}
}
}
private fun assignFunctionCall(assign: AsmAssignment, value: PtFunctionCall) {
val symbol = asmgen.symbolTable.lookup(value.name)
val sub = symbol!!.astNode as IPtSubroutine
asmgen.translateFunctionCall(value)
if(sub is PtSub && sub.returns.size>1) {
// 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()
assign.targets.zip(registersReverseOrder).forEach { (target, register) ->
if(target.kind!=TargetStorageKind.VOID)
assignVirtualRegister(target, register)
}
} else {
val target = assign.target
val returnValue = sub.returnsWhatWhere().singleOrNull { it.first.registerOrPair!=null } ?: sub.returnsWhatWhere().single { it.first.statusflag!=null }
when {
returnValue.second.isString -> {
val targetDt = target.datatype
when {
targetDt.isUnsignedWord -> {
// assign the address of the string result value
assignRegisterpairWord(target, RegisterOrPair.AY)
}
targetDt.isString || targetDt.isUnsignedByteArray || targetDt.isByteArray -> {
throw AssemblyError("stringvalue assignment should have been replaced by a call to strcpy")
}
else -> throw AssemblyError("weird target dt")
}
}
returnValue.second.isFloat -> {
// float result from function sits in FAC1
assignFAC1float(target)
}
else -> {
// do NOT restore X register before assigning the result values first
when (returnValue.first.registerOrPair) {
RegisterOrPair.A -> assignRegisterByte(target, CpuRegister.A, returnValue.second.isSigned, true)
RegisterOrPair.X -> assignRegisterByte(target, CpuRegister.X, returnValue.second.isSigned, true)
RegisterOrPair.Y -> assignRegisterByte(target, CpuRegister.Y, returnValue.second.isSigned, true)
RegisterOrPair.AX -> assignVirtualRegister(target, RegisterOrPair.AX)
RegisterOrPair.AY -> assignVirtualRegister(target, RegisterOrPair.AY)
RegisterOrPair.XY -> assignVirtualRegister(target, RegisterOrPair.XY)
RegisterOrPair.R0 -> assignVirtualRegister(target, RegisterOrPair.R0)
RegisterOrPair.R1 -> assignVirtualRegister(target, RegisterOrPair.R1)
RegisterOrPair.R2 -> assignVirtualRegister(target, RegisterOrPair.R2)
RegisterOrPair.R3 -> assignVirtualRegister(target, RegisterOrPair.R3)
RegisterOrPair.R4 -> assignVirtualRegister(target, RegisterOrPair.R4)
RegisterOrPair.R5 -> assignVirtualRegister(target, RegisterOrPair.R5)
RegisterOrPair.R6 -> assignVirtualRegister(target, RegisterOrPair.R6)
RegisterOrPair.R7 -> assignVirtualRegister(target, RegisterOrPair.R7)
RegisterOrPair.R8 -> assignVirtualRegister(target, RegisterOrPair.R8)
RegisterOrPair.R9 -> assignVirtualRegister(target, RegisterOrPair.R9)
RegisterOrPair.R10 -> assignVirtualRegister(target, RegisterOrPair.R10)
RegisterOrPair.R11 -> assignVirtualRegister(target, RegisterOrPair.R11)
RegisterOrPair.R12 -> assignVirtualRegister(target, RegisterOrPair.R12)
RegisterOrPair.R13 -> assignVirtualRegister(target, RegisterOrPair.R13)
RegisterOrPair.R14 -> assignVirtualRegister(target, RegisterOrPair.R14)
RegisterOrPair.R15 -> assignVirtualRegister(target, RegisterOrPair.R15)
else -> {
val sflag = returnValue.first.statusflag
if(sflag!=null)
assignStatusFlagByte(target, sflag)
else
throw AssemblyError("should be just one register byte result value")
}
}
}
}
}
}
private fun assignPrefixedExpressionToArrayElt(assign: AsmAssignment, scope: IPtSubroutine?) {
require(assign.source.expression is PtPrefix)
if(assign.source.datatype.isFloat) {

View File

@ -136,7 +136,7 @@ internal class AstChecker(private val program: Program,
} else if(valueDt issimpletype BaseDataType.UWORD && expectedDt.isString) {
// you can return an uword pointer when the return type is a string
} else {
errors.err("type $valueDt of return value doesn't match subroutine's return type ${expectedDt}", actual.position)
errors.err("return value's type $valueDt doesn't match subroutine's return type ${expectedDt}", actual.position)
}
}
}
@ -1558,6 +1558,18 @@ internal class AstChecker(private val program: Program,
}
}
}
if(target.returntypes.size>1) {
if (DataType.forDt(BaseDataType.FLOAT) in target.returntypes) {
errors.err("floats cannot be used as part of a multi-value result", target.position)
}
}
if(target.returntypes.size>16) {
errors.err("cannot have more than 16 return values", target.position)
}
if(target.returntypes.size>3) {
errors.info("a large number of return values incurs a substantial value copying overhead", target.position)
}
}
args.forEach{

View File

@ -2,6 +2,7 @@ package prog8tests.compiler
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.types.instanceOf
@ -289,4 +290,25 @@ class TestSubroutines: FunSpec({
stmts.dropLast(1).last() shouldBe instanceOf<Return>() // this prevents the fallthrough
stmts.dropLast(2).last() shouldBe instanceOf<IFunctionCall>()
}
test("multi-value returns from regular (non-asmsub) subroutines") {
val src= """
main {
sub start() {
uword a
ubyte b
bool c
a, b, c = multi()
a, void, c = multi()
void, b, c = multi()
void multi()
}
sub multi() -> uword, ubyte, bool {
return 12345, 66, true
}
}"""
compileText(C64Target(), false, src, writeAssembly = true).shouldNotBeNull()
// compileText(VMTarget(), false, src, writeAssembly = true).shouldNotBeNull() TODO("multi-value return ; unittest")
}
})

View File

@ -885,8 +885,8 @@ main {
val errors = ErrorReporterForTests()
compileText(C64Target(), false, src, writeAssembly = false, errors = errors) shouldBe null
errors.errors.size shouldBe 2
errors.errors[0] shouldContain "17:16: type ubyte of return value doesn't match subroutine's return type byte"
errors.errors[1] shouldContain "20:16: type uword of return value doesn't match subroutine's return type word"
errors.errors[0] shouldContain "17:16: return value's type ubyte doesn't match subroutine's return type byte"
errors.errors[1] shouldContain "20:16: return value's type uword doesn't match subroutine's return type word"
}
test("if-expression adjusts different value types to common type") {

View File

@ -1,7 +1,8 @@
TODO
====
- implement the TODO("multi-value occurences in both codegens, to handle multi-value subroutine return values
- 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)
- rename "intermediate AST" into "simplified AST" (docs + classes in code)

View File

@ -1,20 +1,57 @@
%import textio
%option no_sysinit
%zeropage basicsafe
main {
sub start() {
cx16.r0 = single()
uword a
ubyte b
bool c
a=9999
b=255
c=false
a, b, c = multi()
txt.print_uw(a)
txt.spc()
txt.print_uw(b)
txt.spc()
txt.print_bool(c)
txt.nl()
a=9999
b=255
c=false
a, void, c = multi()
txt.print_uw(a)
txt.spc()
txt.print_uw(b)
txt.spc()
txt.print_bool(c)
txt.nl()
a=9999
b=255
c=false
void, b, c = multi()
txt.print_uw(a)
txt.spc()
txt.print_uw(b)
txt.spc()
txt.print_bool(c)
txt.nl()
a=9999
b=255
c=false
void multi()
cx16.r0,void = multi()
cx16.r0,cx16.r1 = multi()
txt.print_uw(a)
txt.spc()
txt.print_uw(b)
txt.spc()
txt.print_bool(c)
txt.nl()
}
sub single() -> uword {
return 42+cx16.r0L
}
sub multi() -> uword, uword {
defer cx16.r0++
return 42+cx16.r0L, 99
sub multi() -> uword, ubyte, bool {
return 12345, 66, true
}
}