call convention for @Rx parameters, also use cpu registers if possible, like normal parameters

This commit is contained in:
Irmen de Jong 2024-11-25 20:34:10 +01:00
parent d58f9f56c4
commit 2eed75f602
12 changed files with 115 additions and 93 deletions

View File

@ -15,7 +15,7 @@ data class Position(val file: String, val line: Int, val startCol: Int, val endC
return try { return try {
val path = Path(file).absolute().normalize().toString() val path = Path(file).absolute().normalize().toString()
"file://$path:$line:$startCol:" "file://$path:$line:$startCol:"
} catch(x: InvalidPathException) { } catch(_: InvalidPathException) {
// this can occur on Windows when the source origin contains "invalid" characters such as ':' // this can occur on Windows when the source origin contains "invalid" characters such as ':'
"file://$file:$line:$startCol:" "file://$file:$line:$startCol:"
} }

View File

@ -55,7 +55,7 @@ class C64MachineDefinition: IMachineDefinition {
val process: Process val process: Process
try { try {
process=processb.start() process=processb.start()
} catch(x: IOException) { } catch(_: IOException) {
continue // try the next emulator executable continue // try the next emulator executable
} }
process.waitFor() process.waitFor()

View File

@ -15,16 +15,7 @@ internal class FunctionCallAsmGen(private val program: PtProgram, private val as
// just ignore any result values from the function call. // just ignore any result values from the function call.
} }
@JvmName("optimizeIntArgsViaRegisters") internal fun optimizeIntArgsViaCpuRegisters(params: List<PtSubroutineParameter>) =
internal fun optimizeIntArgsViaRegisters(params: List<IndexedValue<PtSubroutineParameter>>) =
when(params.size) {
1 -> params[0].value.type in IntegerDatatypesWithBoolean
2 -> params[0].value.type in ByteDatatypesWithBoolean && params[1].value.type in ByteDatatypesWithBoolean
else -> false
}
@JvmName("optimizeIntArgsViaRegistersNotIndexed")
internal fun optimizeIntArgsViaRegisters(params: List<PtSubroutineParameter>) =
when(params.size) { when(params.size) {
1 -> params[0].type in IntegerDatatypesWithBoolean 1 -> params[0].type in IntegerDatatypesWithBoolean
2 -> params[0].type in ByteDatatypesWithBoolean && params[1].type in ByteDatatypesWithBoolean 2 -> params[0].type in ByteDatatypesWithBoolean && params[1].type in ByteDatatypesWithBoolean
@ -133,39 +124,23 @@ internal class FunctionCallAsmGen(private val program: PtProgram, private val as
} }
} }
else if(sub is PtSub) { else if(sub is PtSub) {
val (paramsViaRegisters, normalParams) = sub.parameters.withIndex().partition { it.value.register!=null } if(optimizeIntArgsViaCpuRegisters(sub.parameters)) {
if(normalParams.isNotEmpty()) { // Note that if the args fit into cpu registers, we don't concern ourselves here
if(optimizeIntArgsViaRegisters(normalParams)) { // if they should be put into regular subroutine parameter variables, or the R0-R15 register variables.
when(normalParams.size) { // That is now up to the subroutine itself.
1 -> { useCpuRegistersForArgs(call.args, sub)
val register = if (normalParams[0].value.type in ByteDatatypesWithBoolean) RegisterOrPair.A else RegisterOrPair.AY } else {
argumentViaRegister(sub, IndexedValue(0, normalParams[0].value), call.args[0], register) // arguments via variables
} val (normalParams, registerParams) = sub.parameters.withIndex().partition { it.value.register == null }
2 -> { if (normalParams.isNotEmpty()) {
if(normalParams[0].value.type in ByteDatatypesWithBoolean && normalParams[1].value.type in ByteDatatypesWithBoolean) { for (arg in normalParams.zip(call.args))
// 2 byte params, second in Y, first in A argumentViaVariable(sub, arg.first.value, arg.second)
argumentViaRegister(sub, IndexedValue(0, normalParams[0].value), call.args[0], RegisterOrPair.A) }
if(asmgen.needAsaveForExpr(call.args[1])) if (registerParams.isNotEmpty()) {
asmgen.out(" pha") // the R0-R15 'registers' are not really registers. They're just special variables.
argumentViaRegister(sub, IndexedValue(1, normalParams[1].value), call.args[1], RegisterOrPair.Y) for (arg in registerParams.zip(call.args))
if(asmgen.needAsaveForExpr(call.args[1]))
asmgen.out(" pla")
} else {
throw AssemblyError("cannot use registers for word+byte")
}
}
else -> throw AssemblyError("cannot use registers for >2 arguments")
}
} else {
// arguments via variables
for(arg in normalParams.zip(call.args))
argumentViaVariable(sub, arg.first.value, arg.second) argumentViaVariable(sub, arg.first.value, arg.second)
} }
}
if(paramsViaRegisters.isNotEmpty()) {
// the R0-R15 'registers' are not really registers. They're just special variables.
for(arg in paramsViaRegisters.zip(call.args))
argumentViaVariable(sub, arg.first.value, arg.second)
} }
asmgen.out(" jsr $subAsmName") asmgen.out(" jsr $subAsmName")
} }
@ -174,6 +149,30 @@ internal class FunctionCallAsmGen(private val program: PtProgram, private val as
// remember: dealing with the X register and/or dealing with return values is the responsibility of the caller // remember: dealing with the X register and/or dealing with return values is the responsibility of the caller
} }
private fun useCpuRegistersForArgs(args: List<PtExpression>, sub: PtSub) {
val params = sub.parameters
when(params.size) {
1 -> {
val register = if (params[0].type in ByteDatatypesWithBoolean) RegisterOrPair.A else RegisterOrPair.AY
argumentViaRegister(sub, IndexedValue(0, params[0]), args[0], register)
}
2 -> {
if(params[0].type in ByteDatatypesWithBoolean && params[1].type in ByteDatatypesWithBoolean) {
// 2 byte params, second in Y, first in A
argumentViaRegister(sub, IndexedValue(0, params[0]), args[0], RegisterOrPair.A)
if(asmgen.needAsaveForExpr(args[1]))
asmgen.out(" pha")
argumentViaRegister(sub, IndexedValue(1, params[1]), args[1], RegisterOrPair.Y)
if(asmgen.needAsaveForExpr(args[1]))
asmgen.out(" pla")
} else {
throw AssemblyError("cannot use registers for word+byte")
}
}
else -> throw AssemblyError("cannot use cpu registers for >2 arguments")
}
}
private fun usesOtherRegistersWhileEvaluating(arg: PtExpression): Boolean { private fun usesOtherRegistersWhileEvaluating(arg: PtExpression): Boolean {
return when(arg) { return when(arg) {

View File

@ -421,28 +421,35 @@ internal class ProgramAndVarsGen(
if((sub.name=="start" || sub.name=="p8s_start") && (sub.definingBlock()!!.name=="main" || sub.definingBlock()!!.name=="p8b_main")) if((sub.name=="start" || sub.name=="p8s_start") && (sub.definingBlock()!!.name=="main" || sub.definingBlock()!!.name=="p8b_main"))
entrypointInitialization() entrypointInitialization()
val normalParams = sub.parameters.filter { it.register==null } val params = sub.parameters
if(functioncallAsmGen.optimizeIntArgsViaRegisters(normalParams)) { if(functioncallAsmGen.optimizeIntArgsViaCpuRegisters(params)) {
asmgen.out("; simple int arg(s) passed via register(s)") asmgen.out("; simple int arg(s) passed via cpu register(s)")
when(normalParams.size) {
fun varname(param: PtSubroutineParameter): String =
if(param.register==null)
param.name
else
param.register!!.asScopedNameVirtualReg(param.type).joinToString(".")
when(params.size) {
1 -> { 1 -> {
val dt = normalParams[0].type val dt = params[0].type
val target = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, dt, sub, normalParams[0].position, variableAsmName = normalParams[0].name) val target = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, dt, sub, params[0].position, variableAsmName = varname(params[0]))
if(dt in ByteDatatypesWithBoolean) if(dt in ByteDatatypesWithBoolean)
asmgen.assignRegister(RegisterOrPair.A, target) asmgen.assignRegister(RegisterOrPair.A, target) // single byte in A
else else
asmgen.assignRegister(RegisterOrPair.AY, target) asmgen.assignRegister(RegisterOrPair.AY, target) // word in AY
} }
2 -> { 2 -> {
val target1 = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, normalParams[0].type, sub, normalParams[0].position, variableAsmName = normalParams[0].name) val target1 = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, params[0].type, sub, params[0].position, variableAsmName = varname(params[0]))
val target2 = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, normalParams[1].type, sub, normalParams[1].position, variableAsmName = normalParams[1].name) val target2 = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, params[1].type, sub, params[1].position, variableAsmName = varname(params[1]))
if(normalParams[0].type in ByteDatatypesWithBoolean && normalParams[1].type in ByteDatatypesWithBoolean) { if(params[0].type in ByteDatatypesWithBoolean && params[1].type in ByteDatatypesWithBoolean) {
// 2 byte args, first in A, second in Y // 2 byte args, first in A, second in Y
asmgen.assignRegister(RegisterOrPair.A, target1) asmgen.assignRegister(RegisterOrPair.A, target1)
asmgen.assignRegister(RegisterOrPair.Y, target2) asmgen.assignRegister(RegisterOrPair.Y, target2)
} else throw AssemblyError("cannot use registers for word+byte") } else throw AssemblyError("cannot use registers for word+byte args")
} }
else -> throw AssemblyError("cannot use registers for >2 arguments") else -> throw AssemblyError("cannot use registers for >2 args")
} }
} }

View File

@ -286,10 +286,10 @@ private class BuiltinFunctionsFacade(functions: Map<String, FSignature>): IBuilt
if(exprfunc!=null) { if(exprfunc!=null) {
return try { return try {
exprfunc(args, position, program) exprfunc(args, position, program)
} catch(x: NotConstArgumentException) { } catch(_: NotConstArgumentException) {
// const-evaluating the builtin function call failed. // const-evaluating the builtin function call failed.
null null
} catch(x: CannotEvaluateException) { } catch(_: CannotEvaluateException) {
// const-evaluating the builtin function call failed. // const-evaluating the builtin function call failed.
null null
} }
@ -354,7 +354,7 @@ fun determineCompilationOptions(program: Program, compTarget: ICompilationTarget
else else
try { try {
ZeropageType.valueOf(zpoption) ZeropageType.valueOf(zpoption)
} catch (x: IllegalArgumentException) { } catch (_: IllegalArgumentException) {
ZeropageType.KERNALSAFE ZeropageType.KERNALSAFE
// error will be printed by the astchecker // error will be printed by the astchecker
} }
@ -383,7 +383,7 @@ fun determineCompilationOptions(program: Program, compTarget: ICompilationTarget
} else { } else {
try { try {
OutputType.valueOf(outputTypeStr) OutputType.valueOf(outputTypeStr)
} catch (x: IllegalArgumentException) { } catch (_: IllegalArgumentException) {
// set default value; actual check and error handling of invalid option is handled in the AstChecker later // set default value; actual check and error handling of invalid option is handled in the AstChecker later
OutputType.PRG OutputType.PRG
} }
@ -396,7 +396,7 @@ fun determineCompilationOptions(program: Program, compTarget: ICompilationTarget
} else { } else {
try { try {
CbmPrgLauncherType.valueOf(launcherTypeStr) CbmPrgLauncherType.valueOf(launcherTypeStr)
} catch (x: IllegalArgumentException) { } catch (_: IllegalArgumentException) {
// set default value; actual check and error handling of invalid option is handled in the AstChecker later // set default value; actual check and error handling of invalid option is handled in the AstChecker later
CbmPrgLauncherType.BASIC CbmPrgLauncherType.BASIC
} }

View File

@ -516,7 +516,7 @@ internal class AstChecker(private val program: Program,
if (!subroutine.isAsmSubroutine && p.registerOrPair!=null) { if (!subroutine.isAsmSubroutine && p.registerOrPair!=null) {
if (p.registerOrPair !in Cx16VirtualRegisters) errors.err("can only use R0-R15 as register param for normal subroutines", p.position) if (p.registerOrPair !in Cx16VirtualRegisters) errors.err("can only use R0-R15 as register param for normal subroutines", p.position)
else { else {
errors.warn("\uD83D\uDCA3 footgun: reusing R0-R15 as parameters risks overwriting due to no callstack or clobbering", subroutine.position) errors.warn("\uD83D\uDCA3 footgun: reusing R0-R15 as parameters risks overwriting due to clobbering or no callstack", subroutine.position)
if(p.type !in WordDatatypes && p.type !in ByteDatatypesWithBoolean) { if(p.type !in WordDatatypes && p.type !in ByteDatatypesWithBoolean) {
errors.err("can only use register param when type is boolean, byte or word", p.position) errors.err("can only use register param when type is boolean, byte or word", p.position)
} }

View File

@ -219,8 +219,8 @@ class AstPreprocessor(val program: Program,
val namesInSub = symbolsInSub.map{ it.first }.toSet() val namesInSub = symbolsInSub.map{ it.first }.toSet()
if(subroutine.asmAddress==null) { if(subroutine.asmAddress==null) {
if(!subroutine.isAsmSubroutine && subroutine.parameters.isNotEmpty()) { if(!subroutine.isAsmSubroutine && subroutine.parameters.isNotEmpty()) {
var mods = mutableListOf<IAstModification>() val mods = mutableListOf<IAstModification>()
var (normalParams, registerParams) = subroutine.parameters.partition { it.registerOrPair==null } val (normalParams, registerParams) = subroutine.parameters.partition { it.registerOrPair==null }
if(normalParams.isNotEmpty()) { if(normalParams.isNotEmpty()) {
val existingVars = subroutine.statements.asSequence().filterIsInstance<VarDecl>().map { it.name }.toSet() val existingVars = subroutine.statements.asSequence().filterIsInstance<VarDecl>().map { it.name }.toSet()
normalParams normalParams

View File

@ -1050,7 +1050,7 @@ main {
val src=""" val src="""
main { sub start() { cx16.r0++ cx16.r1++ } } main { sub start() { cx16.r0++ cx16.r1++ } }
other { asmsub thing() { %asm {{ inx }} } } other { asmsub thing() { %asm {{ inx }} } }
"""; """
val result = compileText(VMTarget(), false, src, writeAssembly = false)!! val result = compileText(VMTarget(), false, src, writeAssembly = false)!!
val st = result.compilerAst.entrypoint.statements val st = result.compilerAst.entrypoint.statements
st.size shouldBe 2 st.size shouldBe 2

View File

@ -58,7 +58,9 @@ Variables
Subroutines Subroutines
----------- -----------
- There is no call stack. Subroutine parameters are overwritten when called again (recursion is not easily possible, but you can do it with manual stack manipulations). - Subroutines can be nested. Inner subroutines can directly access variables from their parent.
- Subroutine parameters are just local variables in the subroutine. (you can access them directly as such via their scoped name, if you want)
- There is no call stack. So subroutine parameters are overwritten when called again. Thus recursion is not easily possible, but you can do it with manual stack manipulations.
- There is no function overloading (except for a couple of builtin functions). - 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. - Some subroutine types 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 - Because every declared variable allocates some memory, it might be beneficial to share the same variables over different subroutines

View File

@ -1,29 +1,43 @@
%import textio
%zeropage basicsafe
main { main {
sub start() { sub start() {
foo(42) foo(42)
bar(9999,55) foo(42)
; faulty1(false) foo(42)
faulty2(42) bar(9999)
faulty3(9999,55) bar(9999)
bar(9999)
baz(42, 123)
baz(42, 123)
baz(42, 123)
meh(42, 9999)
meh(42, 9999)
meh(42, 9999)
} }
sub foo(ubyte arg @R2) { sub foo(ubyte arg @R0) {
arg++ txt.print_ub(arg)
txt.nl()
} }
sub bar(uword arg @R0, ubyte arg2 @R1) { sub bar(uword arg @R0) {
arg += arg2 txt.print_uw(arg)
txt.nl()
} }
; sub faulty1(bool flag @Pc) { sub baz(ubyte arg1 @R0, ubyte arg2 @R1) {
; cx16.r0++ txt.print_ub(arg1)
; } txt.spc()
txt.print_ub(arg2)
sub faulty2(byte arg @Y) { txt.nl()
arg++
} }
sub faulty3(uword arg @R1, ubyte arg2 @R1) { sub meh(ubyte arg1 @R0, uword arg2 @R1) {
arg += arg2 txt.print_ub(arg1)
txt.spc()
txt.print_uw(arg2)
txt.nl()
} }
} }

View File

@ -72,7 +72,7 @@ fun parseIRCodeLine(line: String): Either<IRInstruction, String> {
val (instr, typestr, rest) = match.destructured val (instr, typestr, rest) = match.destructured
val opcode = try { val opcode = try {
Opcode.valueOf(instr.uppercase()) Opcode.valueOf(instr.uppercase())
} catch (ax: IllegalArgumentException) { } catch (_: IllegalArgumentException) {
throw IRParseException("invalid vmasm instruction: $instr") throw IRParseException("invalid vmasm instruction: $instr")
} }
var type: IRDataType? = convertIRType(typestr) var type: IRDataType? = convertIRType(typestr)
@ -181,11 +181,11 @@ fun parseIRCodeLine(line: String): Either<IRInstruction, String> {
throw IRParseException("needs value or symbol for $line") throw IRParseException("needs value or symbol for $line")
when (type) { when (type) {
IRDataType.BYTE -> { IRDataType.BYTE -> {
if (immediateInt!=null && (immediateInt!! < -128 || immediateInt!! > 255)) if (immediateInt!=null && (immediateInt < -128 || immediateInt > 255))
throw IRParseException("immediate value out of range for byte: $immediateInt") throw IRParseException("immediate value out of range for byte: $immediateInt")
} }
IRDataType.WORD -> { IRDataType.WORD -> {
if (immediateInt!=null && (immediateInt!! < -32768 || immediateInt!! > 65535)) if (immediateInt!=null && (immediateInt < -32768 || immediateInt > 65535))
throw IRParseException("immediate value out of range for word: $immediateInt") throw IRParseException("immediate value out of range for word: $immediateInt")
} }
IRDataType.FLOAT -> {} IRDataType.FLOAT -> {}
@ -198,13 +198,13 @@ fun parseIRCodeLine(line: String): Either<IRInstruction, String> {
var offset: Int? = null var offset: Int? = null
if(labelSymbol!=null) { if(labelSymbol!=null) {
if (labelSymbol!![0] == 'r' && labelSymbol!![1].isDigit()) if (labelSymbol[0] == 'r' && labelSymbol[1].isDigit())
throw IRParseException("labelsymbol confused with register?: $labelSymbol") throw IRParseException("labelsymbol confused with register?: $labelSymbol")
if('+' in labelSymbol!!) { if('+' in labelSymbol) {
val offsetStr = labelSymbol!!.substringAfterLast('+') val offsetStr = labelSymbol.substringAfterLast('+')
if (offsetStr.isNotEmpty()) { if (offsetStr.isNotEmpty()) {
offset = offsetStr.toInt() offset = offsetStr.toInt()
labelSymbol = labelSymbol!!.substringBeforeLast('+') labelSymbol = labelSymbol.substringBeforeLast('+')
} }
} }
} }
@ -320,10 +320,10 @@ internal fun parseRegisterOrStatusflag(sourceregs: String): RegisterOrStatusflag
try { try {
reg = RegisterOrPair.valueOf(regs) reg = RegisterOrPair.valueOf(regs)
} catch (x: IllegalArgumentException) { } catch (_: IllegalArgumentException) {
try { try {
sf = Statusflag.valueOf(regs) sf = Statusflag.valueOf(regs)
} catch(x: IllegalArgumentException) { } catch(_: IllegalArgumentException) {
throw IRParseException("invalid IR register or statusflag: $regs") throw IRParseException("invalid IR register or statusflag: $regs")
} }
} }

View File

@ -270,7 +270,7 @@ object SysCalls {
val trimmed = memstring.takeWhile { it in " +-0123456789.eE" } val trimmed = memstring.takeWhile { it in " +-0123456789.eE" }
try { try {
trimmed.toDouble() trimmed.toDouble()
} catch(x: NumberFormatException) { } catch(_: NumberFormatException) {
0.0 0.0
} }
} }
@ -567,7 +567,7 @@ object SysCalls {
val height = response.next().toInt() val height = response.next().toInt()
return returnValue(callspec.returns.single(), height*256 + width, vm) return returnValue(callspec.returns.single(), height*256 + width, vm)
} }
} catch (x: Exception) { } catch (_: Exception) {
// don't know what happened... // don't know what happened...
} }
return returnValue(callspec.returns.single(), 30*256 + 80, vm) // just return some defaults in this case 80*30 return returnValue(callspec.returns.single(), 30*256 + 80, vm) // just return some defaults in this case 80*30