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.ErrorReporterForTests 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("dynamic byte and word for loops stop before wrap") { val src = $$""" %option no_sysinit %launcher none %address $1000 main { &ubyte poweroff = $f203 &ubyte byteAscending = $02 &ubyte byteDescending = $03 &ubyte byteEmpty = $04 &ubyte wordAscending = $05 &ubyte wordDescending = $06 &ubyte evalOrder = $07 &ubyte orderedCount = $08 &ubyte byteUnsignedDescending = $09 &ubyte wordUnsignedDescending = $0a sub start() { byte ordered for ordered in getFrom() to getTo() step getStep() { orderedCount++ } byte b byte @shared bfrom = 1 byte @shared bto = 5 byte @shared bstep = 2 for b in bfrom to bto step bstep { byteAscending++ } byte @shared bdownFrom = 5 byte @shared bdownTo = 1 byte @shared bdownStep = -2 for b in bdownFrom to bdownTo step bdownStep { byteDescending++ } ubyte ub ubyte @shared ubFrom = 5 ubyte @shared ubTo = 1 byte @shared ubStep = -2 for ub in ubFrom to ubTo step ubStep { byteUnsignedDescending++ } byte @shared zeroStep = 0 for b in bfrom to bto step zeroStep { byteEmpty++ } uword w uword @shared wfrom = 65530 uword @shared wto = 65535 uword @shared wstep = 3 for w in wfrom to wto step wstep { wordAscending++ } word wdown word @shared wdownFrom = 300 word @shared wdownTo = 290 word @shared wdownStep = -4 for wdown in wdownFrom to wdownTo step wdownStep { wordDescending++ } uword uwdown uword @shared uwdownFrom = 65530 uword @shared uwdownTo = 65520 word @shared uwdownStep = -3 for uwdown in uwdownFrom to uwdownTo step uwdownStep { wordUnsignedDescending++ } poweroff = 1 } sub getFrom() -> byte { evalOrder = 1 return 1 } sub getTo() -> byte { evalOrder = evalOrder * 10 + 2 return 5 } sub getStep() -> byte { evalOrder = evalOrder * 10 + 3 return 2 } } """.trimIndent() val compileResult = compileText(Cx16Target(), false, src, outputDir) val machine = compileResult!!.simulate() machine.assertMemory(0x02, 3) machine.assertMemory(0x03, 3) machine.assertMemory(0x04, 0) machine.assertMemory(0x05, 2) machine.assertMemory(0x06, 3) machine.assertMemory(0x07, 123) machine.assertMemory(0x08, 3) machine.assertMemory(0x09, 3) machine.assertMemory(0x0a, 4) } test("signed legacy loop variables stop on overflow with unsigned dynamic steps") { val src = $$""" %option no_sysinit %launcher none %address $1000 main { &ubyte poweroff = $f203 &ubyte byteCount = $0b &ubyte wordCount = $0c sub start() { byte b byte @shared bfrom = 120 byte @shared bto = 127 ubyte @shared bstep = 10 for b in bfrom to bto step bstep { byteCount++ } word w word @shared wfrom = 32760 word @shared wto = 32767 uword @shared wstep = 10 for w in wfrom to wto step wstep { wordCount++ } poweroff = 1 } } """.trimIndent() val compileResult = compileText(Cx16Target(), false, src, outputDir) val machine = compileResult!!.simulate() machine.assertMemory(0x0b, 1) machine.assertMemory(0x0c, 1) } test("dynamic legacy for loop break targets remain scoped") { val src = $$""" %option no_sysinit %launcher none %address $1000 main { &ubyte outerCount = $02 &ubyte poweroff = $f203 sub start() { byte outer byte inner byte @shared outerFrom = 1 byte @shared outerTo = 3 byte @shared outerStep = 1 byte @shared innerFrom = 1 byte @shared innerTo = 3 byte @shared innerStep = 1 for outer in outerFrom to outerTo step outerStep { for inner in innerFrom to innerTo step innerStep { break } outerCount++ break } poweroff = 1 } } """.trimIndent() val errors = ErrorReporterForTests() val compileResult = compileText(Cx16Target(), false, src, outputDir, errors = errors) if (compileResult == null) throw AssertionError(errors.errors.joinToString("\n")) val machine = compileResult.simulate() machine.assertMemory(0x02, 1) } 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 { 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) } test("constant byte store via pointer plus constant offset") { // regression: storing a constant byte to ptr[const] or @(ptr+const) never loaded the // constant into A, so the low byte of the pointer (or other garbage) got stored instead val src = $$""" %option no_sysinit %launcher none %address $1000 main { &ubyte poweroff = $f203 struct Point { uword x ubyte y } sub start() { uword @shared ptr_zp = $2000 uword @shared @nozp ptr_nozp = $2100 ptr_zp[3] = $80 @(ptr_zp+4) = $27 ptr_nozp[5] = $42 @(ptr_nozp+6) = $99 ; the originally reported scenario: field store via a cast pointer, ; which goes through a non-zeropage auto_heap_value temp pointer @shared buffer = $2200 (buffer as ^^Point).x = 160 (buffer as ^^Point).y = 120 poweroff = 1 } } """.trimIndent() for (optimize in listOf(false, true)) { val compileResult = compileText(Cx16Target(), optimize, src, outputDir) val machine = compileResult!!.simulate() machine.assertMemory(0x2003, 0x80) // ptr_zp[3] = $80 machine.assertMemory(0x2004, 0x27) // @(ptr_zp+4) = $27 machine.assertMemory(0x2105, 0x42) // ptr_nozp[5] = $42 machine.assertMemory(0x2106, 0x99) // @(ptr_nozp+6) = $99 machine.assertMemory(0x2200, 160) // Point.x lsb machine.assertMemory(0x2201, 0) // Point.x msb machine.assertMemory(0x2202, 120) // Point.y } } test("union zero-init via memory, write byte and read overlapping word") { val src = $$""" %option no_sysinit %launcher none %address $1000 main { &ubyte poweroff = $f203 &uword result = $02 union U { uword w ubyte b } sub start() { ^^U u = memory("u", sizeof(U), 0) u.b = $42 result = u.w poweroff = 1 } } """.trimIndent() val compileResult = compileText(Cx16Target(), false, src, outputDir) val machine = compileResult!!.simulate() machine.assertMemory(0x02, 0x42) machine.assertMemory(0x03, 0x00) } test("union copy via pointer dereference") { val src = $$""" %option no_sysinit %launcher none %address $1000 main { &ubyte poweroff = $f203 &uword result = $02 union U { uword w ubyte[2] bytes } sub start() { ^^U u1 = memory("u1", sizeof(U), 0) ^^U u2 = memory("u2", sizeof(U), 0) u1.bytes[0] = $78 u1.bytes[1] = $56 u2^^ = u1^^ result = u2.w poweroff = 1 } } """.trimIndent() val compileResult = compileText(Cx16Target(), false, src, outputDir) val machine = compileResult!!.simulate() machine.assertMemory(0x02, 0x78) machine.assertMemory(0x03, 0x56) } test("union in array of instances") { val src = $$""" %option no_sysinit %launcher none %address $1000 main { &ubyte poweroff = $f203 &uword result0 = $02 &uword result1 = $04 union U { uword w ubyte b } U[2] arr sub start() { arr[0].b = $42 arr[1].b = $77 result0 = arr[0].w result1 = arr[1].w poweroff = 1 } } """.trimIndent() val compileResult = compileText(Cx16Target(), false, src, outputDir) val machine = compileResult!!.simulate() machine.assertMemory(0x02, 0x42) machine.assertMemory(0x03, 0x00) machine.assertMemory(0x04, 0x77) machine.assertMemory(0x05, 0x00) } test("lsb/msb/lsw/msw result delivered to a cx16 virtual register parameter") { val src = $$""" %option no_sysinit %launcher none %address $1000 main { &ubyte poweroff = $f203 ; each routine copies the value it received in the @R0 virtual register to a probe address asmsub recordLsb(ubyte c @R0) { %asm {{ lda cx16.r0L sta $3000 rts }} } asmsub recordMsb(ubyte c @R0) { %asm {{ lda cx16.r0L sta $3001 rts }} } asmsub recordLsw(uword c @R0) { %asm {{ lda cx16.r0 sta $3002 lda cx16.r0+1 sta $3003 rts }} } asmsub recordMsw(uword c @R0) { %asm {{ lda cx16.r0 sta $3004 lda cx16.r0+1 sta $3005 rts }} } sub start() { uword @shared l l = $1234 recordLsb(lsb(l)) recordMsb(msb(l)) recordLsw(lsw(l)) recordMsw(msw(l)) poweroff = 1 } } """.trimIndent() val compileResult = compileText(Cx16Target(), false, src, outputDir) val machine = compileResult!!.simulate() machine.assertMemory(0x3000, 0x34) // lsb($1234) machine.assertMemory(0x3001, 0x12) // msb($1234) machine.assertMemory(0x3002, 0x34) // lsw($1234) low machine.assertMemory(0x3003, 0x12) // lsw($1234) high machine.assertMemory(0x3004, 0x00) // msw($1234) low machine.assertMemory(0x3005, 0x00) // msw($1234) high } })