2025-04-19 13:49:03 +02:00

196 lines
6.7 KiB
Kotlin

package prog8tests.compiler.codegeneration
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import prog8.code.ast.PtBlock
import prog8.code.ast.PtVariable
import prog8.code.core.BaseDataType
import prog8.code.core.DataType
import prog8.code.target.C64Target
import prog8.code.target.Cx16Target
import prog8tests.helpers.compileText
import kotlin.io.path.readBytes
import kotlin.io.path.readText
class TestLibrary: FunSpec({
val outputDir = tempdir().toPath()
test("library compilation (x16)") {
val src="""
%address ${'$'}A050
%memtop ${'$'}C000
%output library
%import textio
main {
; Create a jump table as first thing in the library.
uword[] @shared @nosplit jumptable = [
; NOTE: the compiler has inserted a single JMP instruction at the start of the 'main' block, that jumps to the start() routine.
; This is convenient because the rest of the jump table simply follows it,
; making the first jump neatly be the required initialization routine for the library (initializing variables and BSS region).
; btw, $4c = opcode for JMP.
$4c00, &library.func1,
$4c00, &library.func2,
]
sub start() {
; has to be here for initialization (BSS, variables init).
%asm {{
nop
}}
}
}
library {
sub func1() {
cx16.r0L++
}
sub func2() {
cx16.r0L--
}
}"""
val result = compileText(Cx16Target(), true, src, outputDir, writeAssembly = true)!!
val ep = result.codegenAst!!.entrypoint()
val main = ep!!.parent as PtBlock
main.name shouldBe "p8b_main"
val jumptable = main.children[0] as PtVariable
jumptable.name shouldBe "p8v_jumptable"
jumptable.type shouldBe DataType.arrayFor(BaseDataType.UWORD, false)
jumptable.arraySize shouldBe 4u
val bin = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".bin").readBytes().map { it.toUByte() }
val loadAddr = 0xa050u
val startAddr = loadAddr + 0xbu
val func1Addr = loadAddr + 0x10u
val func2Addr = loadAddr + 0x13u
fun msb(addr: UInt) = addr.shr(8).toUByte()
fun lsb(addr: UInt) = addr.and(255u).toUByte()
fun offset(addr: UInt) = (addr-loadAddr).toInt()+0 // no PRG header
// cx16 library output has no PRG header.
// first jump table entry; a JMP to start() inserted by prog8 itself
bin[0] shouldBe 0x4cu // JMP
bin[1] shouldBe lsb(startAddr)
bin[2] shouldBe msb(startAddr)
bin[3] shouldBe 0x00u // 0 padding
// second jump table entry (first array element!)
bin[4] shouldBe 0x4cu // JMP
bin[5] shouldBe lsb(func1Addr)
bin[6] shouldBe msb(func1Addr)
bin[7] shouldBe 0x00u // 0 padding
// third jump table entry (second array element!)
bin[8] shouldBe 0x4cu // JMP
bin[9] shouldBe lsb(func2Addr)
bin[10] shouldBe msb(func2Addr)
bin[11] shouldNotBe 0x00u // no more padding after jump table
// check start() contents
bin[offset(startAddr)] shouldBe 0x20u // JSR (to clear_bss)
bin[offset(startAddr)+3] shouldBe 0xeau // NOP
bin[offset(startAddr)+4] shouldBe 0x60u // RTS
// check func1
bin[offset(func1Addr)] shouldBe 0xe6u // INC
// check func2
bin[offset(func2Addr)] shouldBe 0xC6u // DEC
}
test("library compilation (c64)") {
val src="""
%address ${'$'}8050
%memtop ${'$'}a000
%output library
%import textio
main {
; Create a jump table as first thing in the library.
uword[] @shared @nosplit jumptable = [
; NOTE: the compiler has inserted a single JMP instruction at the start of the 'main' block, that jumps to the start() routine.
; This is convenient because the rest of the jump table simply follows it,
; making the first jump neatly be the required initialization routine for the library (initializing variables and BSS region).
; btw, ${'$'}4c = opcode for JMP.
${'$'}4c00, &library.func1,
${'$'}4c00, &library.func2,
]
sub start() {
; has to be here for initialization (BSS, variables init).
%asm {{
nop
}}
}
}
library {
sub func1() {
cx16.r0L++
}
sub func2() {
cx16.r0L--
}
}"""
val result = compileText(C64Target(), true, src, outputDir, writeAssembly = true)!!
val ep = result.codegenAst!!.entrypoint()
val main = ep!!.parent as PtBlock
main.name shouldBe "p8b_main"
val jumptable = main.children[0] as PtVariable
jumptable.name shouldBe "p8v_jumptable"
jumptable.type shouldBe DataType.arrayFor(BaseDataType.UWORD, false)
jumptable.arraySize shouldBe 4u
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
println(asm)
val bin = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".bin").readBytes().map { it.toUByte() }
val loadAddr = 0x8050u
val startAddr = loadAddr + 0xbu
val func1Addr = loadAddr + 0x10u
val func2Addr = loadAddr + 0x14u
fun msb(addr: UInt) = addr.shr(8).toUByte()
fun lsb(addr: UInt) = addr.and(255u).toUByte()
fun offset(addr: UInt) = (addr-loadAddr).toInt()+2
// PRG header (C64 library output has this)
bin[0] shouldBe lsb(loadAddr)
bin[1] shouldBe msb(loadAddr)
// first jump table entry; a JMP to start() inserted by prog8 itself
bin[2] shouldBe 0x4cu // JMP
bin[3] shouldBe lsb(startAddr)
bin[4] shouldBe msb(startAddr)
bin[5] shouldBe 0x00u // 0 padding
// second jump table entry (first array element!)
bin[6] shouldBe 0x4cu // JMP
bin[7] shouldBe lsb(func1Addr)
bin[8] shouldBe msb(func1Addr)
bin[9] shouldBe 0x00u // 0 padding
// third jump table entry (second array element!)
bin[10] shouldBe 0x4cu // JMP
bin[11] shouldBe lsb(func2Addr)
bin[12] shouldBe msb(func2Addr)
bin[13] shouldNotBe 0x00u // no more padding after jump table
// check start() contents
bin[offset(startAddr)] shouldBe 0x20u // JSR (to clear_bss)
bin[offset(startAddr)+3] shouldBe 0xeau // NOP
bin[offset(startAddr)+4] shouldBe 0x60u // RTS
// check func1
bin[offset(func1Addr)] shouldBe 0xeeu // INC abs
// check func2
bin[offset(func2Addr)] shouldBe 0xCeu // DEC abs
}
})