mirror of
				https://github.com/irmen/prog8.git
				synced 2025-11-03 19:16:13 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			333 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Kotlin
		
	
	
	
	
	
			
		
		
	
	
			333 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Kotlin
		
	
	
	
	
	
package prog8tests.compiler
 | 
						|
 | 
						|
import io.kotest.assertions.withClue
 | 
						|
import io.kotest.core.spec.style.FunSpec
 | 
						|
import io.kotest.engine.spec.tempdir
 | 
						|
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.shouldNotBe
 | 
						|
import io.kotest.matchers.string.shouldContain
 | 
						|
import prog8.ast.Program
 | 
						|
import prog8.ast.statements.Block
 | 
						|
import prog8.ast.statements.Subroutine
 | 
						|
import prog8.code.source.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({
 | 
						|
 | 
						|
    val outputDir = tempdir().toPath()
 | 
						|
 | 
						|
    test("testGraphForEmptySubs") {
 | 
						|
        val sourcecode = """
 | 
						|
            %import conv
 | 
						|
            main {
 | 
						|
                sub start() {
 | 
						|
                }
 | 
						|
                sub empty() {
 | 
						|
                }
 | 
						|
            }
 | 
						|
        """
 | 
						|
        val result = compileText(C64Target(), false, sourcecode, outputDir)!!
 | 
						|
        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 "conv"
 | 
						|
        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{it.name=="main"}
 | 
						|
        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 conv
 | 
						|
            main {
 | 
						|
                sub start() {
 | 
						|
                    uword xx = &empty
 | 
						|
                    xx++
 | 
						|
                }
 | 
						|
                sub empty() {
 | 
						|
                }
 | 
						|
            }
 | 
						|
        """
 | 
						|
        val result = compileText(C64Target(), false, sourcecode, outputDir)!!
 | 
						|
        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 "conv"
 | 
						|
        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{it.name=="main"}
 | 
						|
        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, outputDir)!!
 | 
						|
        val graph = CallGraph(result.compilerAst)
 | 
						|
        graph.allIdentifiers.size shouldBeGreaterThanOrEqual 5
 | 
						|
        val empties = graph.allIdentifiers.filter { it.first.nameInSource==listOf("empty") }
 | 
						|
        println(graph.allIdentifiers)
 | 
						|
        empties.size shouldBe 3
 | 
						|
        empties[0].first.position.line shouldBe 4
 | 
						|
        empties[1].first.position.line shouldBe 5
 | 
						|
        empties[2].first.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, outputDir, 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, outputDir, 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(), false)
 | 
						|
    }
 | 
						|
 | 
						|
    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, outputDir, writeAssembly = true)!!
 | 
						|
        val blocks = result.codegenAst!!.allBlocks().toList()
 | 
						|
        blocks.any { it.name=="xyz" } shouldBe false
 | 
						|
        val result2 = compileText(C64Target(), true, src, outputDir, writeAssembly = true)!!
 | 
						|
        val blocks2 = result2.codegenAst!!.allBlocks().toList()
 | 
						|
        blocks2.any { it.name=="xyz" } shouldBe false
 | 
						|
    }
 | 
						|
 | 
						|
    test("symbol lookup of pointer fields should mark variable as used in callgraph") {
 | 
						|
        val src = """
 | 
						|
main {
 | 
						|
    struct List {
 | 
						|
        ^^uword s
 | 
						|
        ubyte n
 | 
						|
    }
 | 
						|
    sub start() {
 | 
						|
        ^^List l1 = List()
 | 
						|
        l1.s^^ = 2 
 | 
						|
    }
 | 
						|
}"""
 | 
						|
 | 
						|
        compileText(VMTarget(), true, src, outputDir, writeAssembly = true) shouldNotBe null
 | 
						|
    }
 | 
						|
})
 |