mirror of
https://github.com/irmen/prog8.git
synced 2026-10-22 05:02:58 +00:00
598 lines
20 KiB
Kotlin
598 lines
20 KiB
Kotlin
package prog8tests.codegeneration
|
|
|
|
import io.kotest.assertions.throwables.shouldThrow
|
|
import io.kotest.core.spec.style.FunSpec
|
|
import io.kotest.matchers.collections.shouldNotBeEmpty
|
|
import io.kotest.matchers.longs.shouldBeLessThan
|
|
import io.kotest.matchers.shouldBe
|
|
import io.kotest.matchers.shouldNotBe
|
|
import prog8.code.target.Cx16Target
|
|
import prog8tests.helpers.CapturingSerialIO
|
|
import prog8tests.helpers.compileText
|
|
import prog8tests.helpers.simulate
|
|
import razorvine.ksim65.Disassembler
|
|
import java.nio.file.Files
|
|
|
|
|
|
/**
|
|
* These tests execute the compiled 6502 code in the ksim65 simulator.
|
|
* To interface correctly with the simulator, the following directives are required in the Prog8 source:
|
|
*
|
|
* - %option no_sysinit: Prevents the inclusion of the standard system initialization code which is not needed here.
|
|
* - %launcher none: No need for a program launcher.
|
|
* - %address $1000: The program should be loaded at $1000, which is where the simulator starts execution.
|
|
* - You can use memory-mapped variables to easily read their values after execution.
|
|
* - Best practice is to cleanly power off the simulator by writing to the poweroff register at $f203.
|
|
* - The Disassembler class can be used to disassemble the code loaded into the simulator's memory.
|
|
* That is an alternative way to see what assembly code is generated by Prog8. (the usual way is to load the 64tass-generated .asm file)
|
|
* A big benefit of using the Disassembler could be that the resulting assembly source code is much more compact BUT it has NO SYMBOLS WHATSOEVER.
|
|
*
|
|
* Simulator hardware interface registers:
|
|
* - $f000-$f003: Programmable Timer (4 registers).
|
|
* - $f100-$f109: Real Time Clock (10 registers).
|
|
* - $f200: Serial input register (read a byte from it).
|
|
* - $f201: Serial output register (write a byte to output it).
|
|
* - $f202: Reset register (writing any value triggers a CPU reset).
|
|
* - $f203: Power-off register (writing any value stops the simulator).
|
|
*/
|
|
class TestExecution6502 : FunSpec({
|
|
val outputDir = Files.createTempDirectory("prog8test")
|
|
|
|
test("simple addition") {
|
|
val src = $$"""
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
|
|
main {
|
|
&ubyte poweroff = $f203
|
|
&ubyte result = $02 ; result is stored in memory at $02
|
|
sub start() {
|
|
ubyte @shared a = 10
|
|
ubyte @shared b = 20
|
|
result = a + b + 12
|
|
|
|
poweroff = 1 ; power off the simulator
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
|
|
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
|
val machine = compileResult!!.simulate()
|
|
machine.assertMemory(0x02, 42)
|
|
}
|
|
|
|
test("indexed pointer fields in structs") {
|
|
val src = $$"""
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
%zeropage basicsafe
|
|
|
|
main {
|
|
struct Node {
|
|
bool flag
|
|
^^uword uptr
|
|
}
|
|
struct Wrapper {
|
|
bool z
|
|
^^Node node
|
|
}
|
|
^^Node @shared n = [true, 4000]
|
|
^^Node @shared inner = [true, 4500]
|
|
^^Wrapper @shared w = [false, 4000]
|
|
&ubyte poweroff = $f203
|
|
&uword slot_low = $0300
|
|
&uword slot_hi = $0302
|
|
uword index
|
|
|
|
sub start() {
|
|
n.uptr[3] = 9999
|
|
n.uptr[1] = 7777
|
|
n.uptr[9] = 5555
|
|
slot_low = n.uptr[3]
|
|
index = 1
|
|
n.uptr[index] = 1111
|
|
slot_hi = n.uptr[1]
|
|
w.node = inner
|
|
w.node.uptr[2] = 1234
|
|
poweroff = 1
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
|
|
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
|
val machine = compileResult!!.simulate()
|
|
machine.assertMemory(0x0300, 9999 and 0xff)
|
|
machine.assertMemory(0x0301, 9999 shr 8)
|
|
machine.assertMemory(0x0302, 1111 and 0xff)
|
|
machine.assertMemory(0x0303, 1111 shr 8)
|
|
machine.assertMemory(4000+6, 9999 and 0xff)
|
|
machine.assertMemory(4000+7, 9999 shr 8)
|
|
machine.assertMemory(4000+2, 1111 and 0xff)
|
|
machine.assertMemory(4000+3, 1111 shr 8)
|
|
machine.assertMemory(4000+18, 5555 and 0xff)
|
|
machine.assertMemory(4000+19, 5555 shr 8)
|
|
machine.assertMemory(4500+4, 1234 and 0xff)
|
|
machine.assertMemory(4500+5, 1234 shr 8)
|
|
}
|
|
|
|
test("more struct pointer field operations") {
|
|
val src = $$"""
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
%zeropage basicsafe
|
|
|
|
main {
|
|
struct Node {
|
|
bool flag
|
|
^^uword uptr
|
|
}
|
|
^^Node @shared n = [true, 4000]
|
|
^^Node @shared m = [true, 5000]
|
|
^^uword @shared normalptr = 4000
|
|
&ubyte poweroff = $f203
|
|
&uword n_after_swap = $0300
|
|
&uword m_after_swap = $0302
|
|
&uword n_after_add = $0304
|
|
&uword lsb_result = $0306
|
|
&uword msb_result = $0308
|
|
&uword idxinc_result = $030A
|
|
|
|
sub start() {
|
|
swap(n.uptr, m.uptr)
|
|
n_after_swap = n.uptr
|
|
m_after_swap = m.uptr
|
|
n.uptr += 2
|
|
n_after_add = n.uptr
|
|
n.uptr[3] = 100
|
|
n.uptr[3]++
|
|
idxinc_result = n.uptr[3]
|
|
lsb_result = lsb(normalptr)
|
|
msb_result = msb(normalptr)
|
|
poweroff = 1
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
|
|
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
|
val machine = compileResult!!.simulate()
|
|
machine.assertMemory(0x0300, 5000 and 0xff)
|
|
machine.assertMemory(0x0301, 5000 shr 8)
|
|
machine.assertMemory(0x0302, 4000 and 0xff)
|
|
machine.assertMemory(0x0303, 4000 shr 8)
|
|
machine.assertMemory(0x0304, 5004 and 0xff)
|
|
machine.assertMemory(0x0305, 5004 shr 8)
|
|
machine.assertMemory(0x0306, 160)
|
|
machine.assertMemory(0x0308, 15)
|
|
machine.assertMemory(0x030A, 101)
|
|
machine.assertMemory(0x030B, 0)
|
|
machine.assertMemory(5010, 101)
|
|
machine.assertMemory(5011, 0)
|
|
}
|
|
|
|
test("serial output") {
|
|
val src = $$"""
|
|
%encoding iso
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
|
|
main {
|
|
&ubyte poweroff = $f203
|
|
sub start() {
|
|
serial.print("HELLO!")
|
|
poweroff = 1 ; power off the simulator
|
|
}
|
|
}
|
|
|
|
serial {
|
|
&ubyte serial_out = $f201
|
|
|
|
sub print(str message) {
|
|
repeat {
|
|
cx16.r0L = @(message)
|
|
if_z
|
|
return
|
|
serial_out = cx16.r0L
|
|
message++
|
|
}
|
|
}
|
|
}
|
|
|
|
""".trimIndent()
|
|
|
|
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
|
compileResult shouldNotBe null
|
|
|
|
val serialIO = CapturingSerialIO()
|
|
val machine = compileResult!!.simulate(serialAndPower = serialIO)
|
|
serialIO.assertOutput("HELLO!")
|
|
println(machine.cpu.snapshot())
|
|
}
|
|
|
|
test("poweroff register") {
|
|
val src = $$"""
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
main {
|
|
&ubyte poweroff = $f203
|
|
sub start() {
|
|
poweroff = 1 ; power off the simulator
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
|
val machine = compileResult!!.simulate(1000)
|
|
machine.cpu.totalCycles shouldBeLessThan 200
|
|
}
|
|
|
|
test("long repeat loops") {
|
|
val src = $$"""
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
|
|
main {
|
|
&ubyte poweroff = $f203
|
|
&long result = $02 ; result is stored in memory at $02
|
|
|
|
sub start() {
|
|
long @shared qq = 0
|
|
repeat 70000 {
|
|
qq++
|
|
}
|
|
result = qq
|
|
|
|
long @shared iters = 70000
|
|
repeat iters {
|
|
qq++
|
|
}
|
|
result = qq
|
|
|
|
poweroff = 1
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
|
|
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
|
// 140000 iterations * ~40 cycles = 5.6 million. Let's use 10 million to be safe.
|
|
val machine = compileResult!!.simulate(maxCycles = 10000000L)
|
|
machine.assertMemory(0x02, (140000L and 0xff).toInt())
|
|
machine.assertMemory(0x03, ((140000L shr 8) and 0xff).toInt())
|
|
machine.assertMemory(0x04, ((140000L shr 16) and 0xff).toInt())
|
|
machine.assertMemory(0x05, ((140000L shr 24) and 0xff).toInt())
|
|
}
|
|
|
|
test("top level RTS exits simulator") {
|
|
val src = $$"""
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
main {
|
|
sub start() {
|
|
; do nothing just RTS
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
|
val machine = compileResult!!.simulate(1000)
|
|
machine.cpu.totalCycles shouldBeLessThan 250
|
|
}
|
|
|
|
test("reset register") {
|
|
val src = $$"""
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
|
|
main {
|
|
&ubyte reset_reg = $f202
|
|
&ubyte counter = $02
|
|
sub start() {
|
|
counter++
|
|
if counter == 1 {
|
|
reset_reg = 1
|
|
}
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
|
val machine = compileResult!!.simulate()
|
|
machine.assertMemory(0x02, 2)
|
|
}
|
|
|
|
test("real time clock") {
|
|
val src = $$"""
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
|
|
main {
|
|
&ubyte poweroff = $f203
|
|
&word year = $f100
|
|
&ubyte month = $f102
|
|
&ubyte day = $f103
|
|
|
|
&word res_year = $02
|
|
&ubyte res_month = $04
|
|
&ubyte res_day = $05
|
|
|
|
sub start() {
|
|
res_year = year
|
|
res_month = month
|
|
res_day = day
|
|
poweroff = 1 ; power off the simulator
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
|
val machine = compileResult!!.simulate()
|
|
|
|
val now = java.time.LocalDateTime.now()
|
|
machine.assertMemory(0x02, now.year and 0xff)
|
|
machine.assertMemory(0x03, (now.year shr 8) and 0xff)
|
|
machine.assertMemory(0x04, now.monthValue)
|
|
machine.assertMemory(0x05, now.dayOfMonth)
|
|
}
|
|
|
|
test("timer") {
|
|
val src = $$"""
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
|
|
main {
|
|
&ubyte poweroff = $f203
|
|
&ubyte timer_ctrl = $f000
|
|
&ubyte timer_interval_hi = $f003
|
|
&ubyte timer_cnt_lo = $f001
|
|
&ubyte res_lo = $02
|
|
|
|
sub start() {
|
|
timer_interval_hi = $ff
|
|
timer_ctrl = 1 ; enable
|
|
repeat 100 {
|
|
cx16.r0L++ ; some delay
|
|
}
|
|
res_lo = timer_cnt_lo
|
|
poweroff = 1 ; power off the simulator
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
|
val machine = compileResult!!.simulate()
|
|
|
|
machine.assertMemory(0x02, 28)
|
|
}
|
|
|
|
test("serial input") {
|
|
val src = $$"""
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
|
|
main {
|
|
&ubyte poweroff = $f203
|
|
&ubyte serial_in = $f200
|
|
&ubyte res1 = $02
|
|
&ubyte res2 = $03
|
|
|
|
sub start() {
|
|
res1 = serial_in
|
|
res2 = serial_in
|
|
poweroff = 1 ; power off the simulator
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
|
compileResult shouldNotBe null
|
|
|
|
val serialIO = CapturingSerialIO()
|
|
serialIO.provideInput("AB")
|
|
val machine = compileResult!!.simulate(serialAndPower = serialIO)
|
|
|
|
machine.assertMemory(0x02, 'A'.code)
|
|
machine.assertMemory(0x03, 'B'.code)
|
|
}
|
|
|
|
test("disassembler") {
|
|
val src = $$"""
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
|
|
main {
|
|
&ubyte poweroff = $f203
|
|
sub start() {
|
|
ubyte @shared a = 10
|
|
ubyte @shared b = 20
|
|
ubyte c = a + b
|
|
poweroff = 1
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
|
val machine = compileResult!!.simulate(execute = false)
|
|
|
|
val disassembler = Disassembler(machine.cpu)
|
|
val (lines, _) = disassembler.disassemble(machine.ram.data, 0x1000..0x1040, 0x0000)
|
|
|
|
lines.shouldNotBeEmpty()
|
|
// we check if some expected instructions are there
|
|
lines.any { it.contains("lda", ignoreCase = true) } shouldBe true
|
|
lines.any { it.contains("sta", ignoreCase = true) } shouldBe true
|
|
lines.any { it.contains("adc", ignoreCase = true) } shouldBe true
|
|
}
|
|
|
|
test("timeout throws exception") {
|
|
val src = """
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
|
|
main {
|
|
sub start() {
|
|
repeat {
|
|
; infinite loop that isn't just jmp *
|
|
cx16.r0L = 1
|
|
}
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
|
|
val compileResult = compileText(Cx16Target(), true, src, outputDir)
|
|
compileResult shouldNotBe null
|
|
|
|
shouldThrow<AssertionError> {
|
|
compileResult!!.simulate(maxCycles = 100)
|
|
}
|
|
}
|
|
|
|
test("div_longs positive 1234567890 / 88888 = 13889") {
|
|
val src = $$"""
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
|
|
main {
|
|
&ubyte poweroff = $f203
|
|
sub start() {
|
|
long @shared v = 1234567890
|
|
v /= 88888
|
|
%asm {{
|
|
lda p8v_v
|
|
sta $2000
|
|
lda p8v_v+1
|
|
sta $2001
|
|
lda p8v_v+2
|
|
sta $2002
|
|
lda p8v_v+3
|
|
sta $2003
|
|
}}
|
|
poweroff = 1
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
|
val machine = compileResult!!.simulate(maxCycles = 30000)
|
|
val b0 = machine.ram[0x2000].toInt() and 0xff
|
|
val b1 = machine.ram[0x2001].toInt() and 0xff
|
|
val b2 = machine.ram[0x2002].toInt() and 0xff
|
|
val b3 = machine.ram[0x2003].toInt() and 0xff
|
|
println("div_longs result bytes: $b0 $b1 $b2 $b3 (hex: ${b0.toString(16)} ${b1.toString(16)} ${b2.toString(16)} ${b3.toString(16)})")
|
|
machine.assertMemory(0x2000, 0x41)
|
|
machine.assertMemory(0x2001, 0x36)
|
|
machine.assertMemory(0x2002, 0x00)
|
|
machine.assertMemory(0x2003, 0x00)
|
|
}
|
|
|
|
test("double increment/decrement of uword struct field via typed pointer") {
|
|
val src = $$"""
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
|
|
main {
|
|
&ubyte poweroff = $f203
|
|
&uword result = $02
|
|
|
|
struct Sprite {
|
|
ubyte pad
|
|
ubyte pad2
|
|
uword x
|
|
}
|
|
|
|
^^Sprite[4] sprites
|
|
|
|
sub start() {
|
|
^^Sprite sprite = sprites[0]
|
|
|
|
sprite.x = 255
|
|
sprite.x++
|
|
sprite.x++
|
|
result = sprite.x ; expect $0101
|
|
poweroff = 1
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
|
|
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
|
var machine = compileResult!!.simulate()
|
|
machine.assertMemory(0x02, 0x01)
|
|
machine.assertMemory(0x03, 0x01)
|
|
|
|
val src2 = $$"""
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
|
|
main {
|
|
&ubyte poweroff = $f203
|
|
&uword result = $02
|
|
|
|
struct Sprite {
|
|
ubyte pad
|
|
ubyte pad2
|
|
uword x
|
|
}
|
|
|
|
^^Sprite[4] sprites
|
|
|
|
sub start() {
|
|
^^Sprite sprite = sprites[0]
|
|
|
|
sprite.x = 1
|
|
sprite.x--
|
|
sprite.x--
|
|
result = sprite.x ; expect $ffff
|
|
poweroff = 1
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
|
|
val compileResult2 = compileText(Cx16Target(), false, src2, outputDir)
|
|
machine = compileResult2!!.simulate()
|
|
machine.assertMemory(0x02, 0xff)
|
|
machine.assertMemory(0x03, 0xff)
|
|
}
|
|
|
|
test("struct with inlined array memory layout (6502)") {
|
|
val src = $$"""
|
|
%option no_sysinit
|
|
%launcher none
|
|
%address $1000
|
|
|
|
main {
|
|
&ubyte poweroff = $f203
|
|
struct Node {
|
|
ubyte a
|
|
bool flag
|
|
ubyte[5] array
|
|
word number
|
|
}
|
|
|
|
sub start() {
|
|
^^Node k2 = [1, false, [65,66,67,68,0], 9999]
|
|
^^Node k3 = $4000
|
|
k3^^=k2^^
|
|
poweroff = 1
|
|
}
|
|
}
|
|
""".trimIndent()
|
|
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
|
val machine = compileResult!!.simulate()
|
|
machine.assertMemory(0x4000, 1)
|
|
machine.assertMemory(0x4001, 0)
|
|
machine.assertMemory(0x4002, 65)
|
|
machine.assertMemory(0x4003, 66)
|
|
machine.assertMemory(0x4004, 67)
|
|
machine.assertMemory(0x4005, 68)
|
|
machine.assertMemory(0x4006, 0)
|
|
machine.assertMemory(0x4007, 9999 and 0xff)
|
|
machine.assertMemory(0x4008, (9999 shr 8) and 0xff)
|
|
}
|
|
})
|