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

673 lines
26 KiB
Kotlin

package prog8tests.codegeneration
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.ints.shouldBeGreaterThan
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import prog8.code.target.Amiga500Target
import prog8.code.target.Qemu68kTarget
import prog8tests.helpers.ErrorReporterForTests
import prog8tests.helpers.compileText
class TestM68k : FunSpec({
val outputDir = tempdir().toPath()
test("void subroutine returning the result of a void function call compiles") {
// Regression test: "return voidfunc(args)" inside a void subroutine is valid Prog8.
// The optimizer only rewrites this to "voidfunc(args); return" when it can inline the
// caller, so in general it reaches the IR codegen as a Return whose value expression is a
// void function call. The m68k/IR backend must translate the call for its side effects and
// then return void instead of dying with 'no IR datatype for undefined'.
val src = """
main {
ubyte flag
sub start() {
flag = 1
benchmark(flag)
}
sub benchmark(ubyte f) {
when f {
1 -> return helper(f)
else -> {
}
}
}
sub helper(ubyte x) {
}
}
"""
compileText(Amiga500Target(), optimize = false, src, outputDir, writeAssembly = true, assemble = false) shouldNotBe null
compileText(Amiga500Target(), optimize = true, src, outputDir, writeAssembly = true, assemble = false) shouldNotBe null
compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = true, assemble = false) shouldNotBe null
compileText(Qemu68kTarget(), optimize = true, src, outputDir, writeAssembly = true, assemble = false) shouldNotBe null
}
test("module sub returning uword computed from a long expression assembles on qemu68k") {
// Regression test for a m68k backend bug: a module-level sub whose return
// type is uword, but whose return expression is a 'long' value cast to uword,
// caused the return-value slot to be sized as a single byte (.b) instead of a
// word. This made the assembler emit invalid 'move.l d0, mod.sub.b' and fail.
val src = """
%zeropage basicsafe
bugrepro {
%option no_symbol_prefixing
sub env_samples(ubyte param) -> uword {
long b = (param as long) * 100000 / 65025
return ((param as long) * b) as uword
}
sub useit(ubyte p) -> uword {
return env_samples(p)
}
}
main {
uword @shared sink
sub start() {
sink = bugrepro.useit(100)
}
}
"""
val result = compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = true, assemble = false)
result shouldNotBe null
}
test("variables whose scoped name ends with vasm size extension assemble on qemu68k") {
// Regression test for vasm size-extension clash: a scoped symbol such as
// "mymod.b" is emitted as a label `mymod.b:` and operands `move.b mymod.b,...`.
// Vasm interprets trailing ".b/.w/.l/.s/.d/.x/.p/.q" as a size extension, yielding
// "bad size extension", "label redefined" and "unknown mnemonic <.b:>" errors.
// The m68k backend must mangle the last dot when the suffix is a size extension.
val src = """
mymod {
%option no_symbol_prefixing
ubyte b
uword w
ubyte l
ubyte s
ubyte d
ubyte x
ubyte p
ubyte q
ubyte w2
ubyte l2
sub use() {
b = 1
w = 1000
l = 2
s = 3
d = 4
x = 5
p = 6
q = 7
w2 = 8
l2 = 9
}
}
main {
sub start() {
mymod.use()
}
}
"""
val result = compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = true, assemble = false)
result shouldNotBe null
// Also verify that the mangled names don't contain a trailing size extension
// and that the assembler didn't treat them as size overrides.
// The raw names would be "mymod.b", "mymod.w", etc.; the emitted asm must use "mymod_b" etc.
val asm = outputDir.toFile().walkTopDown()
.filter { it.extension == "asm" }
.map { it.readText() }
.joinToString("\n")
// there must be no bare label `mymod.b:` (would be mis-parsed as size extension)
// we check the asm lines after fix contain the mangled form
// use lines.any with exact label to avoid dumping whole asm on failure
val lines = asm.lines().map { it.trim() }
// mangled labels should exist
lines.any { it == "mymod_b:" } shouldNotBe false
lines.any { it == "mymod_w:" } shouldNotBe false
lines.any { it == "mymod_l:" } shouldNotBe false
lines.any { it == "mymod_s:" } shouldNotBe false
lines.any { it == "mymod_d:" } shouldNotBe false
lines.any { it == "mymod_x:" } shouldNotBe false
lines.any { it == "mymod_p:" } shouldNotBe false
lines.any { it == "mymod_q:" } shouldNotBe false
// raw size-extension labels must NOT exist (they would trigger vasm errors)
lines.any { it == "mymod.b:" } shouldNotBe true
lines.any { it == "mymod.w:" } shouldNotBe true
lines.any { it == "mymod.l:" } shouldNotBe true
}
test("local variables named after size extensions assemble on qemu68k") {
// Local variable `b` inside a sub becomes scoped name `mymod2.sub.b` which also
// ends with ".b". This is the exact pattern of the original bugrepro (long b local).
val src = """
%zeropage basicsafe
mymod2 {
sub foo(ubyte param) -> uword {
ubyte b = param
uword w = b as uword
ubyte l = b
ubyte s = 1
return w
}
}
main {
uword @shared sink
sub start() {
sink = mymod2.foo(42)
}
}
"""
val result = compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = true, assemble = false)
result shouldNotBe null
}
test("long/pointer type casts and assignments on 32-bit target") {
val src = """
main {
sub start() {
uword @shared uw = 12345
long @shared lg
^^ubyte @shared typedptr
lg = uw
uw = lg as uword
lg = typedptr
typedptr = lg as ^^ubyte
typedptr = uw as ^^ubyte
uw = typedptr as uword
lg = uw as ^^ubyte as long
long @shared lresult = uw + 5
uw = lresult as uword
uw = subRetLong() as uword
lg = subRetUword()
}
sub subRetLong() -> long {
long @shared val_ = 99999
return val_
}
sub subRetUword() -> uword {
uword @shared val_ = 54321
return val_
}
}"""
compileText(Qemu68kTarget(), false, src, outputDir, writeAssembly = false) shouldNotBe null
compileText(Qemu68kTarget(), true, src, outputDir, writeAssembly = false) shouldNotBe null
compileText(Amiga500Target(), false, src, outputDir, writeAssembly = false) shouldNotBe null
compileText(Amiga500Target(), true, src, outputDir, writeAssembly = false) shouldNotBe null
}
test("long/pointer indexing and type errors on 32-bit target") {
val src = """
main {
sub start() {
long @shared lptr
ubyte @shared idx
lptr[10] = 42
lptr[idx] = idx
ubyte @shared a = lptr[10]
ubyte @shared b = lptr[idx]
}
}"""
compileText(Qemu68kTarget(), false, src, outputDir, writeAssembly = false) shouldNotBe null
compileText(Qemu68kTarget(), true, src, outputDir, writeAssembly = false) shouldNotBe null
val srcUword = """
main {
sub start() {
uword @shared uptr
ubyte @shared dummy = uptr[10]
}
}"""
val errorsUword = ErrorReporterForTests()
compileText(Qemu68kTarget(), false, srcUword, outputDir, writeAssembly = false, errors = errorsUword) shouldBe null
errorsUword.errors.any { it.contains("indexing requires an iterable, address long, or pointer variable") } shouldBe true
val srcWord = """
main {
sub start() {
word @shared wptr
ubyte @shared dummy = wptr[10]
}
}"""
val errorsWord = ErrorReporterForTests()
compileText(Qemu68kTarget(), false, srcWord, outputDir, writeAssembly = false, errors = errorsWord) shouldBe null
errorsWord.errors.any { it.contains("indexing requires an iterable, address long, or pointer variable") } shouldBe true
val srcUbyte = """
main {
sub start() {
ubyte @shared bptr
ubyte @shared dummy = bptr[10]
}
}"""
val errorsUbyte = ErrorReporterForTests()
compileText(Qemu68kTarget(), false, srcUbyte, outputDir, writeAssembly = false, errors = errorsUbyte) shouldBe null
errorsUbyte.errors.any { it.contains("indexing requires an iterable, address long, or pointer variable") } shouldBe true
val srcByte = """
main {
sub start() {
byte @shared bptr
ubyte @shared dummy = bptr[10]
}
}"""
val errorsByte = ErrorReporterForTests()
compileText(Qemu68kTarget(), false, srcByte, outputDir, writeAssembly = false, errors = errorsByte) shouldBe null
errorsByte.errors.any { it.contains("indexing requires an iterable, address long, or pointer variable") } shouldBe true
val srcNarrow = """
main {
sub start() {
long @shared lg = 999
uword @shared uw
uw = lg
}
}"""
val errorsNarrow = ErrorReporterForTests()
compileText(Qemu68kTarget(), false, srcNarrow, outputDir, writeAssembly = false, errors = errorsNarrow) shouldBe null
errorsNarrow.errors.any { it.contains("doesn't match target type") } shouldBe true
}
test("large constant index on long pointer compiles on qemu68k") {
val src = """
main {
sub start() {
long @shared lptr
lptr[999999] = 42
ubyte @shared val_ = lptr[999999]
}
}"""
compileText(Qemu68kTarget(), false, src, outputDir, writeAssembly = false) shouldNotBe null
}
test("pointer post-increment fuses to (a0)+ on qemu68k") {
// #3.6: loadm+loadi+incm and loadm+storei+incm on same pointer variable
// should fuse to loadp_inc/storep_inc and lower to m68k (a0)+
val src = """
main {
sub start() {
ubyte[4] @shared src = [1,2,3,4]
ubyte[4] @shared dst = [0,0,0,0]
pointer pSrc = &src
pointer pDst = &dst
ubyte i
for i in 0 to 3 {
@(pDst) = @(pSrc)
pSrc++
pDst++
}
ubyte @shared dummy = dst[0]
}
}"""
// without optimization the peephole is disabled, so no post-inc fusion
val resNoOpt = compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = true, assemble = false)
resNoOpt shouldNotBe null
// with optimization the IR should contain the new post-inc ops
val resOpt = compileText(Qemu68kTarget(), optimize = true, src, outputDir, writeAssembly = true, assemble = false)
resOpt shouldNotBe null
val optimized = resOpt!!
val p8ir = optimized.compilationOptions.outputDir.resolve("${optimized.compilerAst.name}.p8ir").toFile().readText()
// check that the optimized IR uses the fused ops (single type specifier, no second type field)
(p8ir.contains("loadp_inc.b") || p8ir.contains("loadp_inc")) shouldBe true
(p8ir.contains("storep_inc.b") || p8ir.contains("storep_inc")) shouldBe true
// and that the m68k assembly uses post-increment addressing (a0)+
val asm = optimized.compilationOptions.outputDir.resolve("${optimized.compilerAst.name}.asm").toFile().readText()
val lines = asm.lines().map { it.trim() }
lines.any { it.contains("(a0)+") } shouldBe true
// cx16 must still compile (fallback lowers loadp_inc to $22 indirect + inc)
val resCx16 = compileText(prog8.code.target.Cx16Target(), optimize = true, src, outputDir, writeAssembly = true, assemble = false)
resCx16 shouldNotBe null
}
test("struct and union type symbols are emitted for inline asm on m68k") {
// The m68k codegen must emit vasm 'equ' symbols for struct/union sizes and member
// offsets, using the same dotted scoped names as the 64tass-based backends, so that
// inline asm can reference them. Offsets use the packed layout (no alignment padding).
val src = """
main {
struct Sprite {
ubyte flags
uword xpos
ubyte[8] name
}
union Fill {
uword combined
ubyte[2] bytes
}
sub start() {
^^Sprite sp = 4
sp.flags = 1
sp.xpos = 2
^^Fill ff = 6
ff.combined = 3
}
}
"""
for (target in listOf(Amiga500Target(), Qemu68kTarget())) {
for (opt in listOf(false, true)) {
val result = compileText(target, optimize = opt, src, outputDir, writeAssembly = true, assemble = false)
result shouldNotBe null
val asm = result!!.compilationOptions.outputDir.resolve("${result.compilerAst.name}.asm").toFile().readText()
val lines = asm.lines().map { it.trim() }
lines.any { it == "p8b_main.p8t_Sprite_size equ 11" } shouldBe true
lines.any { it == "p8b_main.p8t_Sprite.p8v_flags equ 0" } shouldBe true
lines.any { it == "p8b_main.p8t_Sprite.p8v_xpos equ 1" } shouldBe true
lines.any { it == "p8b_main.p8t_Sprite.p8v_name equ 3" } shouldBe true
lines.any { it == "p8b_main.p8t_Fill_size equ 2" } shouldBe true
lines.any { it == "p8b_main.p8t_Fill.p8v_combined equ 0" } shouldBe true
lines.any { it == "p8b_main.p8t_Fill.p8v_bytes equ 0" } shouldBe true
}
}
}
test("no struct type symbols are emitted when there are no struct definitions") {
val src = """
main {
sub start() {
}
}
"""
val result = compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = true, assemble = false)
result shouldNotBe null
val asm = result!!.compilationOptions.outputDir.resolve("${result.compilerAst.name}.asm").toFile().readText()
val lines = asm.lines().map { it.trim() }
lines.count { it.contains("_size equ") } shouldBe 0
}
test("struct type symbols use unprefixed names in no_symbol_prefixing blocks") {
val src = """
mymod {
%option no_symbol_prefixing
struct Point {
ubyte x
ubyte y
}
^^Point @shared p
sub use() {
p.x = 1
}
}
main {
sub start() {
mymod.use()
}
}
"""
val result = compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = true, assemble = false)
result shouldNotBe null
val asm = result!!.compilationOptions.outputDir.resolve("${result.compilerAst.name}.asm").toFile().readText()
val lines = asm.lines().map { it.trim() }
lines.any { it == "mymod.Point_size equ 2" } shouldBe true
lines.any { it == "mymod.Point.p8v_x equ 0" } shouldBe true
lines.any { it == "mymod.Point.p8v_y equ 1" } shouldBe true
}
test("struct field offsets above 255 are emitted correctly on m68k") {
// Regression test: m68k allows structs up to ARRAY_SIZE_LIMIT (32768) bytes, so
// field offsets can exceed 255. This used to crash in StStruct.getField.
val src = """
main {
struct Big {
ubyte[300] pad
uword val
}
sub start() {
^^Big b = 4000
b.val = 1
}
}
"""
for (target in listOf(Amiga500Target(), Qemu68kTarget())) {
val result = compileText(target, optimize = false, src, outputDir, writeAssembly = true, assemble = false)
result shouldNotBe null
val asm = result!!.compilationOptions.outputDir.resolve("${result.compilerAst.name}.asm").toFile().readText()
val lines = asm.lines().map { it.trim() }
lines.any { it == "p8b_main.p8t_Big_size equ 302" } shouldBe true
lines.any { it == "p8b_main.p8t_Big.p8v_pad equ 0" } shouldBe true
lines.any { it == "p8b_main.p8t_Big.p8v_val equ 300" } shouldBe true
}
}
test("status flag return is saved and register return read first on m68k") {
// Regression test: a multi-assign returning both a status flag and a register value
// used to let the flag extraction (which uses D0 as scratch for ROXL) clobber the
// register return. The register return is now read first, protected by a
// register-neutral PUSHST/POPST (the 68010+ 'move ccr,-(sp)' form).
val src = """
main {
inline asmsub f(ubyte arg @D0) -> bool @Pc, ubyte @D0 {
%asm {{
moveq #42, d0
cmp.b #42, d0
}}
}
sub start() {
bool c
ubyte v
c, v = f(42)
}
}
"""
val result = compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = true, assemble = false)
result shouldNotBe null
val asm = result!!.compilationOptions.outputDir.resolve("${result.compilerAst.name}.asm").toFile().readText()
// scope to the start subroutine region
val start = asm.indexOf("; ---- Subroutine: p8b_main.p8s_start")
val end = asm.indexOf("; ---- Subroutine:", start + 1).let { if (it < 0) asm.length else it }
val lines = asm.substring(start, end).lines().map { it.trim() }
// register-neutral PUSHST/POPST form, no d0 round-trip
lines.count { it == "move ccr,-(sp)" } shouldBe 1
lines.count { it == "move (sp)+,ccr" } shouldBe 1
lines.count { it == "move ccr, d0" } shouldBe 0
lines.count { it == "move.b d0, -(sp)" } shouldBe 0
// the @D0 return is read (loadhr) before the flag extraction's scratch use (roxl)
val loadhrIdx = lines.indexOfFirst { it.startsWith("; loadhr.b") }
val roxlIdx = lines.indexOfFirst { it.contains("roxl") }
loadhrIdx shouldBeGreaterThan -1
roxlIdx shouldBeGreaterThan -1
(loadhrIdx < roxlIdx) shouldBe true
// the loadhr sits inside the PUSHST/POPST sandwich
val pushIdx = lines.indexOfFirst { it == "move ccr,-(sp)" }
val popIdx = lines.indexOfFirst { it == "move (sp)+,ccr" }
(pushIdx < loadhrIdx) shouldBe true
(loadhrIdx < popIdx) shouldBe true
}
test("struct type symbols are emitted in the header before program start") {
val src = """
main {
struct Point {
ubyte x
ubyte y
}
^^Point @shared p
sub start() {
p.x = 1
}
}
"""
val result = compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = true, assemble = false)
result shouldNotBe null
val asm = result!!.compilationOptions.outputDir.resolve("${result.compilerAst.name}.asm").toFile().readText()
val lines = asm.lines().map { it.trim() }
val sizeIdx = lines.indexOfFirst { it == "p8b_main.p8t_Point_size equ 2" }
val startIdx = lines.indexOfFirst { it == "prog8_program_start:" }
sizeIdx shouldBeGreaterThan -1
startIdx shouldBeGreaterThan -1
(sizeIdx < startIdx) shouldBe true
}
test("subroutine calling assembly that overwrites a5 keeps the static convention") {
// A5 is the frame pointer of a framed subroutine. An asmsub that clobbers it without
// preserving it would silently invalidate the caller's frame slots (and its `unlk a5`),
// so such a caller must not be given a stack frame at all.
val src = """
main {
inline asmsub clobberframepointer() {
%asm {{
moveq #1, d0
lea.l some_label(PC),a5
some_label:
rts
}}
}
sub start() {
ubyte localvar
clobberframepointer()
localvar = 1
}
}
"""
val result = compileText(Amiga500Target(), optimize = false, src, outputDir, writeAssembly = true, assemble = false)
result shouldNotBe null
val asm = result!!.compilationOptions.outputDir.resolve("${result.compilerAst.name}.asm").toFile().readText()
val start = asm.indexOf("; ---- Subroutine: p8b_main.p8s_start")
val end = asm.indexOf("; End of subroutine: p8b_main.p8s_start")
val lines = asm.substring(start, end).lines().map { it.trim() }
lines.none { it.startsWith("link a5") } shouldBe true
lines.none { it.startsWith("unlk") } shouldBe true
// the local keeps its static cell, so no frame-relative access at all
lines.any { it.contains("p8b_main.p8s_start.p8v_localvar") } shouldBe true
}
test("subroutine calling assembly that preserves a5 gets a stack frame") {
// the same program, but the asmsub saves and restores a5, so the frame stays valid
val src = """
main {
inline asmsub preserveframepointer() {
%asm {{
move.l a5,-(sp)
moveq #1, d0
lea.l some_label(PC),a5
some_label:
move.l (sp)+,a5
rts
}}
}
sub start() {
ubyte localvar
preserveframepointer()
localvar = 1
}
}
"""
val result = compileText(Amiga500Target(), optimize = false, src, outputDir, writeAssembly = true, assemble = false)
result shouldNotBe null
val asm = result!!.compilationOptions.outputDir.resolve("${result.compilerAst.name}.asm").toFile().readText()
val start = asm.indexOf("; ---- Subroutine: p8b_main.p8s_start")
val end = asm.indexOf("; End of subroutine: p8b_main.p8s_start")
val lines = asm.substring(start, end).lines().map { it.trim() }
lines.count { it.startsWith("link a5") } shouldBe 1
lines.count { it.startsWith("unlk") } shouldBe 1
lines.any { it.contains("-1(a5)") } shouldBe true
}
test("custom.grab_system preserves the frame pointer register a5") {
// grab_system needs a5 to pass the VBR-reading helper to exec.Supervisor(), and A5 is now
// the frame pointer of the calling subroutine, so it must be saved and restored.
val src = """
%import custom
main {
sub start() {
custom.grab_system()
custom.restore_system()
}
}
"""
val result = compileText(Amiga500Target(), optimize = false, src, outputDir, writeAssembly = true, assemble = false)
result shouldNotBe null
val asm = result!!.compilationOptions.outputDir.resolve("${result.compilerAst.name}.asm").toFile().readText()
val grab = asm.substring(asm.indexOf("custom.grab_system:"), asm.indexOf("; End of subroutine: custom.grab_system"))
grab.lines().map { it.substringBefore(';').trim() }.let { lines ->
lines.count { it == "move.l a5,-(sp)" } shouldBe 1
lines.count { it == ".skip: move.l (sp)+,a5" } shouldBe 1
}
}
test("adpcm stereo block decoder preserves the frame pointer register a5") {
// the stereo loop kernel uses A5 as its outer loop counter, but A5 is now the frame pointer
// of every framed Prog8 subroutine, so it must save and restore it around the loop.
val src = """
%import adpcm
main {
pointer inbuf = memory("inbuf", 256, 0)
pointer outbuf = memory("outbuf", 1024, 0)
sub start() {
adpcm.decode_block_stereo(inbuf, outbuf)
}
}
"""
val result = compileText(Amiga500Target(), optimize = false, src, outputDir, writeAssembly = true, assemble = false)
result shouldNotBe null
val asm = result!!.compilationOptions.outputDir.resolve("${result.compilerAst.name}.asm").toFile().readText()
val kernel = asm.substring(
asm.indexOf("p8b_adpcm.p8s_decode_block_stereo_loop:"),
asm.indexOf("; End of subroutine: p8b_adpcm.p8s_decode_block_stereo_loop")
)
kernel.lines().map { it.substringBefore(';').trim() }.let { lines ->
lines.count { it == "move.l a5,-(sp)" } shouldBe 1
lines.count { it == "move.l (sp)+,a5" } shouldBe 1
}
// its Prog8 caller and everything above it keep their stack frames
asm.contains("; ---- Subroutine: p8b_adpcm.p8s_decode_block_stereo ----") shouldBe true
asm.contains("link a5,#") shouldBe true
}
test("containment check on arrays larger than 255 elements uses word-length counter") {
// Regression test: the IR used a byte-length register for array contains syscalls,
// which crashed for lengths > 255 and produced false negatives on VM/m68k targets.
val src = """
main {
bool @shared bfound
bool @shared wfound
sub start() {
ubyte[300] @shared barr
barr[260] = 42
bfound = 42 in barr
word[300] @shared warr
warr[280] = 3000
wfound = 3000 in warr
}
}
"""
val result = compileText(Qemu68kTarget(), optimize = false, src, outputDir, writeAssembly = true, assemble = false)
result shouldNotBe null
val asm = result!!.compilationOptions.outputDir.resolve("${result.compilerAst.name}.asm").toFile().readText()
val lines = asm.lines().map { it.substringBefore(';').trim() }
// The m68k backend must load the length as a word and use dbra (word counter),
// not a single-byte loop counter.
lines.any { it.startsWith("move.w") && it.endsWith(",d1") } shouldBe true
lines.count { it.startsWith("dbra d1,.ac_loop_") } shouldBe 2
}
})