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 {
val path = Path(file).absolute().normalize().toString()
"file://$path:$line:$startCol:"
} catch(x: InvalidPathException) {
} catch(_: InvalidPathException) {
// this can occur on Windows when the source origin contains "invalid" characters such as ':'
"file://$file:$line:$startCol:"
}

View File

@ -55,7 +55,7 @@ class C64MachineDefinition: IMachineDefinition {
val process: Process
try {
process=processb.start()
} catch(x: IOException) {
} catch(_: IOException) {
continue // try the next emulator executable
}
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.
}
@JvmName("optimizeIntArgsViaRegisters")
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>) =
internal fun optimizeIntArgsViaCpuRegisters(params: List<PtSubroutineParameter>) =
when(params.size) {
1 -> params[0].type in IntegerDatatypesWithBoolean
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) {
val (paramsViaRegisters, normalParams) = sub.parameters.withIndex().partition { it.value.register!=null }
if(normalParams.isNotEmpty()) {
if(optimizeIntArgsViaRegisters(normalParams)) {
when(normalParams.size) {
1 -> {
val register = if (normalParams[0].value.type in ByteDatatypesWithBoolean) RegisterOrPair.A else RegisterOrPair.AY
argumentViaRegister(sub, IndexedValue(0, normalParams[0].value), call.args[0], register)
}
2 -> {
if(normalParams[0].value.type in ByteDatatypesWithBoolean && normalParams[1].value.type in ByteDatatypesWithBoolean) {
// 2 byte params, second in Y, first in A
argumentViaRegister(sub, IndexedValue(0, normalParams[0].value), call.args[0], RegisterOrPair.A)
if(asmgen.needAsaveForExpr(call.args[1]))
asmgen.out(" pha")
argumentViaRegister(sub, IndexedValue(1, normalParams[1].value), call.args[1], RegisterOrPair.Y)
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))
if(optimizeIntArgsViaCpuRegisters(sub.parameters)) {
// Note that if the args fit into cpu registers, we don't concern ourselves here
// if they should be put into regular subroutine parameter variables, or the R0-R15 register variables.
// That is now up to the subroutine itself.
useCpuRegistersForArgs(call.args, sub)
} else {
// arguments via variables
val (normalParams, registerParams) = sub.parameters.withIndex().partition { it.value.register == null }
if (normalParams.isNotEmpty()) {
for (arg in normalParams.zip(call.args))
argumentViaVariable(sub, arg.first.value, arg.second)
}
if (registerParams.isNotEmpty()) {
// the R0-R15 'registers' are not really registers. They're just special variables.
for (arg in registerParams.zip(call.args))
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")
}
@ -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
}
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 {
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"))
entrypointInitialization()
val normalParams = sub.parameters.filter { it.register==null }
if(functioncallAsmGen.optimizeIntArgsViaRegisters(normalParams)) {
asmgen.out("; simple int arg(s) passed via register(s)")
when(normalParams.size) {
val params = sub.parameters
if(functioncallAsmGen.optimizeIntArgsViaCpuRegisters(params)) {
asmgen.out("; simple int arg(s) passed via cpu register(s)")
fun varname(param: PtSubroutineParameter): String =
if(param.register==null)
param.name
else
param.register!!.asScopedNameVirtualReg(param.type).joinToString(".")
when(params.size) {
1 -> {
val dt = normalParams[0].type
val target = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, dt, sub, normalParams[0].position, variableAsmName = normalParams[0].name)
val dt = params[0].type
val target = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, dt, sub, params[0].position, variableAsmName = varname(params[0]))
if(dt in ByteDatatypesWithBoolean)
asmgen.assignRegister(RegisterOrPair.A, target)
asmgen.assignRegister(RegisterOrPair.A, target) // single byte in A
else
asmgen.assignRegister(RegisterOrPair.AY, target)
asmgen.assignRegister(RegisterOrPair.AY, target) // word in AY
}
2 -> {
val target1 = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, normalParams[0].type, sub, normalParams[0].position, variableAsmName = normalParams[0].name)
val target2 = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, normalParams[1].type, sub, normalParams[1].position, variableAsmName = normalParams[1].name)
if(normalParams[0].type in ByteDatatypesWithBoolean && normalParams[1].type in ByteDatatypesWithBoolean) {
val target1 = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, params[0].type, sub, params[0].position, variableAsmName = varname(params[0]))
val target2 = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, params[1].type, sub, params[1].position, variableAsmName = varname(params[1]))
if(params[0].type in ByteDatatypesWithBoolean && params[1].type in ByteDatatypesWithBoolean) {
// 2 byte args, first in A, second in Y
asmgen.assignRegister(RegisterOrPair.A, target1)
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) {
return try {
exprfunc(args, position, program)
} catch(x: NotConstArgumentException) {
} catch(_: NotConstArgumentException) {
// const-evaluating the builtin function call failed.
null
} catch(x: CannotEvaluateException) {
} catch(_: CannotEvaluateException) {
// const-evaluating the builtin function call failed.
null
}
@ -354,7 +354,7 @@ fun determineCompilationOptions(program: Program, compTarget: ICompilationTarget
else
try {
ZeropageType.valueOf(zpoption)
} catch (x: IllegalArgumentException) {
} catch (_: IllegalArgumentException) {
ZeropageType.KERNALSAFE
// error will be printed by the astchecker
}
@ -383,7 +383,7 @@ fun determineCompilationOptions(program: Program, compTarget: ICompilationTarget
} else {
try {
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
OutputType.PRG
}
@ -396,7 +396,7 @@ fun determineCompilationOptions(program: Program, compTarget: ICompilationTarget
} else {
try {
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
CbmPrgLauncherType.BASIC
}

View File

@ -516,7 +516,7 @@ internal class AstChecker(private val program: Program,
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)
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) {
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()
if(subroutine.asmAddress==null) {
if(!subroutine.isAsmSubroutine && subroutine.parameters.isNotEmpty()) {
var mods = mutableListOf<IAstModification>()
var (normalParams, registerParams) = subroutine.parameters.partition { it.registerOrPair==null }
val mods = mutableListOf<IAstModification>()
val (normalParams, registerParams) = subroutine.parameters.partition { it.registerOrPair==null }
if(normalParams.isNotEmpty()) {
val existingVars = subroutine.statements.asSequence().filterIsInstance<VarDecl>().map { it.name }.toSet()
normalParams

View File

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

View File

@ -58,7 +58,9 @@ Variables
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).
- 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

View File

@ -1,29 +1,43 @@
%import textio
%zeropage basicsafe
main {
sub start() {
foo(42)
bar(9999,55)
; faulty1(false)
faulty2(42)
faulty3(9999,55)
foo(42)
foo(42)
bar(9999)
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) {
arg++
sub foo(ubyte arg @R0) {
txt.print_ub(arg)
txt.nl()
}
sub bar(uword arg @R0, ubyte arg2 @R1) {
arg += arg2
sub bar(uword arg @R0) {
txt.print_uw(arg)
txt.nl()
}
; sub faulty1(bool flag @Pc) {
; cx16.r0++
; }
sub faulty2(byte arg @Y) {
arg++
sub baz(ubyte arg1 @R0, ubyte arg2 @R1) {
txt.print_ub(arg1)
txt.spc()
txt.print_ub(arg2)
txt.nl()
}
sub faulty3(uword arg @R1, ubyte arg2 @R1) {
arg += arg2
sub meh(ubyte arg1 @R0, uword arg2 @R1) {
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 opcode = try {
Opcode.valueOf(instr.uppercase())
} catch (ax: IllegalArgumentException) {
} catch (_: IllegalArgumentException) {
throw IRParseException("invalid vmasm instruction: $instr")
}
var type: IRDataType? = convertIRType(typestr)
@ -181,11 +181,11 @@ fun parseIRCodeLine(line: String): Either<IRInstruction, String> {
throw IRParseException("needs value or symbol for $line")
when (type) {
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")
}
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")
}
IRDataType.FLOAT -> {}
@ -198,13 +198,13 @@ fun parseIRCodeLine(line: String): Either<IRInstruction, String> {
var offset: Int? = 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")
if('+' in labelSymbol!!) {
val offsetStr = labelSymbol!!.substringAfterLast('+')
if('+' in labelSymbol) {
val offsetStr = labelSymbol.substringAfterLast('+')
if (offsetStr.isNotEmpty()) {
offset = offsetStr.toInt()
labelSymbol = labelSymbol!!.substringBeforeLast('+')
labelSymbol = labelSymbol.substringBeforeLast('+')
}
}
}
@ -320,10 +320,10 @@ internal fun parseRegisterOrStatusflag(sourceregs: String): RegisterOrStatusflag
try {
reg = RegisterOrPair.valueOf(regs)
} catch (x: IllegalArgumentException) {
} catch (_: IllegalArgumentException) {
try {
sf = Statusflag.valueOf(regs)
} catch(x: IllegalArgumentException) {
} catch(_: IllegalArgumentException) {
throw IRParseException("invalid IR register or statusflag: $regs")
}
}

View File

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