added inlining certain trivial non-asm subroutine calls

This commit is contained in:
Irmen de Jong 2022-05-22 20:22:09 +02:00
parent e69aeb8b98
commit fd6eb47e68
9 changed files with 199 additions and 110 deletions

5
.idea/misc.xml generated
View File

@ -22,9 +22,4 @@
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="11" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="11" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
<component name="SwUserDefinedSpecifications">
<option name="specTypeByUrl">
<map />
</option>
</component>
</project> </project>

View File

@ -55,11 +55,9 @@ fun Program.optimizeStatements(errors: IErrorReporter,
} }
fun Program.inlineSubroutines(): Int { fun Program.inlineSubroutines(): Int {
// TODO implement the inliner val inliner = Inliner(this)
// val inliner = Inliner(this) inliner.visit(this)
// inliner.visit(this) return inliner.applyModifications()
// return inliner.applyModifications()
return 0
} }
fun Program.simplifyExpressions(errors: IErrorReporter) : Int { fun Program.simplifyExpressions(errors: IErrorReporter) : Int {

View File

@ -3,53 +3,106 @@ package prog8.optimizer
import prog8.ast.IFunctionCall import prog8.ast.IFunctionCall
import prog8.ast.Node import prog8.ast.Node
import prog8.ast.Program import prog8.ast.Program
import prog8.ast.expressions.FunctionCallExpression import prog8.ast.expressions.*
import prog8.ast.statements.* import prog8.ast.statements.*
import prog8.ast.walk.AstWalker import prog8.ast.walk.AstWalker
import prog8.ast.walk.IAstModification import prog8.ast.walk.IAstModification
import prog8.ast.walk.IAstVisitor import prog8.ast.walk.IAstVisitor
import prog8.code.core.InternalCompilerException
private fun isEmptyReturn(stmt: Statement): Boolean = stmt is Return && stmt.value==null
class Inliner(val program: Program): AstWalker() { class Inliner(val program: Program): AstWalker() {
class DetermineInlineSubs(program: Program): IAstVisitor { class DetermineInlineSubs(val program: Program): IAstVisitor {
private val modifications = mutableListOf<IAstModification>()
init { init {
visit(program) visit(program)
modifications.forEach { it.perform() }
modifications.clear()
} }
override fun visit(subroutine: Subroutine) { override fun visit(subroutine: Subroutine) {
if(!subroutine.isAsmSubroutine && !subroutine.inline && subroutine.parameters.isEmpty()) { if(!subroutine.isAsmSubroutine && !subroutine.inline && subroutine.parameters.isEmpty()) {
val containsSubsOrVariables = subroutine.statements.any { it is VarDecl || it is Subroutine} val containsSubsOrVariables = subroutine.statements.any { it is VarDecl || it is Subroutine}
if(!containsSubsOrVariables) { if(!containsSubsOrVariables) {
if(subroutine.statements.size==1 || (subroutine.statements.size==2 && subroutine.statements[1] is Return)) { if(subroutine.statements.size==1 || (subroutine.statements.size==2 && isEmptyReturn(subroutine.statements[1]))) {
// subroutine is possible candidate to be inlined // subroutine is possible candidate to be inlined
subroutine.inline = subroutine.inline =
when(val stmt=subroutine.statements[0]) { when(val stmt=subroutine.statements[0]) {
is Return -> { is Return -> {
if(stmt.value!!.isSimple) { if(stmt.value is NumericLiteral)
makeFullyScoped(stmt)
true true
else if (stmt.value is IdentifierReference) {
makeFullyScoped(stmt.value as IdentifierReference)
true
} else if(stmt.value!! is IFunctionCall && (stmt.value as IFunctionCall).args.size<=1 && (stmt.value as IFunctionCall).args.all { it is NumericLiteral || it is IdentifierReference }) {
when (stmt.value) {
is BuiltinFunctionCall -> {
makeFullyScoped(stmt.value as BuiltinFunctionCall)
true
}
is FunctionCallExpression -> {
makeFullyScoped(stmt.value as FunctionCallExpression)
true
}
else -> false
}
} else } else
false false
} }
is Assignment -> { is Assignment -> {
val inline = stmt.value.isSimple && (stmt.target.identifier!=null || stmt.target.memoryAddress?.addressExpression?.isSimple==true) if(stmt.value.isSimple) {
val targetInline =
if(stmt.target.identifier!=null) {
makeFullyScoped(stmt.target.identifier!!)
true
} else if(stmt.target.memoryAddress?.addressExpression is NumericLiteral || stmt.target.memoryAddress?.addressExpression is IdentifierReference) {
if(stmt.target.memoryAddress?.addressExpression is IdentifierReference)
makeFullyScoped(stmt.target.memoryAddress?.addressExpression as IdentifierReference)
true
} else
false
val valueInline =
if(stmt.value is IdentifierReference) {
makeFullyScoped(stmt.value as IdentifierReference)
true
} else if((stmt.value as? DirectMemoryRead)?.addressExpression is NumericLiteral || (stmt.value as? DirectMemoryRead)?.addressExpression is IdentifierReference) {
if((stmt.value as? DirectMemoryRead)?.addressExpression is IdentifierReference)
makeFullyScoped((stmt.value as? DirectMemoryRead)?.addressExpression as IdentifierReference)
true
} else
false
targetInline || valueInline
} else
false
}
is BuiltinFunctionCallStatement -> {
val inline = stmt.args.size<=1 && stmt.args.all { it is NumericLiteral || it is IdentifierReference }
if(inline) if(inline)
makeFullyScoped(stmt) makeFullyScoped(stmt)
inline inline
} }
is BuiltinFunctionCallStatement,
is FunctionCallStatement -> { is FunctionCallStatement -> {
stmt as IFunctionCall val inline = stmt.args.size<=1 && stmt.args.all { it is NumericLiteral || it is IdentifierReference }
val inline = stmt.args.size<=1 && stmt.args.all { it.isSimple }
if(inline) if(inline)
makeFullyScoped(stmt) makeFullyScoped(stmt)
inline inline
} }
is PostIncrDecr -> { is PostIncrDecr -> {
val inline = (stmt.target.identifier!=null || stmt.target.memoryAddress?.addressExpression?.isSimple==true) if(stmt.target.identifier!=null) {
if(inline) makeFullyScoped(stmt.target.identifier!!)
makeFullyScoped(stmt) true
inline }
else if(stmt.target.memoryAddress?.addressExpression is NumericLiteral || stmt.target.memoryAddress?.addressExpression is IdentifierReference) {
if(stmt.target.memoryAddress?.addressExpression is IdentifierReference)
makeFullyScoped(stmt.target.memoryAddress?.addressExpression as IdentifierReference)
true
} else
false
} }
is Jump, is GoSub -> true is Jump, is GoSub -> true
else -> false else -> false
@ -60,20 +113,51 @@ class Inliner(val program: Program): AstWalker() {
super.visit(subroutine) super.visit(subroutine)
} }
private fun makeFullyScoped(incrdecr: PostIncrDecr) { private fun makeFullyScoped(identifier: IdentifierReference) {
TODO("Not yet implemented") val scoped = (identifier.targetStatement(program)!! as INamedStatement).scopedName
val scopedIdent = IdentifierReference(scoped, identifier.position)
modifications += IAstModification.ReplaceNode(identifier, scopedIdent, identifier.parent)
} }
private fun makeFullyScoped(call: IFunctionCall) { private fun makeFullyScoped(call: BuiltinFunctionCallStatement) {
TODO("Not yet implemented") val scopedArgs = makeScopedArgs(call.args)
val scopedCall = BuiltinFunctionCallStatement(call.target.copy(), scopedArgs.toMutableList(), call.position)
modifications += IAstModification.ReplaceNode(call, scopedCall, call.parent)
} }
private fun makeFullyScoped(assign: Assignment) { private fun makeFullyScoped(call: FunctionCallStatement) {
TODO("Not yet implemented") val sub = call.target.targetSubroutine(program)!!
val scopedName = IdentifierReference(sub.scopedName, call.target.position)
val scopedArgs = makeScopedArgs(call.args)
val scopedCall = FunctionCallStatement(scopedName, scopedArgs.toMutableList(), call.void, call.position)
modifications += IAstModification.ReplaceNode(call, scopedCall, call.parent)
} }
private fun makeFullyScoped(ret: Return) { private fun makeFullyScoped(call: BuiltinFunctionCall) {
TODO("Not yet implemented") val sub = call.target.targetSubroutine(program)!!
val scopedName = IdentifierReference(sub.scopedName, call.target.position)
val scopedArgs = makeScopedArgs(call.args)
val scopedCall = BuiltinFunctionCall(scopedName, scopedArgs.toMutableList(), call.position)
modifications += IAstModification.ReplaceNode(call, scopedCall, call.parent)
}
private fun makeFullyScoped(call: FunctionCallExpression) {
val scopedArgs = makeScopedArgs(call.args)
val scopedCall = FunctionCallExpression(call.target.copy(), scopedArgs.toMutableList(), call.position)
modifications += IAstModification.ReplaceNode(call, scopedCall, call.parent)
}
private fun makeScopedArgs(args: List<Expression>): List<Expression> {
return args.map {
when (it) {
is NumericLiteral -> it.copy()
is IdentifierReference -> {
val scoped = (it.targetStatement(program)!! as INamedStatement).scopedName
IdentifierReference(scoped, it.position)
}
else -> throw InternalCompilerException("expected only number or identifier arg, otherwise too complex")
}
}
} }
} }
@ -84,24 +168,41 @@ class Inliner(val program: Program): AstWalker() {
override fun after(gosub: GoSub, parent: Node): Iterable<IAstModification> { override fun after(gosub: GoSub, parent: Node): Iterable<IAstModification> {
val sub = gosub.identifier.targetStatement(program) as? Subroutine val sub = gosub.identifier.targetStatement(program) as? Subroutine
if(sub!=null && sub.inline) { if(sub!=null && sub.inline && sub.parameters.isEmpty()) {
val inlined = sub.statements require(sub.statements.size == 1 || (sub.statements.size == 2 && isEmptyReturn(sub.statements[1])))
TODO("INLINE GOSUB: $gosub ---> $inlined") return if(sub.isAsmSubroutine) {
// simply insert the asm for the argument-less routine
listOf(IAstModification.ReplaceNode(gosub, sub.statements.single().copy(), parent))
} else {
// note that we don't have to process any args, because we online inline parameterless subroutines.
when (val toInline = sub.statements.first()) {
is Return -> noModifications
else -> listOf(IAstModification.ReplaceNode(gosub, toInline.copy(), parent))
}
}
} }
return noModifications return noModifications
} }
override fun after(functionCallExpr: FunctionCallExpression, parent: Node): Iterable<IAstModification> = inlineCall(functionCallExpr as IFunctionCall, parent) override fun after(functionCallStatement: FunctionCallStatement, parent: Node): Iterable<IAstModification> {
val sub = functionCallStatement.target.targetStatement(program) as? Subroutine
override fun after(functionCallStatement: FunctionCallStatement, parent: Node): Iterable<IAstModification> = inlineCall(functionCallStatement as IFunctionCall, parent) if(sub!=null && sub.inline && sub.parameters.isEmpty()) {
require(sub.statements.size==1 || (sub.statements.size==2 && isEmptyReturn(sub.statements[1])))
private fun inlineCall(call: IFunctionCall, parent: Node): Iterable<IAstModification> { return if(sub.isAsmSubroutine) {
val sub = call.target.targetStatement(program) as? Subroutine // simply insert the asm for the argument-less routine
if(sub!=null && sub.inline) { listOf(IAstModification.ReplaceNode(functionCallStatement, sub.statements.single().copy(), parent))
val inlined = sub.statements } else {
TODO("INLINE FCALL: $call ---> $inlined") // note that we don't have to process any args, because we online inline parameterless subroutines.
when (val toInline = sub.statements.first()) {
is Return -> noModifications
else -> listOf(IAstModification.ReplaceNode(functionCallStatement, toInline.copy(), parent))
}
}
} }
return noModifications return noModifications
} }
// TODO also inline function call expressions, and remove it from the StatementOptimizer
} }

View File

@ -4,10 +4,9 @@ import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result import com.github.michaelbull.result.Result
import com.github.michaelbull.result.getOrElse import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.mapError import com.github.michaelbull.result.mapError
import prog8.ast.IFunctionCall
import prog8.ast.IStatementContainer
import prog8.ast.Program import prog8.ast.Program
import prog8.ast.base.FatalAstException import prog8.ast.base.FatalAstException
import prog8.ast.determineGosubArguments
import prog8.ast.expressions.* import prog8.ast.expressions.*
import prog8.ast.statements.* import prog8.ast.statements.*
import prog8.code.ast.* import prog8.code.ast.*
@ -242,32 +241,10 @@ class IntermediateAstMaker(val program: Program) {
// Gather the Goto and any preceding parameter assignments back into a single Function call node. // Gather the Goto and any preceding parameter assignments back into a single Function call node.
// (the reason it was split up in the first place, is because the Compiler Ast optimizers // (the reason it was split up in the first place, is because the Compiler Ast optimizers
// can then work on any complex expressions that are used as arguments.) // can then work on any complex expressions that are used as arguments.)
val parent = gosub.parent as IStatementContainer val arguments = determineGosubArguments(gosub)
val gosubIdx = parent.statements.indexOf(gosub)
val previousNodes = parent.statements.subList(0, gosubIdx).reversed()
val paramValues = mutableMapOf<String, Expression>()
for (node in previousNodes) {
if(node !is Assignment || node.origin!=AssignmentOrigin.PARAMETERASSIGN)
break
paramValues[node.target.identifier!!.nameInSource.last()] = node.value
}
// instead of just assigning to the parameters, another way is to use push()/pop()
if(previousNodes.isNotEmpty()) {
val first = previousNodes[0] as? IFunctionCall
if(first!=null && (first.target.nameInSource.singleOrNull() in arrayOf("pop", "popw"))) {
val numPops = previousNodes.indexOfFirst { (it as? IFunctionCall)?.target?.nameInSource?.singleOrNull() !in arrayOf("pop", "popw") }
val pops = previousNodes.subList(0, numPops)
val pushes = previousNodes.subList(numPops, numPops+numPops).reversed()
for ((push, pop) in pushes.zip(pops)) {
val name = ((pop as IFunctionCall).args.single() as IdentifierReference).nameInSource.last()
val arg = (push as IFunctionCall).args.single()
paramValues[name] = arg
}
}
}
val parameters = gosub.identifier.targetSubroutine(program)!!.parameters val parameters = gosub.identifier.targetSubroutine(program)!!.parameters
if(paramValues.size != parameters.size) if(arguments.size != parameters.size)
throw FatalAstException("mismatched number of parameter assignments for function call") throw FatalAstException("mismatched number of parameter assignments for function call")
val target = transform(gosub.identifier) val target = transform(gosub.identifier)
@ -275,7 +252,7 @@ class IntermediateAstMaker(val program: Program) {
// put arguments in correct order for the parameters // put arguments in correct order for the parameters
parameters.forEach { parameters.forEach {
val argument = paramValues.getValue(it.name) val argument = arguments.getValue(it.name)
call.add(transformExpression(argument)) call.add(transformExpression(argument))
} }

View File

@ -1,10 +1,10 @@
package prog8.ast package prog8.ast
import prog8.ast.base.FatalAstException import prog8.ast.base.FatalAstException
import prog8.ast.expressions.Expression
import prog8.ast.expressions.IdentifierReference
import prog8.ast.expressions.InferredTypes import prog8.ast.expressions.InferredTypes
import prog8.ast.statements.VarDecl import prog8.ast.statements.*
import prog8.ast.statements.VarDeclOrigin
import prog8.ast.statements.VarDeclType
import prog8.code.core.DataType import prog8.code.core.DataType
import prog8.code.core.Position import prog8.code.core.Position
import prog8.code.core.ZeropageWish import prog8.code.core.ZeropageWish
@ -56,3 +56,32 @@ fun getTempRegisterName(dt: InferredTypes.InferredType): List<String> {
else -> throw FatalAstException("invalid dt $dt") else -> throw FatalAstException("invalid dt $dt")
} }
} }
fun determineGosubArguments(gosub: GoSub): Map<String, Expression> {
val parent = gosub.parent as IStatementContainer
val gosubIdx = parent.statements.indexOf(gosub)
val previousNodes = parent.statements.subList(0, gosubIdx).reversed()
val arguments = mutableMapOf<String, Expression>()
for (node in previousNodes) {
if(node !is Assignment || node.origin!=AssignmentOrigin.PARAMETERASSIGN)
break
arguments[node.target.identifier!!.nameInSource.last()] = node.value
}
// instead of just assigning to the parameters, another way is to use push()/pop()
if(previousNodes.isNotEmpty()) {
val first = previousNodes[0] as? IFunctionCall
if(first!=null && (first.target.nameInSource.singleOrNull() in arrayOf("pop", "popw"))) {
val numPops = previousNodes.indexOfFirst { (it as? IFunctionCall)?.target?.nameInSource?.singleOrNull() !in arrayOf("pop", "popw") }
val pops = previousNodes.subList(0, numPops)
val pushes = previousNodes.subList(numPops, numPops+numPops).reversed()
for ((push, pop) in pushes.zip(pops)) {
val name = ((pop as IFunctionCall).args.single() as IdentifierReference).nameInSource.last()
val arg = (push as IFunctionCall).args.single()
arguments[name] = arg
}
}
}
return arguments
}

View File

@ -932,7 +932,10 @@ class FunctionCallExpression(override var target: IdentifierReference,
} }
override fun copy() = FunctionCallExpression(target.copy(), args.map { it.copy() }.toMutableList(), position) override fun copy() = FunctionCallExpression(target.copy(), args.map { it.copy() }.toMutableList(), position)
override val isSimple = target.nameInSource.size==1 && (target.nameInSource[0] in arrayOf("msb", "lsb", "peek", "peekw")) override val isSimple =
target.nameInSource.size==1
&& target.nameInSource[0] in arrayOf("msb", "lsb", "peek", "peekw", "mkword")
&& args.all { it.isSimple }
override fun replaceChildNode(node: Node, replacement: Node) { override fun replaceChildNode(node: Node, replacement: Node) {
if(node===target) if(node===target)

View File

@ -613,7 +613,10 @@ class FunctionCallStatement(override var target: IdentifierReference,
args.forEach { it.linkParents(this) } args.forEach { it.linkParents(this) }
} }
override fun copy() = throw NotImplementedError("no support for duplicating a FunctionCallStatement") override fun copy(): FunctionCallStatement {
val argsCopies = args.map { it.copy() }
return FunctionCallStatement(target.copy(), argsCopies.toMutableList(), void, position)
}
override fun replaceChildNode(node: Node, replacement: Node) { override fun replaceChildNode(node: Node, replacement: Node) {
if(node===target) if(node===target)
@ -637,7 +640,7 @@ class InlineAssembly(val assembly: String, override val position: Position) : St
this.parent = parent this.parent = parent
} }
override fun copy() = throw NotImplementedError("no support for duplicating a InlineAssembly") override fun copy() = InlineAssembly(assembly, position)
override fun replaceChildNode(node: Node, replacement: Node) = throw FatalAstException("can't replace here") override fun replaceChildNode(node: Node, replacement: Node) = throw FatalAstException("can't replace here")
override fun accept(visitor: IAstVisitor) = visitor.visit(this) override fun accept(visitor: IAstVisitor) = visitor.visit(this)

View File

@ -3,7 +3,6 @@ TODO
For next release For next release
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
- complete the Inliner
- add McCarthy evaluation to shortcircuit and/or expressions. First do ifs by splitting them up? Then do expressions that compute a value? - add McCarthy evaluation to shortcircuit and/or expressions. First do ifs by splitting them up? Then do expressions that compute a value?
... ...
@ -27,6 +26,7 @@ Compiler:
- vm: how to remove all unused subroutines? (in the assembly codegen, we let 64tass solve this for us) - vm: how to remove all unused subroutines? (in the assembly codegen, we let 64tass solve this for us)
- vm: rather than being able to jump to any 'address' (IPTR), use 'blocks' that have entry and exit points -> even better dead code elimination possible too - vm: rather than being able to jump to any 'address' (IPTR), use 'blocks' that have entry and exit points -> even better dead code elimination possible too
- vm: add more assignments to translateInplaceAssign() - vm: add more assignments to translateInplaceAssign()
- Inliner: also inline function call expressions, and remove it from the StatementOptimizer
- when the vm is stable and *if* its language can get promoted to prog8 IL, the variable allocation should be changed. - when the vm is stable and *if* its language can get promoted to prog8 IL, the variable allocation should be changed.
It's now done before the vm code generation, but the IL should probably not depend on the allocations already performed. It's now done before the vm code generation, but the IL should probably not depend on the allocations already performed.
So the CodeGen doesn't do VariableAlloc *before* the codegen, but as a last step. So the CodeGen doesn't do VariableAlloc *before* the codegen, but as a last step.

View File

@ -7,46 +7,29 @@
; NOTE: meant to test to virtual machine output target (use -target vitual) ; NOTE: meant to test to virtual machine output target (use -target vitual)
main { other {
ubyte value = 42 ubyte value = 42
sub inline_candidate() -> ubyte { sub getter() -> ubyte {
return math.sin8u(value) return value
} }
}
sub inline_candidate2() { main {
value++
return
}
sub add(ubyte first, ubyte second) -> ubyte {
return first + second
}
sub mul(ubyte first, ubyte second) -> ubyte {
return first * second
}
ubyte ix
sub start() { sub start() {
ubyte @shared ix = other.getter()
ubyte @shared value1 = inline_candidate() ix = other.getter()
txt.print_ub(value) ; 42 ix++
txt.spc() ix = other.getter()
inline_candidate2() ix++
inline_candidate2() ix = other.getter()
inline_candidate2() ix++
txt.print_ub(value) ; 45 ix = other.getter()
txt.nl() ix++
txt.print_ub(inline_candidate()) ix = other.getter()
txt.nl() ix++
ubyte @shared value=99 ; TODO compiler warning about shadowing
txt.print_ub(value)
txt.nl()
ubyte @shared add=99 ; TODO compiler warning about shadowing
; ; a "pixelshader": ; ; a "pixelshader":
; sys.gfx_enable(0) ; enable lo res screen ; sys.gfx_enable(0) ; enable lo res screen