Files
prog8/compiler/test/codegeneration/TestSquareRangeCheck6502.kt
T

203 lines
7.9 KiB
Kotlin

package prog8tests.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.target.Cx16Target
import prog8tests.helpers.compileText
import prog8tests.helpers.simulate
import razorvine.ksim65.testing.TestMachine
import kotlin.io.path.readText
/**
* Tests for the runtime range check that prog8_math.square / prog8_math.square_long do.
*
* The fast path of square (Lee Davison's routine) only computes (abs(x) and 255) squared, so
* it is only valid for -255..255. Prog8 word semantics require the result to wrap/truncate
* mod 2^16, so anything outside that range must produce the same value as the general
* multiply would. The check lives in the routine itself (rather than inline at every call
* site) so that it is paid for in code size only once.
*/
class TestSquareRangeCheck6502 : FunSpec({
val outputDir = tempdir().toPath()
fun assertWord(machine: TestMachine, address: Int, value: Int) {
machine.assertMemory(address, value and 0xff)
machine.assertMemory(address + 1, (value shr 8) and 0xff)
}
fun assertLong(machine: TestMachine, address: Int, value: Long) {
for (i in 0..3)
machine.assertMemory(address + i, ((value shr (i * 8)) and 0xff).toInt())
}
test("uword x*x is correct inside and outside the fast path range") {
val src = $$"""
%option no_sysinit
%launcher none
%address $1000
main {
&ubyte poweroff = $f203
&uword r0 = $02
&uword r1 = $04
&uword r2 = $06
&uword r3 = $08
&uword r4 = $0a
&uword r5 = $0c
&uword r6 = $0e
&uword r7 = $10
&uword r8 = $12
&uword r9 = $14
&uword r10 = $16
&uword r11 = $18
&uword r12 = $1a
sub start() {
uword @shared a0 = 0
r0 = a0*a0
uword @shared a1 = 200
r1 = a1*a1
uword @shared a2 = 255
r2 = a2*a2
; 256: just outside the range, 256*256 wraps to 0
uword @shared a3 = 256
r3 = a3*a3
; 257: 257*257 = 66049, wraps to 513. The unchecked fast path would
; have computed (257 and 255) squared = 1 here.
uword @shared a4 = 257
r4 = a4*a4
; 300: 300*300 = 90000, wraps to 24464 (fast path would give 44*44 = 1936)
uword @shared a5 = 300
r5 = a5*a5
; 1000: wraps to 1000000 and 65536 = 16960
uword @shared a6 = 1000
r6 = a6*a6
; -1: in range, result 1
uword @shared a7 = 65535
r7 = a7*a7
; -255: in range, result 65025
uword @shared a8 = 65281
r8 = a8*a8
; -256: out of range, 65536 wraps to 0
uword @shared a9 = 65280
r9 = a9*a9
; -257: out of range, 66049 wraps to 513
uword @shared a10 = 65279
r10 = a10*a10
; 40000: out of range, wraps to 24464
uword @shared a11 = 40000
r11 = a11*a11
; 65535: out of range, 65535*65535 = 4294836225, mod 65536 = 1
uword @shared a12 = 65535
r12 = a12*a12
poweroff = 1
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, src, outputDir)
result shouldNotBe null
val machine = result!!.simulate()
assertWord(machine, 0x02, 0) // 0
assertWord(machine, 0x04, 40000) // 200
assertWord(machine, 0x06, 65025) // 255
assertWord(machine, 0x08, 0) // 256 -> 0
assertWord(machine, 0x0a, 513) // 257 -> 513
assertWord(machine, 0x0c, 24464) // 300 -> 24464
assertWord(machine, 0x0e, 16960) // 1000 -> 16960
assertWord(machine, 0x10, 1) // -1
assertWord(machine, 0x12, 65025) // -255
assertWord(machine, 0x14, 0) // -256 -> 0
assertWord(machine, 0x16, 513) // -257 -> 513
assertWord(machine, 0x18, 4096) // 40000 -> 4096
assertWord(machine, 0x1a, 1) // 65535 -> 1
}
test("long x*x is correct for values that do and do not fit in a signed word") {
val src = $$"""
%option no_sysinit
%launcher none
%address $1000
main {
&ubyte poweroff = $f203
&long r0 = $02
&long r1 = $06
&long r2 = $0a
&long r3 = $0e
&long r4 = $12
sub start() {
long @shared a0 = 200
r0 = a0*a0
long @shared a1 = 1000
r1 = a1*a1
; 40000 fits in a long but its low word is -25536, so squaring only the
; low word (the fast path) would be wrong; 40000*40000 = 1600000000.
long @shared a2 = 40000
r2 = a2*a2
long @shared a3 = 100000
r3 = a3*a3
long @shared a4 = 1000
r4 = a4*a4
poweroff = 1
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, src, outputDir)
result shouldNotBe null
val machine = result!!.simulate()
assertLong(machine, 0x02, 200L*200) // 40000
assertLong(machine, 0x06, 1000L*1000) // 1000000
assertLong(machine, 0x0a, 40000L*40000) // 1600000000
assertLong(machine, 0x0e, 100000L*100000) // 10000000000
assertLong(machine, 0x12, 1000L*1000) // 1000000
}
test("the range check is emitted in the routine, not inlined at each call site") {
val src = $$"""
%option no_sysinit
%launcher none
%address $1000
main {
sub start() {
uword @shared v = 200
uword @shared r1 = v*v
uword @shared r2 = v*v
uword @shared r3 = v*v
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, src, outputDir)
result shouldNotBe null
val asmFile = result!!.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm")
// normalize runs of spaces so 64tass's column alignment doesn't matter
val lines = asmFile.readText().lines().map { it.trim().replace(Regex("\\s+"), " ") }
// every call site is just a plain jsr, no inline range test
lines.count { it == "jsr prog8_math.square" } shouldBe 3
lines.any { it.contains("squarefast") || it.contains("squareslow") } shouldBe false
// and the check itself exists exactly once, inside each shared routine
val wordStart = lines.indexOfFirst { it == "square .proc" }
(wordStart >= 0) shouldBe true
val wordBody = lines.subList(wordStart, lines.size).takeWhile { !it.startsWith(".pend") }
wordBody.count { it == "jmp multiply_words" } shouldBe 1
val longStart = lines.indexOfFirst { it == "square_long .proc" }
(longStart >= 0) shouldBe true
val longBody = lines.subList(longStart, lines.size).takeWhile { !it.startsWith(".pend") }
longBody.count { it == "jmp multiply_longs" } shouldBe 1
}
})