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