2024-09-08 14:03:57 +00:00
|
|
|
package prog8tests.compiler
|
2021-10-20 20:16:26 +00:00
|
|
|
|
2021-11-07 23:16:58 +00:00
|
|
|
import io.kotest.assertions.withClue
|
|
|
|
import io.kotest.core.spec.style.FunSpec
|
2024-01-11 21:12:01 +00:00
|
|
|
import io.kotest.matchers.collections.shouldContain
|
2022-01-27 22:32:55 +00:00
|
|
|
import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual
|
2021-11-07 23:16:58 +00:00
|
|
|
import io.kotest.matchers.maps.shouldNotContainKey
|
|
|
|
import io.kotest.matchers.shouldBe
|
2022-02-01 22:09:52 +00:00
|
|
|
import io.kotest.matchers.string.shouldContain
|
2022-01-31 23:24:31 +00:00
|
|
|
import prog8.ast.Program
|
2021-10-20 20:16:26 +00:00
|
|
|
import prog8.ast.statements.Block
|
|
|
|
import prog8.ast.statements.Subroutine
|
2022-05-22 21:11:22 +00:00
|
|
|
import prog8.code.core.SourceCode
|
2022-03-11 19:35:25 +00:00
|
|
|
import prog8.code.target.C64Target
|
2024-01-11 21:12:01 +00:00
|
|
|
import prog8.code.target.VMTarget
|
2022-03-11 19:35:25 +00:00
|
|
|
import prog8.compiler.CallGraph
|
2022-01-31 23:24:31 +00:00
|
|
|
import prog8.parser.Prog8Parser.parseModule
|
2024-01-13 12:55:16 +00:00
|
|
|
import prog8.vm.VmRunner
|
2022-01-31 23:24:31 +00:00
|
|
|
import prog8tests.helpers.*
|
2024-01-13 12:55:16 +00:00
|
|
|
import kotlin.io.path.readText
|
2021-10-20 20:16:26 +00:00
|
|
|
|
2021-11-07 23:16:58 +00:00
|
|
|
class TestCallgraph: FunSpec({
|
|
|
|
test("testGraphForEmptySubs") {
|
2021-10-20 20:16:26 +00:00
|
|
|
val sourcecode = """
|
2024-02-07 22:06:01 +00:00
|
|
|
%import conv
|
2021-10-20 20:16:26 +00:00
|
|
|
main {
|
|
|
|
sub start() {
|
|
|
|
}
|
|
|
|
sub empty() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
"""
|
2022-03-07 20:41:12 +00:00
|
|
|
val result = compileText(C64Target(), false, sourcecode)!!
|
2023-02-09 00:46:23 +00:00
|
|
|
val graph = CallGraph(result.compilerAst)
|
2021-10-20 20:16:26 +00:00
|
|
|
|
2021-11-07 23:16:58 +00:00
|
|
|
graph.imports.size shouldBe 1
|
|
|
|
graph.importedBy.size shouldBe 1
|
2023-02-09 00:46:23 +00:00
|
|
|
val toplevelModule = result.compilerAst.toplevelModule
|
2021-10-20 20:16:26 +00:00
|
|
|
val importedModule = graph.imports.getValue(toplevelModule).single()
|
2024-02-07 22:06:01 +00:00
|
|
|
importedModule.name shouldBe "conv"
|
2021-10-20 20:16:26 +00:00
|
|
|
val importedBy = graph.importedBy.getValue(importedModule).single()
|
2021-11-07 23:16:58 +00:00
|
|
|
importedBy.name.startsWith("on_the_fly_test") shouldBe true
|
2021-10-20 20:16:26 +00:00
|
|
|
|
2021-11-07 23:16:58 +00:00
|
|
|
graph.unused(toplevelModule) shouldBe false
|
|
|
|
graph.unused(importedModule) shouldBe false
|
2021-10-20 20:16:26 +00:00
|
|
|
|
2024-11-05 21:12:25 +00:00
|
|
|
val mainBlock = toplevelModule.statements.filterIsInstance<Block>().single{it.name=="main"}
|
2021-10-20 20:16:26 +00:00
|
|
|
for(stmt in mainBlock.statements) {
|
|
|
|
val sub = stmt as Subroutine
|
2021-11-07 23:16:58 +00:00
|
|
|
graph.calls shouldNotContainKey sub
|
|
|
|
graph.calledBy shouldNotContainKey sub
|
2021-10-20 20:16:26 +00:00
|
|
|
|
2023-02-09 00:46:23 +00:00
|
|
|
if(sub === result.compilerAst.entrypoint)
|
2021-11-07 23:16:58 +00:00
|
|
|
withClue("start() should always be marked as used to avoid having it removed") {
|
|
|
|
graph.unused(sub) shouldBe false
|
|
|
|
}
|
2021-10-20 20:16:26 +00:00
|
|
|
else
|
2021-11-07 23:16:58 +00:00
|
|
|
graph.unused(sub) shouldBe true
|
2021-10-20 20:16:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-01 22:09:52 +00:00
|
|
|
test("reference to empty sub") {
|
2021-10-20 20:16:26 +00:00
|
|
|
val sourcecode = """
|
2024-02-07 22:06:01 +00:00
|
|
|
%import conv
|
2021-10-20 20:16:26 +00:00
|
|
|
main {
|
|
|
|
sub start() {
|
|
|
|
uword xx = &empty
|
|
|
|
xx++
|
|
|
|
}
|
|
|
|
sub empty() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
"""
|
2022-03-07 20:41:12 +00:00
|
|
|
val result = compileText(C64Target(), false, sourcecode)!!
|
2023-02-09 00:46:23 +00:00
|
|
|
val graph = CallGraph(result.compilerAst)
|
2021-10-20 20:16:26 +00:00
|
|
|
|
2021-11-07 23:16:58 +00:00
|
|
|
graph.imports.size shouldBe 1
|
|
|
|
graph.importedBy.size shouldBe 1
|
2023-02-09 00:46:23 +00:00
|
|
|
val toplevelModule = result.compilerAst.toplevelModule
|
2021-10-20 20:16:26 +00:00
|
|
|
val importedModule = graph.imports.getValue(toplevelModule).single()
|
2024-02-07 22:06:01 +00:00
|
|
|
importedModule.name shouldBe "conv"
|
2021-10-20 20:16:26 +00:00
|
|
|
val importedBy = graph.importedBy.getValue(importedModule).single()
|
2021-11-07 23:16:58 +00:00
|
|
|
importedBy.name.startsWith("on_the_fly_test") shouldBe true
|
2021-10-20 20:16:26 +00:00
|
|
|
|
2021-11-07 23:16:58 +00:00
|
|
|
graph.unused(toplevelModule) shouldBe false
|
|
|
|
graph.unused(importedModule) shouldBe false
|
2021-10-20 20:16:26 +00:00
|
|
|
|
2024-11-05 21:12:25 +00:00
|
|
|
val mainBlock = toplevelModule.statements.filterIsInstance<Block>().single{it.name=="main"}
|
2021-10-20 20:16:26 +00:00
|
|
|
val startSub = mainBlock.statements.filterIsInstance<Subroutine>().single{it.name=="start"}
|
|
|
|
val emptySub = mainBlock.statements.filterIsInstance<Subroutine>().single{it.name=="empty"}
|
|
|
|
|
2022-02-01 22:09:52 +00:00
|
|
|
graph.calls shouldNotContainKey startSub
|
|
|
|
graph.calledBy shouldNotContainKey emptySub
|
2021-11-07 23:16:58 +00:00
|
|
|
withClue("empty doesn't call anything") {
|
|
|
|
graph.calls shouldNotContainKey emptySub
|
|
|
|
}
|
|
|
|
withClue( "start doesn't get called (except as entrypoint ofc.)") {
|
|
|
|
graph.calledBy shouldNotContainKey startSub
|
|
|
|
}
|
2021-10-20 20:16:26 +00:00
|
|
|
}
|
2022-01-27 22:32:55 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
"""
|
2022-03-07 20:41:12 +00:00
|
|
|
val result = compileText(C64Target(), false, sourcecode)!!
|
2023-02-09 00:46:23 +00:00
|
|
|
val graph = CallGraph(result.compilerAst)
|
2023-01-20 02:10:41 +00:00
|
|
|
graph.allIdentifiers.size shouldBeGreaterThanOrEqual 5
|
2022-01-27 22:32:55 +00:00
|
|
|
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
|
|
|
|
}
|
2022-01-31 23:24:31 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2022-02-01 22:09:52 +00:00
|
|
|
|
|
|
|
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
|
2022-07-21 20:56:25 +00:00
|
|
|
errors.warnings[0] shouldContain "contains recursive subroutines"
|
2022-02-01 22:09:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2024-01-13 12:55:16 +00:00
|
|
|
|
|
|
|
test("subs that aren't called but only used as scope aren't unused (6502)") {
|
2024-01-11 21:12:01 +00:00
|
|
|
val src="""
|
|
|
|
main {
|
|
|
|
sub start() {
|
|
|
|
cx16.r0L = main.scopesub.variable
|
2024-01-13 12:55:16 +00:00
|
|
|
cx16.r1L = main.scopesub.array[1]
|
2024-01-11 21:12:01 +00:00
|
|
|
cx16.r0++
|
|
|
|
}
|
|
|
|
|
|
|
|
sub scopesub() {
|
|
|
|
ubyte variable
|
|
|
|
ubyte[] array = [1,2,3]
|
|
|
|
|
2024-01-13 12:55:16 +00:00
|
|
|
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]
|
2024-01-11 21:12:01 +00:00
|
|
|
cx16.r0++
|
|
|
|
}
|
2024-01-13 12:55:16 +00:00
|
|
|
|
|
|
|
sub scopesub() {
|
|
|
|
ubyte variable
|
|
|
|
ubyte[] array = [1,2,3]
|
|
|
|
|
|
|
|
variable++
|
|
|
|
}
|
2024-01-11 21:12:01 +00:00
|
|
|
}"""
|
2024-01-13 12:55:16 +00:00
|
|
|
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
|
|
|
val result = compileText(VMTarget(), true, src, errors=errors)!!
|
2024-01-11 21:12:01 +00:00
|
|
|
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
|
2024-01-13 12:55:16 +00:00
|
|
|
|
|
|
|
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())
|
2024-01-11 21:12:01 +00:00
|
|
|
}
|
2024-11-29 23:06:02 +00:00
|
|
|
|
|
|
|
test("also remove subroutines with names matching IR asm instruction") {
|
|
|
|
var src="""
|
|
|
|
main {
|
|
|
|
sub start() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
xyz {
|
|
|
|
uword buffer_ptr = memory("buffers_stack", 8192, 0)
|
|
|
|
|
|
|
|
sub pop() -> ubyte { ; pop is also an IR instruction
|
|
|
|
return buffer_ptr[2]
|
|
|
|
}
|
|
|
|
}"""
|
|
|
|
val result = compileText(VMTarget(), true, src, writeAssembly = true)!!
|
|
|
|
val blocks = result.codegenAst!!.allBlocks().toList()
|
|
|
|
blocks.any { it.name=="xyz" } shouldBe false
|
|
|
|
val result2 = compileText(C64Target(), true, src, writeAssembly = true)!!
|
|
|
|
val blocks2 = result2.codegenAst!!.allBlocks().toList()
|
|
|
|
blocks2.any { it.name=="xyz" } shouldBe false
|
|
|
|
}
|
2021-11-07 23:16:58 +00:00
|
|
|
})
|