mirror of
https://github.com/irmen/prog8.git
synced 2026-10-22 05:02:58 +00:00
203 lines
7.9 KiB
Kotlin
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
|
|
}
|
|
})
|