prog8/compiler/test/TestCallgraph.kt

289 lines
10 KiB
Kotlin

package prog8tests
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual
import io.kotest.matchers.maps.shouldNotContainKey
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import prog8.ast.Program
import prog8.ast.statements.Block
import prog8.ast.statements.Subroutine
import prog8.code.core.SourceCode
import prog8.code.target.C64Target
import prog8.code.target.VMTarget
import prog8.compiler.CallGraph
import prog8.parser.Prog8Parser.parseModule
import prog8.vm.VmRunner
import prog8tests.helpers.*
import kotlin.io.path.readText
class TestCallgraph: FunSpec({
test("testGraphForEmptySubs") {
val sourcecode = """
%import string
main {
sub start() {
}
sub empty() {
}
}
"""
val result = compileText(C64Target(), false, sourcecode)!!
val graph = CallGraph(result.compilerAst)
graph.imports.size shouldBe 1
graph.importedBy.size shouldBe 1
val toplevelModule = result.compilerAst.toplevelModule
val importedModule = graph.imports.getValue(toplevelModule).single()
importedModule.name shouldBe "string"
val importedBy = graph.importedBy.getValue(importedModule).single()
importedBy.name.startsWith("on_the_fly_test") shouldBe true
graph.unused(toplevelModule) shouldBe false
graph.unused(importedModule) shouldBe false
val mainBlock = toplevelModule.statements.filterIsInstance<Block>().single()
for(stmt in mainBlock.statements) {
val sub = stmt as Subroutine
graph.calls shouldNotContainKey sub
graph.calledBy shouldNotContainKey sub
if(sub === result.compilerAst.entrypoint)
withClue("start() should always be marked as used to avoid having it removed") {
graph.unused(sub) shouldBe false
}
else
graph.unused(sub) shouldBe true
}
}
test("reference to empty sub") {
val sourcecode = """
%import string
main {
sub start() {
uword xx = &empty
xx++
}
sub empty() {
}
}
"""
val result = compileText(C64Target(), false, sourcecode)!!
val graph = CallGraph(result.compilerAst)
graph.imports.size shouldBe 1
graph.importedBy.size shouldBe 1
val toplevelModule = result.compilerAst.toplevelModule
val importedModule = graph.imports.getValue(toplevelModule).single()
importedModule.name shouldBe "string"
val importedBy = graph.importedBy.getValue(importedModule).single()
importedBy.name.startsWith("on_the_fly_test") shouldBe true
graph.unused(toplevelModule) shouldBe false
graph.unused(importedModule) shouldBe false
val mainBlock = toplevelModule.statements.filterIsInstance<Block>().single()
val startSub = mainBlock.statements.filterIsInstance<Subroutine>().single{it.name=="start"}
val emptySub = mainBlock.statements.filterIsInstance<Subroutine>().single{it.name=="empty"}
graph.calls shouldNotContainKey startSub
graph.calledBy shouldNotContainKey emptySub
withClue("empty doesn't call anything") {
graph.calls shouldNotContainKey emptySub
}
withClue( "start doesn't get called (except as entrypoint ofc.)") {
graph.calledBy shouldNotContainKey startSub
}
}
test("allIdentifiers separates for different positions of the IdentifierReferences") {
val sourcecode = """
main {
sub start() {
uword x1 = &empty
uword x2 = &empty
empty()
}
sub empty() {
%asm {{
nop
}}
}
}
"""
val result = compileText(C64Target(), false, sourcecode)!!
val graph = CallGraph(result.compilerAst)
graph.allIdentifiers.size shouldBeGreaterThanOrEqual 5
val empties = graph.allIdentifiers.keys.filter { it.nameInSource==listOf("empty") }
empties.size shouldBe 3
empties[0].position.line shouldBe 4
empties[1].position.line shouldBe 5
empties[2].position.line shouldBe 6
}
test("checking block and subroutine names usage in assembly code") {
val source = """
main {
sub start() {
%asm {{
lda #<blockname
lda #<blockname.subroutine
correctlabel:
nop
}}
}
}
blockname {
sub subroutine() {
@(1000) = 0
}
sub correctlabel() {
@(1000) = 0
}
}
; all block and subroutines below should NOT be found in asm because they're only substrings of the names in there
locknam {
sub rout() {
@(1000) = 0
}
sub orrectlab() {
@(1000) = 0
}
}"""
val module = parseModule(SourceCode.Text(source))
val program = Program("test", DummyFunctions, DummyMemsizer, DummyStringEncoder)
program.addModule(module)
val callgraph = CallGraph(program)
val blockMain = program.allBlocks.single { it.name=="main" }
val blockBlockname = program.allBlocks.single { it.name=="blockname" }
val blockLocknam = program.allBlocks.single { it.name=="locknam" }
val subStart = blockMain.statements.filterIsInstance<Subroutine>().single { it.name == "start" }
val subSubroutine = blockBlockname.statements.filterIsInstance<Subroutine>().single { it.name == "subroutine" }
val subCorrectlabel = blockBlockname.statements.filterIsInstance<Subroutine>().single { it.name == "correctlabel" }
val subRout = blockLocknam.statements.filterIsInstance<Subroutine>().single { it.name == "rout" }
val subOrrectlab = blockLocknam.statements.filterIsInstance<Subroutine>().single { it.name == "orrectlab" }
callgraph.unused(blockMain) shouldBe false
callgraph.unused(blockBlockname) shouldBe false
callgraph.unused(blockLocknam) shouldBe true
callgraph.unused(subStart) shouldBe false
callgraph.unused(subSubroutine) shouldBe false
callgraph.unused(subCorrectlabel) shouldBe false
callgraph.unused(subRout) shouldBe true
callgraph.unused(subOrrectlab) shouldBe true
}
test("recursion detection") {
val source="""
main {
sub start() {
recurse1()
}
sub recurse1() {
recurse2()
}
sub recurse2() {
start()
}
}"""
val module = parseModule(SourceCode.Text(source))
val program = Program("test", DummyFunctions, DummyMemsizer, DummyStringEncoder)
program.addModule(module)
val callgraph = CallGraph(program)
val errors = ErrorReporterForTests()
callgraph.checkRecursiveCalls(errors)
errors.errors.size shouldBe 0
errors.warnings.size shouldBe 4
errors.warnings[0] shouldContain "contains recursive subroutines"
}
test("no recursion warning if reference isn't a call") {
val source="""
main {
sub start() {
recurse1()
}
sub recurse1() {
recurse2()
}
sub recurse2() {
uword @shared address = &start
}
}"""
val module = parseModule(SourceCode.Text(source))
val program = Program("test", DummyFunctions, DummyMemsizer, DummyStringEncoder)
program.addModule(module)
val callgraph = CallGraph(program)
val errors = ErrorReporterForTests()
callgraph.checkRecursiveCalls(errors)
errors.errors.size shouldBe 0
errors.warnings.size shouldBe 0
}
test("subs that aren't called but only used as scope aren't unused (6502)") {
val src="""
main {
sub start() {
cx16.r0L = main.scopesub.variable
cx16.r1L = main.scopesub.array[1]
cx16.r0++
}
sub scopesub() {
ubyte variable
ubyte[] array = [1,2,3]
variable++
}
}"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
val result = compileText(C64Target(), true, src, errors=errors)!!
val callgraph = CallGraph(result.compilerAst)
val scopeSub = result.compilerAst.entrypoint.lookup(listOf("main", "scopesub")) as Subroutine
scopeSub.name shouldBe "scopesub"
callgraph.notCalledButReferenced shouldContain scopeSub
callgraph.unused(scopeSub) shouldBe false
errors.warnings.any { "unused" in it } shouldBe false
errors.infos.any { "unused" in it } shouldBe false
}
test("subs that aren't called but only used as scope aren't unused (IR/VM)") {
val src="""
main {
sub start() {
cx16.r0L = main.scopesub.variable
cx16.r1L = main.scopesub.array[1]
cx16.r0++
}
sub scopesub() {
ubyte variable
ubyte[] array = [1,2,3]
variable++
}
}"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
val result = compileText(VMTarget(), true, src, errors=errors)!!
val callgraph = CallGraph(result.compilerAst)
val scopeSub = result.compilerAst.entrypoint.lookup(listOf("main", "scopesub")) as Subroutine
scopeSub.name shouldBe "scopesub"
callgraph.notCalledButReferenced shouldContain scopeSub
callgraph.unused(scopeSub) shouldBe false
errors.warnings.any { "unused" in it } shouldBe false
errors.infos.any { "unused" in it } shouldBe false
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runProgram(virtfile.readText())
}
})