replace subroutine calls (statement) by GoSub

This commit is contained in:
Irmen de Jong 2021-11-24 01:41:04 +01:00
parent 17d403d812
commit 110e047681
14 changed files with 166 additions and 89 deletions

View File

@ -545,6 +545,7 @@ class AsmGen(private val program: Program,
fun asmVariableName(identifier: IdentifierReference) =
fixNameSymbols(identifier.nameInSource.joinToString("."))
// TODO use INamedStatement.scopedName
private fun getScopedSymbolNameForTarget(actualName: String, target: Statement): MutableList<String> {
val scopedName = mutableListOf(actualName)
var node: Node = target
@ -857,8 +858,8 @@ class AsmGen(private val program: Program,
internal fun translateBuiltinFunctionCallExpression(functionCall: FunctionCall, signature: FSignature, resultToStack: Boolean, resultRegister: RegisterOrPair?) =
builtinFunctionsAsmGen.translateFunctioncallExpression(functionCall, signature, resultToStack, resultRegister)
internal fun translateFunctionCall(functionCall: FunctionCall) =
functioncallAsmGen.translateFunctionCall(functionCall)
internal fun translateFunctionCall(functionCall: FunctionCall, isExpression: Boolean) =
functioncallAsmGen.translateFunctionCall(functionCall, isExpression)
internal fun saveXbeforeCall(functionCall: IFunctionCall) =
functioncallAsmGen.saveXbeforeCall(functionCall)

View File

@ -53,7 +53,7 @@ internal class ExpressionsAsmGen(private val program: Program, private val asmge
} else {
sub as Subroutine
asmgen.saveXbeforeCall(call)
asmgen.translateFunctionCall(call)
asmgen.translateFunctionCall(call, true)
if(sub.regXasResult()) {
// store the return value in X somewhere that we can acces again below
asmgen.out(" stx P8ZP_SCRATCH_REG")

View File

@ -5,10 +5,7 @@ import prog8.ast.Node
import prog8.ast.Program
import prog8.ast.base.*
import prog8.ast.expressions.*
import prog8.ast.statements.InlineAssembly
import prog8.ast.statements.RegisterOrStatusflag
import prog8.ast.statements.Subroutine
import prog8.ast.statements.SubroutineParameter
import prog8.ast.statements.*
import prog8.compiler.target.AssemblyError
import prog8.compiler.target.cpu6502.codegen.assignment.AsmAssignSource
import prog8.compiler.target.cpu6502.codegen.assignment.AsmAssignTarget
@ -21,7 +18,7 @@ internal class FunctionCallAsmGen(private val program: Program, private val asmg
internal fun translateFunctionCallStatement(stmt: IFunctionCall) {
saveXbeforeCall(stmt)
translateFunctionCall(stmt)
translateFunctionCall(stmt, false)
restoreXafterCall(stmt)
// just ignore any result values from the function call.
}
@ -48,26 +45,30 @@ internal class FunctionCallAsmGen(private val program: Program, private val asmg
}
}
internal fun translateFunctionCall(stmt: IFunctionCall) {
internal fun translateFunctionCall(call: IFunctionCall, isExpression: Boolean) {
// Output only the code to set up the parameters and perform the actual call
// NOTE: does NOT output the code to deal with the result values!
// NOTE: does NOT output code to save/restore the X register for this call! Every caller should deal with this in their own way!!
// (you can use subroutine.shouldSaveX() and saveX()/restoreX() routines as a help for this)
val sub = stmt.target.targetSubroutine(program) ?: throw AssemblyError("undefined subroutine ${stmt.target}")
val subName = asmgen.asmSymbolName(stmt.target)
if(stmt.args.isNotEmpty()) {
val sub = call.target.targetSubroutine(program) ?: throw AssemblyError("undefined subroutine ${call.target}")
if(!isExpression && !sub.isAsmSubroutine) {
throw AssemblyError("functioncall statments to non-asmsub should have been replaced by GoSub $call")
}
if(call.args.isNotEmpty()) {
if(sub.asmParameterRegisters.isEmpty()) {
// via variables
for(arg in sub.parameters.withIndex().zip(stmt.args)) {
for(arg in sub.parameters.withIndex().zip(call.args)) {
argumentViaVariable(sub, arg.first, arg.second)
}
} else {
require(sub.isAsmSubroutine)
if(sub.parameters.size==1) {
// just a single parameter, no risk of clobbering registers
argumentViaRegister(sub, IndexedValue(0, sub.parameters.single()), stmt.args[0])
argumentViaRegister(sub, IndexedValue(0, sub.parameters.single()), call.args[0])
} else {
fun isNoClobberRisk(expr: Expression): Boolean {
@ -89,10 +90,10 @@ internal class FunctionCallAsmGen(private val program: Program, private val asmg
}
when {
stmt.args.all {isNoClobberRisk(it)} -> {
call.args.all {isNoClobberRisk(it)} -> {
// There's no risk of clobbering for these simple argument types. Optimize the register loading directly from these values.
// register assignment order: 1) cx16 virtual word registers, 2) actual CPU registers, 3) CPU Carry status flag.
val argsInfo = sub.parameters.withIndex().zip(stmt.args).zip(sub.asmParameterRegisters)
val argsInfo = sub.parameters.withIndex().zip(call.args).zip(sub.asmParameterRegisters)
val (cx16virtualRegs, args2) = argsInfo.partition { it.second.registerOrPair in Cx16VirtualRegisters }
val (cpuRegs, statusRegs) = args2.partition { it.second.registerOrPair!=null }
for(arg in cx16virtualRegs)
@ -104,13 +105,14 @@ internal class FunctionCallAsmGen(private val program: Program, private val asmg
}
else -> {
// Risk of clobbering due to complex expression args. Evaluate first, then assign registers.
registerArgsViaStackEvaluation(stmt, sub)
registerArgsViaStackEvaluation(call, sub)
}
}
}
}
}
val subName = asmgen.asmSymbolName(call.target)
if(!sub.inline || !asmgen.options.optimize) {
asmgen.out(" jsr $subName")
} else {
@ -267,7 +269,7 @@ internal class FunctionCallAsmGen(private val program: Program, private val asmg
if(!isArgumentTypeCompatible(valueDt, parameter.value.type))
throw AssemblyError("argument type incompatible")
val varName = asmgen.asmVariableName(sub.scopedname+"."+parameter.value.name)
val varName = asmgen.asmVariableName(sub.scopedName + parameter.value.name)
asmgen.assignExpressionToVariable(value, varName, parameter.value.type, sub)
}

View File

@ -159,7 +159,7 @@ internal class AssignmentAsmGen(private val program: Program, private val asmgen
when (val sub = value.target.targetStatement(program)) {
is Subroutine -> {
asmgen.saveXbeforeCall(value)
asmgen.translateFunctionCall(value)
asmgen.translateFunctionCall(value, true)
val returnValue = sub.returntypes.zip(sub.asmReturnvaluesRegisters).singleOrNull { it.second.registerOrPair!=null } ?:
sub.returntypes.zip(sub.asmReturnvaluesRegisters).single { it.second.statusflag!=null }
when (returnValue.first) {

View File

@ -57,6 +57,7 @@ class StatementOptimizer(private val program: Program,
}
override fun after(functionCallStatement: FunctionCallStatement, parent: Node): Iterable<IAstModification> {
// TODO use functionCallStatement.target.targetStatement() is BuiltinFunctionStatementPlaceholder ?
if(functionCallStatement.target.nameInSource.size==1 && functionCallStatement.target.nameInSource[0] in functions.names) {
val functionName = functionCallStatement.target.nameInSource[0]
if (functionName in functions.purefunctionNames) {

View File

@ -69,35 +69,10 @@ internal class BeforeAsmGenerationAstChanger(val program: Program, private val o
}
private val subroutineVariables = mutableListOf<Pair<String, VarDecl>>()
private val addedIfConditionVars = mutableSetOf<Pair<Subroutine, String>>()
override fun before(subroutine: Subroutine, parent: Node): Iterable<IAstModification> {
subroutineVariables.clear()
addedIfConditionVars.clear()
if(!subroutine.isAsmSubroutine) {
// change 'str' parameters into 'uword' (just treat it as an address)
val stringParams = subroutine.parameters.filter { it.type==DataType.STR }
val parameterChanges = stringParams.map {
val uwordParam = SubroutineParameter(it.name, DataType.UWORD, it.position)
IAstModification.ReplaceNode(it, uwordParam, subroutine)
}
val stringParamsByNames = stringParams.associateBy { it.name }
val varsChanges =
if(stringParamsByNames.isNotEmpty()) {
subroutine.statements
.filterIsInstance<VarDecl>()
.filter { it.subroutineParameter!=null && it.name in stringParamsByNames }
.map {
val newvar = VarDecl(it.type, DataType.UWORD, it.zeropage, null, it.name, null, false, true, it.sharedWithAsm, stringParamsByNames.getValue(it.name), it.position)
IAstModification.ReplaceNode(it, newvar, subroutine)
}
}
else emptyList()
return parameterChanges + varsChanges
}
return noModifications
}

View File

@ -418,11 +418,10 @@ internal class AstChecker(private val program: Program,
val stmt = (assignment.value as FunctionCall).target.targetStatement(program)
if (stmt is Subroutine) {
val idt = assignment.target.inferType(program)
if(!idt.isKnown) {
errors.err("return type mismatch", assignment.value.position)
}
if(!idt.isKnown)
throw FatalAstException("assignment target invalid dt")
if(stmt.returntypes.isEmpty() || (stmt.returntypes.size == 1 && stmt.returntypes.single() isNotAssignableTo idt.getOr(DataType.BYTE))) {
errors.err("return type mismatch", assignment.value.position)
errors.err("return type mismatch: ${stmt.returntypes.single()} expected $idt", assignment.value.position)
}
}
}
@ -977,21 +976,9 @@ internal class AstChecker(private val program: Program,
override fun visit(functionCallStatement: FunctionCallStatement) {
val targetStatement = checkFunctionOrLabelExists(functionCallStatement.target, functionCallStatement)
if(targetStatement!=null)
if(targetStatement!=null) {
checkFunctionCall(targetStatement, functionCallStatement.args, functionCallStatement.position)
if (!functionCallStatement.void) {
// check for unused return values
if (targetStatement is Subroutine && targetStatement.returntypes.isNotEmpty()) {
if(targetStatement.returntypes.size==1)
errors.warn("result value of subroutine call is discarded (use void?)", functionCallStatement.position)
else
errors.warn("result values of subroutine call are discarded (use void?)", functionCallStatement.position)
}
else if(targetStatement is BuiltinFunctionStatementPlaceholder) {
val rt = builtinFunctionReturnType(targetStatement.name, functionCallStatement.args, program)
if(rt.isKnown)
errors.warn("result value of a function call is discarded (use void?)", functionCallStatement.position)
}
checkUnusedReturnValues(functionCallStatement, targetStatement, program, errors)
}
if(functionCallStatement.target.nameInSource.last() == "sort") {
@ -1402,3 +1389,19 @@ internal class AstChecker(private val program: Program,
return false
}
}
internal fun checkUnusedReturnValues(call: FunctionCallStatement, target: Statement, program: Program, errors: IErrorReporter) {
if (!call.void) {
// check for unused return values
if (target is Subroutine && target.returntypes.isNotEmpty()) {
if (target.returntypes.size == 1)
errors.warn("result value of subroutine call is discarded (use void?)", call.position)
else
errors.warn("result values of subroutine call are discarded (use void?)", call.position)
} else if (target is BuiltinFunctionStatementPlaceholder) {
val rt = builtinFunctionReturnType(target.name, call.args, program)
if (rt.isKnown)
errors.warn("result value of a function call is discarded (use void?)", call.position)
}
}
}

View File

@ -21,6 +21,7 @@ internal class StatementReorderer(val program: Program, val errors: IErrorReport
// - in-place assignments are reordered a bit so that they are mostly of the form A = A <operator> <rest>
// - sorts the choices in when statement.
// - insert AddressOf (&) expression where required (string params to a UWORD function param etc.).
// - replace subroutine calls (statement) by just assigning the arguments to the parameters and then a GoSub to the routine.
private val directivesToMove = setOf("%output", "%launcher", "%zeropage", "%zpreserved", "%address", "%option")
@ -122,6 +123,30 @@ internal class StatementReorderer(val program: Program, val errors: IErrorReport
subs.map { IAstModification.InsertLast(it, subroutine) }
}
if(!subroutine.isAsmSubroutine) {
// change 'str' parameters into 'uword' (just treat it as an address)
val stringParams = subroutine.parameters.filter { it.type==DataType.STR }
val parameterChanges = stringParams.map {
val uwordParam = SubroutineParameter(it.name, DataType.UWORD, it.position)
IAstModification.ReplaceNode(it, uwordParam, subroutine)
}
val stringParamsByNames = stringParams.associateBy { it.name }
val varsChanges =
if(stringParamsByNames.isNotEmpty()) {
subroutine.statements
.filterIsInstance<VarDecl>()
.filter { it.subroutineParameter!=null && it.name in stringParamsByNames }
.map {
val newvar = VarDecl(it.type, DataType.UWORD, it.zeropage, null, it.name, null, false, true, it.sharedWithAsm, stringParamsByNames.getValue(it.name), it.position)
IAstModification.ReplaceNode(it, newvar, subroutine)
}
}
else emptyList()
return parameterChanges + varsChanges
}
return noModifications
}
@ -333,4 +358,34 @@ internal class StatementReorderer(val program: Program, val errors: IErrorReport
)
return listOf(IAstModification.ReplaceNode(assign, memcopy, assign.parent))
}
override fun after(functionCallStatement: FunctionCallStatement, parent: Node): Iterable<IAstModification> {
val function = functionCallStatement.target.targetStatement(program)!!
checkUnusedReturnValues(functionCallStatement, function, program, errors)
if(function is Subroutine) {
if(function.isAsmSubroutine)
return noModifications // TODO new logic for passing arguments to asmsub
// regular subroutine call: replace the call with assigning the params directly + actual call with a GoSub
require(function.asmParameterRegisters.isEmpty())
val assignParams =
function.parameters.zip(functionCallStatement.args).map {
var argumentValue = it.second
val paramIdentifier = IdentifierReference(function.scopedName + it.first.name, argumentValue.position)
val argDt = argumentValue.inferType(program).getOrElse { throw FatalAstException("invalid dt") }
if(argDt in ArrayDatatypes) {
// pass the address of the array instead
argumentValue = AddressOf(argumentValue as IdentifierReference, argumentValue.position)
}
Assignment(AssignTarget(paramIdentifier, null, null, argumentValue.position), argumentValue, argumentValue.position)
}
return assignParams.map { IAstModification.InsertBefore(functionCallStatement, it, parent as IStatementContainer) } +
IAstModification.ReplaceNode(
functionCallStatement as Node,
GoSub(null, functionCallStatement.target, null, (functionCallStatement as Node).position),
parent)
}
return noModifications
}
}

View File

@ -50,17 +50,18 @@ class TestSubroutines: FunSpec({
asmfunc.parameters.single().type shouldBe DataType.STR
asmfunc.statements.isEmpty() shouldBe true
func.isAsmSubroutine shouldBe false
func.parameters.single().type shouldBe DataType.STR
func.statements.size shouldBe 4
val paramvar = func.statements[0] as VarDecl
paramvar.name shouldBe "thing"
paramvar.datatype shouldBe DataType.STR
withClue("str param for normal subroutine should be changed into UWORD") {
func.parameters.single().type shouldBe DataType.UWORD
func.statements.size shouldBe 4
val paramvar = func.statements[0] as VarDecl
paramvar.name shouldBe "thing"
paramvar.datatype shouldBe DataType.UWORD
}
val assign = func.statements[2] as Assignment
assign.target.identifier!!.nameInSource shouldBe listOf("t2")
withClue("str param in function body should not be transformed by normal compiler steps") {
assign.value shouldBe instanceOf<TypecastExpression>()
withClue("str param in function body should have been transformed into just uword assignment") {
assign.value shouldBe instanceOf<IdentifierReference>()
}
(assign.value as TypecastExpression).type shouldBe DataType.UWORD
val call = func.statements[3] as FunctionCallStatement
call.target.nameInSource.single() shouldBe "asmfunc"
withClue("str param in function body should not be transformed by normal compiler steps") {

View File

@ -29,7 +29,7 @@ enum class DataType {
UWORD -> targetType.oneOf(UWORD, FLOAT)
WORD -> targetType.oneOf(WORD, FLOAT)
FLOAT -> targetType == FLOAT
STR -> targetType == STR
STR -> targetType.oneOf(STR, UWORD)
in ArrayDatatypes -> targetType == this
else -> false
}

View File

@ -302,7 +302,7 @@ class ArrayIndexedExpression(var arrayvar: IdentifierReference,
val target = arrayvar.targetStatement(program)
if (target is VarDecl) {
return when (target.datatype) {
DataType.STR -> InferredTypes.knownFor(DataType.UBYTE)
DataType.STR, DataType.UWORD -> InferredTypes.knownFor(DataType.UBYTE)
in ArrayDatatypes -> InferredTypes.knownFor(ArrayToElementTypes.getValue(target.datatype))
else -> InferredTypes.unknown()
}

View File

@ -9,6 +9,19 @@ import prog8.ast.walk.IAstVisitor
interface INamedStatement {
val name: String
val scopedName: List<String>
get() {
val scopedName = mutableListOf(name)
var node: Node = this as Node
while (node !is Block) {
node = node.parent
if(node is INameScope) {
scopedName.add(0, node.name)
}
}
return scopedName
}
}
sealed class Statement : Node {
@ -16,6 +29,7 @@ sealed class Statement : Node {
abstract fun accept(visitor: IAstVisitor)
abstract fun accept(visitor: AstWalker, parent: Node)
@Deprecated("get rid of this in favor of INamedStatement.scopedName") // TODO
fun makeScopedName(name: String): String {
// easy way out is to always return the full scoped name.
// it would be nicer to find only the minimal prefixed scoped name, but that's too much hassle for now.
@ -34,6 +48,7 @@ sealed class Statement : Node {
return scope.joinToString(".")
}
fun nextSibling(): Statement? {
val statements = (parent as? IStatementContainer)?.statements ?: return null
val nextIdx = statements.indexOfFirst { it===this } + 1
@ -664,7 +679,6 @@ class Subroutine(override val name: String,
override lateinit var parent: Node
val asmGenInfo = AsmGenInfo()
val scopedname: String by lazy { makeScopedName(name) }
override fun copy() = throw NotImplementedError("no support for duplicating a Subroutine")
@ -751,6 +765,7 @@ open class SubroutineParameter(val name: String,
}
override fun copy() = SubroutineParameter(name, type, position)
override fun toString() = "Param($type:$name)"
}
class IfStatement(var condition: Expression,

View File

@ -3,12 +3,16 @@ TODO
For next compiler release (7.4)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Use GoSub to call subroutines (not in expressions?):
Use GoSub to call subroutines (statements):
- [DONE] allow separate assigns to subroutine's parameter variables / registers
- for asmsubs: implement asmgen for assigning to asm parameter (register)
- for asmsubs: implement asmgen for reading asm parameter (register)
- replace subroutine param load code with just the right order of those assigns
- finally replace the actual call with a GoSub
- turn a regular subroutine call into assignments to the parameters + GoSub (take code from gosub branch)
Function call expression:
- move args to assignments to params
- add tempvar immediately in front of expression with the fuction call
- replace the function call in the expression with the tempvar
...

View File

@ -1,26 +1,46 @@
%import textio
%import conv
main {
sub start() {
ubyte @shared xx
main.routine.r1arg = 20
; main.routine2.r2arg = 20 ; TODO asmgen
xx = main.routine.r1arg
xx++
;xx = main.routine2.r2arg ; TODO asmgen
;xx++
uword xx
xx = random_name()
; concat_string(random_name())
; ubyte xx=20
; ubyte yy=10
;
; routine(33)
; txt.setcc(xx+1, yy+3, 81, 7)
; txt.setcc(xx+2, yy+2, 81, 7)
; txt.setcc(xx+3, yy+1, 81, 7)
;
; ; TODO test new param load with subroutine call in expression:
; ; yy=routine(33)
;
; main.routine.r1arg = 20
; ; main.routine2.r2arg = 20 ; TODO asmgen
;
; xx = main.routine.r1arg
; xx++
; ;xx = main.routine2.r2arg ; TODO asmgen
; ;xx++
printstuff("hello")
repeat {
}
}
sub printstuff(str addr) {
sub random_name() -> str {
ubyte ii
str name = " " ; 8 chars max
return name
}
sub routine(ubyte r1arg) {
sub routine(ubyte r1arg) -> ubyte {
r1arg++
return r1arg
}
asmsub routine2(ubyte r2arg @ A) {