mirror of
https://github.com/irmen/prog8.git
synced 2026-10-24 18:02:40 +00:00
1432 lines
63 KiB
Kotlin
1432 lines
63 KiB
Kotlin
package prog8tests.codegen.m68k
|
|
|
|
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 prog8.code.core.*
|
|
import prog8.code.target.Amiga500Target
|
|
import prog8.code.target.Qemu68kTarget
|
|
import prog8.codegen.m68k.AsmGen
|
|
import prog8.codegen.m68k.optimizeAssembly
|
|
import prog8.intermediate.*
|
|
import prog8tests.helpers.DummyStringEncoder
|
|
import java.nio.file.Path
|
|
import kotlin.io.path.exists
|
|
import kotlin.io.path.readText
|
|
|
|
class TestInstructionSelectionOptimizations : FunSpec({
|
|
|
|
val tempRoot = tempdir().toPath()
|
|
|
|
fun generateAsmChunks(outputDir: Path, chunks: List<IRCodeChunkBase>, target: ICompilationTarget = Qemu68kTarget()): List<String> {
|
|
val options = CompilationOptions.builder(target)
|
|
.output(OutputType.RAW)
|
|
.zeropage(ZeropageType.FLOATSAFE)
|
|
.floats(false)
|
|
.compilerVersion("test")
|
|
.memtopAddress(0xffffu)
|
|
.optimize(true)
|
|
.build()
|
|
val program = IRProgram("test", IRSymbolTable(), options, DummyStringEncoder)
|
|
program.options.outputDir = outputDir
|
|
val sub = IRSubroutine("test.start", emptyList(), emptyList(), Position.DUMMY)
|
|
sub.chunks.addAll(chunks)
|
|
val block = IRBlock("test", false, IRBlock.Options(), Position.DUMMY)
|
|
block.children.add(sub)
|
|
program.blocks.add(block)
|
|
|
|
val output = outputDir.toFile()
|
|
output.deleteRecursively()
|
|
output.mkdirs()
|
|
AsmGen(program, target).generate()
|
|
val asmFile = outputDir.resolve("test.asm")
|
|
check(asmFile.exists()) { "Assembly file not written: $asmFile" }
|
|
return asmFile.readText().lines().map { it.trim() }
|
|
}
|
|
|
|
fun generateAsm(outputDir: Path, instructions: List<IRInstruction>, target: ICompilationTarget = Qemu68kTarget()): List<String> {
|
|
val chunk = IRCodeChunk(null, null)
|
|
chunk.instructions.addAll(instructions)
|
|
return generateAsmChunks(outputDir, listOf(chunk), target)
|
|
}
|
|
|
|
fun hwArg(register: Int, type: IRDataType, slot: Int): CallArgument =
|
|
Calls.argument(register, type, CallLocation.HardwareRegister(CallingConventionSlot(slot)))
|
|
|
|
fun hwResult(register: Int, type: IRDataType, slot: Int): CallResult =
|
|
Calls.result(register, type, CallLocation.HardwareRegister(CallingConventionSlot(slot)))
|
|
|
|
fun flagResult(register: Int, flag: Statusflag): CallResult =
|
|
Calls.result(register, IRDataType.BYTE, CallLocation.StatusFlag(flag))
|
|
|
|
fun callSite(
|
|
label: String,
|
|
arguments: List<CallArgument> = emptyList(),
|
|
results: List<CallResult> = emptyList()
|
|
): CallSite = CallSite(CallTarget.Direct(codeLabel(label)), arguments, results)
|
|
|
|
test("uses quick address adjustments and preserves large offsets") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-offsets"),
|
|
listOf(
|
|
IRInstructions.storeZero(Opcode.STOREZI, IRDataType.FLOAT, IRMemory.indirect(1, 1)),
|
|
IRInstructions.storeZero(Opcode.STOREZI, IRDataType.FLOAT, IRMemory.indirect(1, 65535))
|
|
)
|
|
)
|
|
|
|
lines.count { it == "addq.l #1,a0" } shouldBe 1
|
|
lines.count { it == "adda.l #65535,a0" } shouldBe 1
|
|
}
|
|
|
|
test("uses moveq for zero extension and only representable immediate returns") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-control"),
|
|
listOf(
|
|
IRInstructions.binary(Opcode.EXT, IRDataType.BYTE, 2, 1),
|
|
IRInstructions.binary(Opcode.EXT, IRDataType.WORD, 3, 2),
|
|
IRInstructions.returnImmediate(IRDataType.BYTE, 42),
|
|
IRInstructions.returnImmediate(IRDataType.BYTE, 255)
|
|
)
|
|
)
|
|
|
|
lines.count { it == "moveq #0,d0" } shouldBe 2
|
|
lines.any { it == "moveq #42,d0" } shouldBe true
|
|
lines.any { it == "move.b #255,d0" } shouldBe true
|
|
}
|
|
|
|
test("compares a virtual register directly against memory") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-compare"),
|
|
listOf(IRInstructions.compare(IRDataType.BYTE, 1, 2))
|
|
)
|
|
|
|
lines.any { it == "move.b p8_regfile+0,d0" } shouldBe true
|
|
lines.any { it == "cmp.b p8_regfile+2,d0" } shouldBe true
|
|
lines.any { it == "moveq #0,d0" } shouldBe false
|
|
lines.any { it == "moveq #0,d1" } shouldBe false
|
|
}
|
|
|
|
test("byte modulus takes the remainder via swap without corrupting shifts") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-byte-mod"),
|
|
listOf(
|
|
IRInstructions.binary(Opcode.MODR, IRDataType.BYTE, 1, 2),
|
|
IRInstructions.binary(Opcode.MODSR, IRDataType.BYTE, 3, 4)
|
|
)
|
|
)
|
|
|
|
lines.count { it == "divu.w d1,d0" } shouldBe 1
|
|
lines.count { it == "divs.w d1,d0" } shouldBe 1
|
|
lines.count { it.startsWith("lsr.l") } shouldBe 0
|
|
lines.count { it.startsWith("swap d0") } shouldBe 2
|
|
}
|
|
|
|
test("word and long multiplication use memory sources instead of loading both operands") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-mul-memsrc"),
|
|
listOf(
|
|
IRInstructions.binary(Opcode.MULR, IRDataType.WORD, 1, 2),
|
|
IRInstructions.binary(Opcode.MULR, IRDataType.LONG, 3, 4)
|
|
)
|
|
)
|
|
|
|
lines.count { it == "mulu.w p8_regfile+2,d0" } shouldBe 1
|
|
lines.count { it == "mulu.l p8_regfile+8,d0" } shouldBe 1
|
|
lines.count { it.startsWith("move.w p8_regfile+2,d1") } shouldBe 0
|
|
lines.count { it.startsWith("move.l p8_regfile+8,d1") } shouldBe 0
|
|
}
|
|
|
|
test("indirect jumps and calls use memory-indirect addressing on 68020") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-indirect"),
|
|
listOf(
|
|
IRInstructions.jumpIndirect(1),
|
|
IRInstructions.call(Opcode.CALLI, CallSite(CallTarget.Direct(codeIndirect(2))))
|
|
)
|
|
)
|
|
|
|
lines.count { it.startsWith("jmp ([p8_regfile+") } shouldBe 1
|
|
lines.count { it.startsWith("jsr ([p8_regfile+") } shouldBe 1
|
|
lines.count { it.startsWith("move.l p8_regfile+") && it.endsWith(",a0") } shouldBe 0
|
|
}
|
|
|
|
test("signed word divmod omits redundant zero extension") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-divmod-word"),
|
|
listOf(
|
|
IRInstructions.divmodRegister(Opcode.DIVMODR, IRDataType.WORD, 1, 2),
|
|
IRInstructions.divmodRegister(Opcode.SDIVMODR, IRDataType.WORD, 3, 4)
|
|
)
|
|
)
|
|
|
|
lines.count { it.startsWith("moveq #0,d0") && "clear upper word" in it } shouldBe 1
|
|
lines.count { it.startsWith("ext.l d0") } shouldBe 1
|
|
lines.count { it.startsWith("divu.w p8_regfile+") } shouldBe 1
|
|
lines.count { it.startsWith("divs.w p8_regfile+") } shouldBe 1
|
|
}
|
|
|
|
test("byte and word extraction uses direct register-file loads") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-extract"),
|
|
listOf(
|
|
IRInstructions.binary(Opcode.MSIGB, IRDataType.WORD, 1, 2),
|
|
IRInstructions.binary(Opcode.MSIGB, IRDataType.LONG, 3, 4),
|
|
IRInstructions.binary(Opcode.LSIGB, IRDataType.WORD, 5, 6),
|
|
IRInstructions.binary(Opcode.LSIGB, IRDataType.LONG, 7, 8),
|
|
IRInstructions.binary(Opcode.MSIGW, IRDataType.LONG, 9, 10),
|
|
IRInstructions.binary(Opcode.LSIGW, IRDataType.LONG, 11, 12),
|
|
IRInstructions.binary(Opcode.BSIGB, IRDataType.LONG, 13, 14),
|
|
IRInstructions.binary(Opcode.MIDB, IRDataType.LONG, 15, 16)
|
|
)
|
|
)
|
|
|
|
lines.count { it.startsWith("lsr") } shouldBe 0
|
|
lines.count { it.startsWith("swap") } shouldBe 0
|
|
lines.count { it.startsWith("clr.w") } shouldBe 0
|
|
lines.count { it.startsWith("move.b p8_regfile+") && ",d0" in it } shouldBe 4
|
|
}
|
|
|
|
test("lsb/msb on words and longs use direct byte loads") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-lsb-msb"),
|
|
listOf(
|
|
IRInstructions.binary(Opcode.LSIGB, IRDataType.WORD, 1, 2),
|
|
IRInstructions.binary(Opcode.LSIGB, IRDataType.LONG, 3, 4),
|
|
IRInstructions.binary(Opcode.MSIGB, IRDataType.WORD, 5, 6),
|
|
IRInstructions.binary(Opcode.MSIGB, IRDataType.LONG, 7, 8)
|
|
)
|
|
)
|
|
|
|
lines.count { it.startsWith("lsr") } shouldBe 0
|
|
lines.count { it.startsWith("swap") } shouldBe 0
|
|
lines.count { it.startsWith("move.b p8_regfile+") && ",d0" in it } shouldBe 2
|
|
}
|
|
|
|
test("lsw/msw on longs use direct word loads") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-lsw-msw"),
|
|
listOf(
|
|
IRInstructions.binary(Opcode.LSIGW, IRDataType.LONG, 1, 2),
|
|
IRInstructions.binary(Opcode.MSIGW, IRDataType.LONG, 3, 4)
|
|
)
|
|
)
|
|
|
|
lines.count { it.startsWith("lsr") } shouldBe 0
|
|
lines.count { it.startsWith("swap") } shouldBe 0
|
|
lines.count { it.startsWith("clr.w") } shouldBe 0
|
|
lines.count { it.startsWith("move.w p8_regfile+") && ",d0" in it } shouldBe 1
|
|
}
|
|
|
|
test("lmh uses BSIGB, MIDB, LSIGB") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-lmh"),
|
|
listOf(
|
|
IRInstructions.binary(Opcode.BSIGB, IRDataType.LONG, 1, 2),
|
|
IRInstructions.binary(Opcode.MIDB, IRDataType.LONG, 3, 4),
|
|
IRInstructions.binary(Opcode.LSIGB, IRDataType.LONG, 5, 6)
|
|
)
|
|
)
|
|
|
|
lines.count { it.startsWith("lsr") } shouldBe 0
|
|
lines.count { it.startsWith("move.b p8_regfile+") && ",d0" in it } shouldBe 2
|
|
}
|
|
|
|
test("forwards an immediate load into a following hardware-register call argument") {
|
|
val site = callSite("copper.move", arguments = listOf(hwArg(1, IRDataType.WORD, 10)))
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-call-forward"),
|
|
listOf(
|
|
IRInstructions.load(IRDataType.WORD, 1, 384),
|
|
IRInstructions.call(site)
|
|
)
|
|
)
|
|
|
|
lines.any { it == "move.w #384,p8_regfile+0" } shouldBe false
|
|
lines.any { it == "move.w #384,d0" } shouldBe true
|
|
lines.any { it == "move.w p8_regfile+0,d0" } shouldBe false
|
|
}
|
|
|
|
test("forwards all immediate loads for a multi-argument call") {
|
|
val site = callSite("copper.move", arguments = listOf(hwArg(1, IRDataType.WORD, 10), hwArg(2, IRDataType.WORD, 11)))
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-call-forward-multi"),
|
|
listOf(
|
|
IRInstructions.load(IRDataType.WORD, 1, 384),
|
|
IRInstructions.load(IRDataType.WORD, 2, 1220),
|
|
IRInstructions.call(site)
|
|
)
|
|
)
|
|
|
|
lines.any { it == "move.w #384,d0" } shouldBe true
|
|
lines.any { it == "move.w #1220,d1" } shouldBe true
|
|
lines.any { it == "move.w #384,p8_regfile+0" } shouldBe false
|
|
lines.any { it == "move.w #1220,p8_regfile+2" } shouldBe false
|
|
}
|
|
|
|
test("does not partially forward a multi-argument call") {
|
|
val site = callSite("copper.move", arguments = listOf(hwArg(1, IRDataType.WORD, 10), hwArg(2, IRDataType.WORD, 11)))
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-call-forward-atomic"),
|
|
listOf(
|
|
IRInstructions.load(IRDataType.WORD, 1, 384),
|
|
IRInstructions.call(site)
|
|
)
|
|
)
|
|
|
|
lines.any { it == "move.w #384,d0" } shouldBe true
|
|
lines.any { it == "move.w p8_regfile+0,d0" } shouldBe false
|
|
}
|
|
|
|
test("retains immediate loads for named call arguments") {
|
|
val site = callSite(
|
|
"callee",
|
|
arguments = listOf(Calls.argument(1, IRDataType.BYTE, CallLocation.ParameterMemory("value")))
|
|
)
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-call-named-argument"),
|
|
listOf(
|
|
IRInstructions.load(IRDataType.BYTE, 1, 42),
|
|
IRInstructions.call(site)
|
|
)
|
|
)
|
|
|
|
lines.any { it == "move.b #42,p8_regfile+0" } shouldBe true
|
|
lines.any { it == "move.b p8_regfile+0,callee.value" } shouldBe true
|
|
}
|
|
|
|
test("retains the register-file store when the value is used after the call") {
|
|
val site = callSite("copper.move", arguments = listOf(hwArg(1, IRDataType.WORD, 10)))
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-call-forward-live"),
|
|
listOf(
|
|
IRInstructions.load(IRDataType.WORD, 1, 384),
|
|
IRInstructions.call(site),
|
|
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.WORD, 1, IRMemory.direct("p8b_test.p8v_value"))
|
|
)
|
|
)
|
|
|
|
lines.any { it == "move.w #384,p8_regfile+0" } shouldBe true
|
|
lines.any { it == "move.w #384,d0" } shouldBe true
|
|
}
|
|
|
|
test("uses moveq for forwarded small immediate call arguments") {
|
|
val site = callSite("copper.wait", arguments = listOf(hwArg(1, IRDataType.WORD, 10), hwArg(2, IRDataType.BYTE, 11)))
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-call-forward-moveq"),
|
|
listOf(
|
|
IRInstructions.load(IRDataType.WORD, 1, 100),
|
|
IRInstructions.load(IRDataType.BYTE, 2, 48),
|
|
IRInstructions.call(site)
|
|
)
|
|
)
|
|
|
|
lines.any { it == "moveq #100,d0" } shouldBe true
|
|
lines.any { it == "moveq #48,d1" } shouldBe true
|
|
lines.any { it == "move.w #100,d0" } shouldBe false
|
|
lines.any { it == "move.b #48,d1" } shouldBe false
|
|
}
|
|
|
|
test("maps byte values 128-255 to signed moveq range for forwarded arguments") {
|
|
val site = callSite("copper.wait", arguments = listOf(hwArg(1, IRDataType.BYTE, 10)))
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-call-forward-moveq-signed"),
|
|
listOf(
|
|
IRInstructions.load(IRDataType.BYTE, 1, 200),
|
|
IRInstructions.call(site)
|
|
)
|
|
)
|
|
|
|
lines.any { it == "moveq #-56,d0" } shouldBe true
|
|
lines.any { it == "move.b #200,d0" } shouldBe false
|
|
}
|
|
|
|
test("keeps move.w for forwarded immediates that do not fit moveq range") {
|
|
val site = callSite("copper.move", arguments = listOf(hwArg(1, IRDataType.WORD, 10)))
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-call-forward-nomoveq"),
|
|
listOf(
|
|
IRInstructions.load(IRDataType.WORD, 1, 384),
|
|
IRInstructions.call(site)
|
|
)
|
|
)
|
|
|
|
lines.any { it == "move.w #384,d0" } shouldBe true
|
|
lines.any { it == "moveq #384,d0" } shouldBe false
|
|
}
|
|
|
|
// === Floating-point immediate call-argument forwarding and dead-store removal ===
|
|
|
|
test("forwards an immediate float load into a following FPU hardware-register call argument") {
|
|
val site = callSite("math.func", arguments = listOf(hwArg(1, IRDataType.FLOAT, 25)))
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-call-forward-float"),
|
|
listOf(
|
|
IRInstructions.loadFloat(1, 1.0),
|
|
IRInstructions.call(site)
|
|
)
|
|
)
|
|
|
|
lines.any { it == $$"fmovecr #$32,fp0" } shouldBe true
|
|
lines.any { it == "fmove.s fp0,p8_fregfile+0" } shouldBe false
|
|
lines.any { it == "fmove.s p8_fregfile+0,fp0" } shouldBe false
|
|
}
|
|
|
|
test("forwards an immediate float load into a different FPU register and drops the dead fregfile store") {
|
|
val site = callSite("math.func", arguments = listOf(hwArg(1, IRDataType.FLOAT, 26)))
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-call-forward-float-fp1"),
|
|
listOf(
|
|
IRInstructions.loadFloat(1, 100.0),
|
|
IRInstructions.call(site)
|
|
)
|
|
)
|
|
|
|
lines.any { it == $$"fmovecr #$34,fp1" } shouldBe true
|
|
lines.any { it == "fmove.s fp0,p8_fregfile+0" } shouldBe false
|
|
lines.any { it == "fmove.s p8_fregfile+0,fp1" } shouldBe false
|
|
}
|
|
|
|
test("forwards both integer and float immediate loads for a mixed-argument call") {
|
|
val site = callSite("mixed.func", arguments = listOf(hwArg(1, IRDataType.WORD, 10), hwArg(2, IRDataType.FLOAT, 25)))
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-call-forward-mixed"),
|
|
listOf(
|
|
IRInstructions.load(IRDataType.WORD, 1, 384),
|
|
IRInstructions.loadFloat(2, 1.0),
|
|
IRInstructions.call(site)
|
|
)
|
|
)
|
|
|
|
lines.any { it == "move.w #384,d0" } shouldBe true
|
|
lines.any { it == "move.w #384,p8_regfile+0" } shouldBe false
|
|
lines.any { it == $$"fmovecr #$32,fp0" } shouldBe true
|
|
lines.any { it == "fmove.s fp0,p8_fregfile+0" } shouldBe false
|
|
lines.any { it == "fmove.s p8_fregfile+0,fp0" } shouldBe false
|
|
}
|
|
|
|
test("retains the fregfile store when a forwarded float value is used after the call") {
|
|
val site = callSite("math.func", arguments = listOf(hwArg(1, IRDataType.FLOAT, 25)))
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-call-forward-float-live"),
|
|
listOf(
|
|
IRInstructions.loadFloat(1, 1.0),
|
|
IRInstructions.call(site),
|
|
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.FLOAT, 1, IRMemory.direct("p8b_test.p8v_value"))
|
|
)
|
|
)
|
|
|
|
lines.any { it == "fmove.s fp0,p8_fregfile+0" } shouldBe true
|
|
lines.count { it == "fmove.s p8_fregfile+0,fp0" } shouldBe 1
|
|
}
|
|
|
|
// === sys.memcopy inlining ===
|
|
|
|
fun memcopySite(): CallSite = callSite(
|
|
"sys.memcopy",
|
|
arguments = listOf(
|
|
hwArg(1, IRDataType.LONG, 18),
|
|
hwArg(2, IRDataType.LONG, 19),
|
|
hwArg(3, IRDataType.LONG, 10)
|
|
)
|
|
)
|
|
|
|
fun generateMemcopyAsm(outputDir: Path, srcImm: UInt?, tgtImm: UInt?, countImm: UInt?): List<String> {
|
|
val instructions = mutableListOf<IRInstruction>()
|
|
if (srcImm != null)
|
|
instructions.add(IRInstructions.load(IRDataType.LONG, 1, srcImm.toInt()))
|
|
if (tgtImm != null)
|
|
instructions.add(IRInstructions.load(IRDataType.LONG, 2, tgtImm.toInt()))
|
|
if (countImm != null)
|
|
instructions.add(IRInstructions.load(IRDataType.LONG, 3, countImm.toInt()))
|
|
instructions.add(IRInstructions.call(memcopySite()))
|
|
return generateAsm(outputDir, instructions)
|
|
}
|
|
|
|
test("inlines sys.memcopy for small constant byte counts") {
|
|
val lines = generateMemcopyAsm(tempRoot.resolve("test-m68k-memcopy-byte"), 0x1000u, 0x2000u, 5u)
|
|
|
|
lines.any { it == "bsr sys.memcopy" } shouldBe false
|
|
lines.count { it == "move.b (a0)+,(a1)+" } shouldBe 5
|
|
lines.any { it.endsWith("inline memcopy 5 bytes") } shouldBe true
|
|
}
|
|
|
|
test("inlines sys.memcopy with long moves when both pointers are long-aligned") {
|
|
val lines = generateMemcopyAsm(tempRoot.resolve("test-m68k-memcopy-word"), 0x1000u, 0x2000u, 8u)
|
|
|
|
lines.any { it == "bsr sys.memcopy" } shouldBe false
|
|
lines.count { it == "move.l (a0)+,(a1)+" } shouldBe 2
|
|
lines.count { it == "move.b (a0)+,(a1)+" } shouldBe 0
|
|
}
|
|
|
|
test("uses dbra long loop for long counts above unrolled limit") {
|
|
val lines = generateMemcopyAsm(tempRoot.resolve("test-m68k-memcopy-long-loop"), 0x1000u, 0x2000u, 36u)
|
|
|
|
lines.any { it == "bsr sys.memcopy" } shouldBe false
|
|
lines.count { it == "move.l (a0)+,(a1)+" } shouldBe 1
|
|
lines.any { it.contains("dbra") } shouldBe true
|
|
}
|
|
|
|
test("calls sys.memcopy when long count exceeds inline threshold") {
|
|
val lines = generateMemcopyAsm(tempRoot.resolve("test-m68k-memcopy-long-threshold"), 0x1000u, 0x2000u, 68u)
|
|
|
|
lines.any { it == "bsr sys.memcopy" } shouldBe true
|
|
lines.count { it == "move.l (a0)+,(a1)+" } shouldBe 0
|
|
}
|
|
|
|
test("falls back to byte loop when source pointer is odd") {
|
|
val lines = generateMemcopyAsm(tempRoot.resolve("test-m68k-memcopy-unaligned"), 0x1001u, 0x2000u, 8u)
|
|
|
|
lines.any { it == "bsr sys.memcopy" } shouldBe false
|
|
lines.count { it == "move.b (a0)+,(a1)+" } shouldBe 1
|
|
lines.any { it.contains("dbra") } shouldBe true
|
|
}
|
|
|
|
test("uses dbra byte loop for byte counts above unrolled limit") {
|
|
val lines = generateMemcopyAsm(tempRoot.resolve("test-m68k-memcopy-byte-loop"), 0x1001u, 0x2000u, 12u)
|
|
|
|
lines.any { it == "bsr sys.memcopy" } shouldBe false
|
|
lines.count { it == "move.b (a0)+,(a1)+" } shouldBe 1
|
|
lines.any { it.contains("dbra") } shouldBe true
|
|
}
|
|
|
|
test("calls sys.memcopy when byte count exceeds inline threshold") {
|
|
val lines = generateMemcopyAsm(tempRoot.resolve("test-m68k-memcopy-large"), 0x1000u, 0x2000u, 17u)
|
|
|
|
lines.any { it == "bsr sys.memcopy" } shouldBe true
|
|
lines.count { it == "move.b (a0)+,(a1)+" } shouldBe 0
|
|
}
|
|
|
|
test("calls sys.memcopy when count is not a forwarded immediate") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-memcopy-variable"),
|
|
listOf(
|
|
IRInstructions.load(IRDataType.LONG, 1, 0x1000),
|
|
IRInstructions.load(IRDataType.LONG, 2, 0x2000),
|
|
// no LOAD for r3, count comes from elsewhere (simulated by loading from a made-up value)
|
|
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.LONG, 3, IRMemory.direct("p8b_test.p8v_count")),
|
|
IRInstructions.call(memcopySite())
|
|
)
|
|
)
|
|
|
|
lines.any { it == "bsr sys.memcopy" } shouldBe true
|
|
}
|
|
|
|
test("inlines sys.memcopy zero bytes as no-op") {
|
|
val lines = generateMemcopyAsm(tempRoot.resolve("test-m68k-memcopy-zero"), 0x1000u, 0x2000u, 0u)
|
|
|
|
lines.any { it == "bsr sys.memcopy" } shouldBe false
|
|
lines.count { it == "move.b (a0)+,(a1)+" } shouldBe 0
|
|
}
|
|
|
|
test("does not partially forward a mixed-argument call when a float arg has no immediate load") {
|
|
val site = callSite("mixed.func", arguments = listOf(hwArg(1, IRDataType.WORD, 10), hwArg(2, IRDataType.FLOAT, 25)))
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-call-forward-mixed-atomic"),
|
|
listOf(
|
|
IRInstructions.load(IRDataType.WORD, 1, 384),
|
|
IRInstructions.call(site)
|
|
)
|
|
)
|
|
|
|
lines.any { it == "move.w #384,d0" } shouldBe true
|
|
lines.any { it == "fmove.s p8_fregfile+0,fp0" } shouldBe true
|
|
}
|
|
|
|
test("loads pointers into a0 with movea and without a +0 offset") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-pointer-a0"),
|
|
listOf(
|
|
IRInstructions.storeZero(Opcode.STOREZI, IRDataType.WORD, IRMemory.indirect(1, 1))
|
|
)
|
|
)
|
|
|
|
lines.any { it == "movea.l p8_regfile,a0" } shouldBe true
|
|
lines.any { it.startsWith("move.l p8_regfile") } shouldBe false
|
|
}
|
|
|
|
test("ADDR.P zero-extends a word source") {
|
|
val chunk = IRCodeChunk(null, null)
|
|
chunk.instructions.add(IRInstructions.load(IRDataType.POINTER, 1, 0x10000))
|
|
chunk.instructions.add(IRInstructions.load(IRDataType.WORD, 2, 10))
|
|
chunk.instructions.add(IRInstructions.binary(Opcode.ADDR, IRDataType.POINTER, 1, 2))
|
|
val lines = generateAsmChunks(
|
|
tempRoot.resolve("test-m68k-addr-p-word-src"),
|
|
listOf(chunk)
|
|
)
|
|
lines.any { it == "moveq #0,d0" } shouldBe true
|
|
lines.any { it == "move.w p8_regfile+4,d0" } shouldBe true
|
|
lines.any { it == "add.l d0,p8_regfile+0" } shouldBe true
|
|
lines.any { it == "move.l p8_regfile+4,d0" } shouldBe false
|
|
}
|
|
|
|
test("ADDR.P does not zero-extend a long source") {
|
|
val chunk = IRCodeChunk(null, null)
|
|
chunk.instructions.add(IRInstructions.load(IRDataType.POINTER, 1, 0x10000))
|
|
chunk.instructions.add(IRInstructions.load(IRDataType.LONG, 2, 10))
|
|
chunk.instructions.add(IRInstructions.binary(Opcode.ADDR, IRDataType.POINTER, 1, 2))
|
|
val lines = generateAsmChunks(
|
|
tempRoot.resolve("test-m68k-addr-p-long-src"),
|
|
listOf(chunk)
|
|
)
|
|
lines.any { it == "move.l p8_regfile+4,d0" } shouldBe true
|
|
lines.any { it == "add.l d0,p8_regfile+0" } shouldBe true
|
|
lines.any { it == "moveq #0,d0" } shouldBe false
|
|
}
|
|
|
|
test("SUBR.P zero-extends a word source") {
|
|
val chunk = IRCodeChunk(null, null)
|
|
chunk.instructions.add(IRInstructions.load(IRDataType.POINTER, 1, 0x10000))
|
|
chunk.instructions.add(IRInstructions.load(IRDataType.WORD, 2, 10))
|
|
chunk.instructions.add(IRInstructions.binary(Opcode.SUBR, IRDataType.POINTER, 1, 2))
|
|
val lines = generateAsmChunks(
|
|
tempRoot.resolve("test-m68k-subr-p-word-src"),
|
|
listOf(chunk)
|
|
)
|
|
lines.any { it == "moveq #0,d0" } shouldBe true
|
|
lines.any { it == "move.w p8_regfile+4,d0" } shouldBe true
|
|
lines.any { it == "sub.l d0,p8_regfile+0" } shouldBe true
|
|
lines.any { it == "move.l p8_regfile+4,d0" } shouldBe false
|
|
}
|
|
|
|
test("does not forward across an intervening instruction") {
|
|
val site = callSite("copper.move", arguments = listOf(hwArg(1, IRDataType.WORD, 10)))
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-call-forward-boundary"),
|
|
listOf(
|
|
IRInstructions.load(IRDataType.WORD, 1, 384),
|
|
IRInstructions.simple(Opcode.NOP),
|
|
IRInstructions.call(site)
|
|
)
|
|
)
|
|
|
|
lines.any { it == "move.w #384,d0" } shouldBe true
|
|
lines.any { it == "move.w p8_regfile+0,d0" } shouldBe false
|
|
}
|
|
|
|
test("retains the register-file store when the value is read via a backward jump") {
|
|
val site = callSite("copper.move", arguments = listOf(hwArg(1, IRDataType.WORD, 10)))
|
|
val loopHead = IRCodeChunk("test.start.loop", null)
|
|
loopHead.instructions.add(
|
|
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.WORD, 1, IRMemory.direct("p8b_test.p8v_out"))
|
|
)
|
|
val loopBody = IRCodeChunk(null, null)
|
|
loopBody.instructions.add(IRInstructions.load(IRDataType.WORD, 1, 384))
|
|
loopBody.instructions.add(IRInstructions.call(site))
|
|
loopBody.instructions.add(IRInstructions.jump(codeLabel("test.start.loop")))
|
|
val lines = generateAsmChunks(
|
|
tempRoot.resolve("test-m68k-call-forward-backjump"),
|
|
listOf(loopHead, loopBody)
|
|
)
|
|
|
|
lines.any { it == "move.w #384,d0" } shouldBe true
|
|
lines.any { it == "move.w #384,p8_regfile+0" } shouldBe true
|
|
}
|
|
|
|
test("suppresses the register-file stores inside a loop when the registers are only used by the call") {
|
|
val site = callSite("copper.move", arguments = listOf(hwArg(1, IRDataType.WORD, 10)))
|
|
val loopHead = IRCodeChunk("test.start.loop", null)
|
|
loopHead.instructions.add(IRInstructions.load(IRDataType.WORD, 2, 1))
|
|
val loopBody = IRCodeChunk(null, null)
|
|
loopBody.instructions.add(IRInstructions.load(IRDataType.WORD, 1, 384))
|
|
loopBody.instructions.add(IRInstructions.call(site))
|
|
loopBody.instructions.add(IRInstructions.jump(codeLabel("test.start.loop")))
|
|
val lines = generateAsmChunks(
|
|
tempRoot.resolve("test-m68k-call-forward-loop-singleuse"),
|
|
listOf(loopHead, loopBody)
|
|
)
|
|
|
|
lines.any { it == "move.w #384,d0" } shouldBe true
|
|
lines.any { it == "move.w #384,p8_regfile+0" } shouldBe false
|
|
}
|
|
|
|
test("retains the register-file store when the value is read by a later inline asm chunk") {
|
|
val site = callSite("copper.move", arguments = listOf(hwArg(1, IRDataType.WORD, 10)))
|
|
val chunk = IRCodeChunk(null, null)
|
|
chunk.instructions.add(IRInstructions.load(IRDataType.WORD, 1, 384))
|
|
chunk.instructions.add(IRInstructions.call(site))
|
|
val inlineAsm = IRInlineAsmChunk(null, "move.w p8_regfile+0,d1", false, null)
|
|
val lines = generateAsmChunks(
|
|
tempRoot.resolve("test-m68k-call-forward-inlineasm"),
|
|
listOf(chunk, inlineAsm)
|
|
)
|
|
|
|
lines.any { it == "move.w #384,d0" } shouldBe true
|
|
lines.any { it == "move.w #384,p8_regfile+0" } shouldBe true
|
|
}
|
|
|
|
test("still suppresses the register-file store when an inline asm chunk does not read the register file") {
|
|
val site = callSite("copper.move", arguments = listOf(hwArg(1, IRDataType.WORD, 10)))
|
|
val chunk = IRCodeChunk(null, null)
|
|
chunk.instructions.add(IRInstructions.load(IRDataType.WORD, 1, 384))
|
|
chunk.instructions.add(IRInstructions.call(site))
|
|
val inlineAsm = IRInlineAsmChunk(null, "move.w custom.INTREQR,d1", false, null)
|
|
val lines = generateAsmChunks(
|
|
tempRoot.resolve("test-m68k-call-forward-inlineasm-harmless"),
|
|
listOf(chunk, inlineAsm)
|
|
)
|
|
|
|
lines.any { it == "move.w #384,d0" } shouldBe true
|
|
lines.any { it == "move.w #384,p8_regfile+0" } shouldBe false
|
|
}
|
|
|
|
test("removes jmp to immediately following label") {
|
|
val chunk1 = IRCodeChunk(null, null)
|
|
chunk1.instructions.add(IRInstructions.load(IRDataType.BYTE, 1, 42))
|
|
chunk1.instructions.add(IRInstructions.jump(codeLabel("test.end")))
|
|
val chunk2 = IRCodeChunk("test.end", null)
|
|
chunk2.instructions.add(IRInstructions.returnVoid())
|
|
val lines = generateAsmChunks(
|
|
tempRoot.resolve("test-m68k-jmp-to-next-label"),
|
|
listOf(chunk1, chunk2)
|
|
)
|
|
|
|
lines.any { it.startsWith("bra test.end") } shouldBe false
|
|
lines.any { it == "rts" } shouldBe true
|
|
}
|
|
|
|
test("removes jmp to immediately following label but keeps label on jmp line") {
|
|
val chunk1 = IRCodeChunk(null, null)
|
|
chunk1.instructions.add(IRInstructions.load(IRDataType.BYTE, 1, 42))
|
|
val chunk2 = IRCodeChunk("test.mid", null)
|
|
chunk2.instructions.add(IRInstructions.jump(codeLabel("test.end")))
|
|
val chunk3 = IRCodeChunk("test.end", null)
|
|
chunk3.instructions.add(IRInstructions.returnVoid())
|
|
val lines = generateAsmChunks(
|
|
tempRoot.resolve("test-m68k-jmp-to-next-label-with-label"),
|
|
listOf(chunk1, chunk2, chunk3)
|
|
)
|
|
|
|
lines.any { it.startsWith("bra test.end") } shouldBe false
|
|
lines.any { it.startsWith("test.mid:") } shouldBe true
|
|
lines.any { it == "rts" } shouldBe true
|
|
}
|
|
|
|
test("optimizes bsr+rts to bra (tail call)") {
|
|
// When bsr+rts is followed immediately by the target label, both bsr and rts are removed
|
|
// (optimizeJmpToNextLabel removes the bra that optimizeTailCall created)
|
|
val chunk1 = IRCodeChunk(null, null)
|
|
chunk1.instructions.add(IRInstructions.call(callSite("test.target")))
|
|
chunk1.instructions.add(IRInstructions.returnVoid())
|
|
val chunk2 = IRCodeChunk("test.target", null)
|
|
chunk2.instructions.add(IRInstructions.returnVoid())
|
|
val lines = generateAsmChunks(
|
|
tempRoot.resolve("test-m68k-tail-call"),
|
|
listOf(chunk1, chunk2)
|
|
)
|
|
|
|
lines.any { it.startsWith("bsr test.target") } shouldBe false
|
|
// The bra is removed by optimizeJmpToNextLabel because test.target: immediately follows
|
|
lines.any { it.startsWith("bra test.target") } shouldBe false
|
|
// Count rts only in the test subroutine (after "test.start:"), not in startup code
|
|
val testStartIdx = lines.indexOfFirst { it.startsWith("test.start:") }
|
|
val testLines = lines.drop(testStartIdx)
|
|
testLines.count { it == "rts" } shouldBe 1
|
|
}
|
|
|
|
test("optimizes bsr+rts to bra when target is not immediately following") {
|
|
// When there's code between the bsr+rts and the target label, the bra is kept
|
|
val chunk1 = IRCodeChunk(null, null)
|
|
chunk1.instructions.add(IRInstructions.call(callSite("test.target")))
|
|
chunk1.instructions.add(IRInstructions.returnVoid())
|
|
val chunk2 = IRCodeChunk(null, null)
|
|
chunk2.instructions.add(IRInstructions.load(IRDataType.BYTE, 1, 99))
|
|
val chunk3 = IRCodeChunk("test.target", null)
|
|
chunk3.instructions.add(IRInstructions.returnVoid())
|
|
val lines = generateAsmChunks(
|
|
tempRoot.resolve("test-m68k-tail-call-with-gap"),
|
|
listOf(chunk1, chunk2, chunk3)
|
|
)
|
|
|
|
lines.any { it.startsWith("bsr test.target") } shouldBe false
|
|
lines.any { it.startsWith("bra test.target") } shouldBe true
|
|
// Count rts only in the test subroutine
|
|
val testStartIdx = lines.indexOfFirst { it.startsWith("test.start:") }
|
|
val testLines = lines.drop(testStartIdx)
|
|
testLines.count { it == "rts" } shouldBe 1
|
|
}
|
|
|
|
test("optimizes bsr+rts to bra but keeps label on bsr line") {
|
|
val chunk1 = IRCodeChunk(null, null)
|
|
chunk1.instructions.add(IRInstructions.load(IRDataType.BYTE, 1, 42))
|
|
val chunk2 = IRCodeChunk("test.caller", null)
|
|
chunk2.instructions.add(IRInstructions.call(callSite("test.target")))
|
|
chunk2.instructions.add(IRInstructions.returnVoid())
|
|
val chunk3 = IRCodeChunk(null, null)
|
|
chunk3.instructions.add(IRInstructions.load(IRDataType.BYTE, 2, 99))
|
|
val chunk4 = IRCodeChunk("test.target", null)
|
|
chunk4.instructions.add(IRInstructions.returnVoid())
|
|
val lines = generateAsmChunks(
|
|
tempRoot.resolve("test-m68k-tail-call-with-label"),
|
|
listOf(chunk1, chunk2, chunk3, chunk4)
|
|
)
|
|
|
|
lines.any { it.startsWith("bsr test.target") } shouldBe false
|
|
lines.any { it.startsWith("bra test.target") } shouldBe true
|
|
lines.any { it.startsWith("test.caller:") } shouldBe true
|
|
// Count rts only in the test subroutine
|
|
val testStartIdx = lines.indexOfFirst { it.startsWith("test.start:") }
|
|
val testLines = lines.drop(testStartIdx)
|
|
testLines.count { it == "rts" } shouldBe 1
|
|
}
|
|
|
|
test("removes redundant tst after move to same location") {
|
|
// Test the optimizer directly on raw assembly lines
|
|
val lines = mutableListOf(
|
|
" move.b d0, p8_regfile+198",
|
|
" tst.b p8_regfile+198",
|
|
" beq somewhere"
|
|
)
|
|
optimizeAssembly(lines)
|
|
lines.any { it.contains("tst.b") } shouldBe false
|
|
lines.any { it.contains("move.b") } shouldBe true
|
|
lines.any { it.contains("beq") } shouldBe true
|
|
}
|
|
|
|
test("removes redundant tst after move with label on move line") {
|
|
val lines = mutableListOf(
|
|
"mylabel:",
|
|
" move.w d0, p8_regfile+200",
|
|
" tst.w p8_regfile+200",
|
|
" bne somewhere"
|
|
)
|
|
optimizeAssembly(lines)
|
|
// tst should be removed, label should stay on move line
|
|
lines.any { it.contains("tst.w") } shouldBe false
|
|
lines.any { it.contains("mylabel:") } shouldBe true
|
|
lines.any { it.contains("move.w") } shouldBe true
|
|
}
|
|
|
|
test("does not remove tst when target is different from move destination") {
|
|
val lines = mutableListOf(
|
|
" move.b d0, p8_regfile+198",
|
|
" tst.b p8_regfile+200",
|
|
" beq somewhere"
|
|
)
|
|
optimizeAssembly(lines)
|
|
// tst should NOT be removed because it tests a different location
|
|
lines.any { it.contains("tst.b") } shouldBe true
|
|
}
|
|
|
|
test("does not remove tst when size differs from move") {
|
|
val lines = mutableListOf(
|
|
" move.b d0, p8_regfile+198",
|
|
" tst.w p8_regfile+198",
|
|
" beq somewhere"
|
|
)
|
|
optimizeAssembly(lines)
|
|
// tst should NOT be removed because sizes differ
|
|
lines.any { it.contains("tst.w") } shouldBe true
|
|
}
|
|
|
|
// === Tests for status flag return handling fixes (commit 1286b6cc8 follow-up) ===
|
|
|
|
test("single-return expression call with @A0 (slot 18) emits store instruction") {
|
|
// Bug fix: single-return expression calls with @A0-A6/@FP0-FP7 returns
|
|
// should emit the store instruction because the IR doesn't generate LOADHR
|
|
// for single-return calls.
|
|
val site = callSite("test.asmsub", results = listOf(hwResult(0, IRDataType.POINTER, 18)))
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-single-return-a0"),
|
|
listOf(IRInstructions.call(site))
|
|
)
|
|
// Should emit a store from a0 to virtual register
|
|
lines.any { it.contains("move.l") && it.contains("a0") && it.contains("p8_regfile") } shouldBe true
|
|
}
|
|
|
|
test("single-return expression call with @FP0 (slot 25) emits store instruction") {
|
|
// Bug fix: single-return expression calls with float register returns
|
|
// should emit the store instruction.
|
|
val site = callSite("test.asmsub", results = listOf(hwResult(0, IRDataType.FLOAT, 25)))
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-single-return-fp0"),
|
|
listOf(IRInstructions.call(site))
|
|
)
|
|
// Should emit fmove.s from fp0 to virtual register
|
|
lines.any { it.contains("fmove.s") && it.contains("fp0") } shouldBe true
|
|
}
|
|
|
|
test("multi-assign with @D0 + @Pz does not emit move between jsr and branch") {
|
|
// Bug fix: in multi-assign context, slot returns should be skipped because
|
|
// the IR generates LOADHR for them. Emitting move here would clobber CPU flags
|
|
// before the IR's branch pattern can read them.
|
|
val site = callSite(
|
|
"test.asmsub",
|
|
results = listOf(hwResult(0, IRDataType.BYTE, 10), flagResult(1, Statusflag.Pz))
|
|
)
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-multi-assign-d0-pz"),
|
|
listOf(IRInstructions.call(site))
|
|
)
|
|
// Find the bsr line
|
|
val jsrIndex = lines.indexOfFirst { it.startsWith("bsr") && it.contains("test.asmsub") }
|
|
jsrIndex shouldBeGreaterThan -1
|
|
// There should be NO move from d0 between jsr and any branch instruction (beq/bne)
|
|
val afterJsr = lines.subList(jsrIndex + 1, lines.size)
|
|
val branchIndex = afterJsr.indexOfFirst { it.startsWith("beq") || it.startsWith("bne") || it.startsWith("bmi") || it.startsWith("bpl") }
|
|
if (branchIndex > 0) {
|
|
val betweenJsrAndBranch = afterJsr.subList(0, branchIndex)
|
|
betweenJsrAndBranch.any { it.contains("move") && it.contains("d0") && it.contains("p8_regfile") } shouldBe false
|
|
}
|
|
}
|
|
|
|
test("CALLFAR multi-assign with slot + flag return does not emit move between jsr and branch") {
|
|
// Bug fix: CALLFAR in multi-assign context should also skip slot returns.
|
|
// CALLFAR only works for amiga targets, so we need a separate helper.
|
|
// For amiga CALLFAR the target is an automatic library call: library number 1 (exec.library)
|
|
// and a negative LVO offset into its jump table.
|
|
val callfarSite = CallSite(
|
|
CallTarget.AmigaLibrary(library = 1, lvo = -30, name = "test.libfunc"),
|
|
results = listOf(hwResult(0, IRDataType.POINTER, 18), flagResult(1, Statusflag.Pz))
|
|
)
|
|
val callfarInsn = IRInstructions.call(Opcode.CALLFAR, callfarSite)
|
|
|
|
val target = Amiga500Target()
|
|
val options = CompilationOptions.builder(target)
|
|
.output(OutputType.RAW)
|
|
.zeropage(ZeropageType.FLOATSAFE)
|
|
.floats(false)
|
|
.compilerVersion("test")
|
|
.memtopAddress(0xffffu)
|
|
.optimize(true)
|
|
.build()
|
|
val program = IRProgram("test", IRSymbolTable(), options, DummyStringEncoder)
|
|
program.options.outputDir = tempRoot.resolve("test-m68k-callfar-multi")
|
|
val sub = IRSubroutine("test.start", emptyList(), emptyList(), Position.DUMMY)
|
|
val chunk = IRCodeChunk(null, null)
|
|
chunk.instructions.add(callfarInsn)
|
|
sub.chunks.add(chunk)
|
|
val block = IRBlock("test", false, IRBlock.Options(), Position.DUMMY)
|
|
block.children.add(sub)
|
|
program.blocks.add(block)
|
|
|
|
val output = program.options.outputDir.toFile()
|
|
output.deleteRecursively()
|
|
output.mkdirs()
|
|
AsmGen(program, target).generate()
|
|
val asmFile = program.options.outputDir.resolve("test.asm")
|
|
check(asmFile.exists()) { "Assembly file not written: $asmFile" }
|
|
val lines = asmFile.readText().lines().map { it.trim() }
|
|
|
|
// Find the jsr line
|
|
val jsrIndex = lines.indexOfFirst { it.startsWith("jsr") }
|
|
jsrIndex shouldBeGreaterThan -1
|
|
// There should be NO move from a0 between jsr and any branch instruction
|
|
val afterJsr = lines.subList(jsrIndex + 1, lines.size)
|
|
val branchIndex = afterJsr.indexOfFirst { it.startsWith("beq") || it.startsWith("bne") }
|
|
if (branchIndex > 0) {
|
|
val betweenJsrAndBranch = afterJsr.subList(0, branchIndex)
|
|
betweenJsrAndBranch.any { it.contains("move") && it.contains("a0") && it.contains("p8_regfile") } shouldBe false
|
|
}
|
|
}
|
|
|
|
test("dbra via IRLoopChunk for repeat word loop 1000") {
|
|
val body = IRCodeChunk(null, null).also { it += IRInstructions.memoryOp(Opcode.INCM, IRDataType.WORD, IRMemory.direct("p8b_main.p8v_sum")) }
|
|
val loop = IRLoopChunk("p8_label_gen_2", 1000, mutableListOf(body))
|
|
val lines = generateAsmChunks(tempRoot, listOf(loop))
|
|
lines.any { it.contains("move.w #999,d7") } shouldBe true
|
|
lines.any { it.contains("dbra d7,p8_label_gen_2") } shouldBe true
|
|
}
|
|
|
|
test("dbra via IRLoopChunk for repeat byte loop 100") {
|
|
val body = IRCodeChunk(null, null).also { it += IRInstructions.memoryOp(Opcode.INCM, IRDataType.WORD, IRMemory.direct("p8b_main.p8v_sum")) }
|
|
val loop = IRLoopChunk("p8_label_gen_1", 100, mutableListOf(body))
|
|
val lines = generateAsmChunks(tempRoot, listOf(loop))
|
|
lines.any { it.contains("move.w #99,d7") } shouldBe true
|
|
lines.any { it.contains("dbra d7,p8_label_gen_1") } shouldBe true
|
|
}
|
|
|
|
test("dbra via IRLoopChunk for repeat 256 special case") {
|
|
val body = IRCodeChunk(null, null).also { it += IRInstructions.memoryOp(Opcode.INCM, IRDataType.WORD, IRMemory.direct("p8b_main.p8v_sum")) }
|
|
val loop = IRLoopChunk("p8_label_gen_1", 256, mutableListOf(body))
|
|
val lines = generateAsmChunks(tempRoot, listOf(loop))
|
|
lines.any { it.contains("move.w #255,d7") } shouldBe true
|
|
lines.any { it.contains("dbra d7,p8_label_gen_1") } shouldBe true
|
|
}
|
|
|
|
test("dbra peephole does not trigger when body uses d7") {
|
|
val lines = mutableListOf(
|
|
" move.w #100,p8_regfile+4",
|
|
"p8_label_gen_2:",
|
|
" move.w d7,p8b_main.p8v_sum",
|
|
" subq.w #1,p8_regfile+4",
|
|
" bne p8_label_gen_2"
|
|
)
|
|
optimizeAssembly(lines)
|
|
lines.any { it.contains("dbra") } shouldBe false
|
|
}
|
|
|
|
test("dbra peephole does not trigger when body contains bsr call") {
|
|
val lines = mutableListOf(
|
|
" move.w #100,p8_regfile+4",
|
|
"p8_label_gen_2:",
|
|
" bsr some_sub",
|
|
" subq.w #1,p8_regfile+4",
|
|
" bne p8_label_gen_2"
|
|
)
|
|
optimizeAssembly(lines)
|
|
lines.any { it.contains("dbra") } shouldBe false
|
|
}
|
|
|
|
test("dbra via IRLoopChunk preserves label on init line") {
|
|
val body = IRCodeChunk(null, null).also { it += IRInstructions.memoryOp(Opcode.INCM, IRDataType.WORD, IRMemory.direct("p8b_main.p8v_sum")) }
|
|
val outerChunk = IRCodeChunk("outer", null)
|
|
val loop = IRLoopChunk("p8_label_gen_2", 100, mutableListOf(body))
|
|
val lines = generateAsmChunks(tempRoot, listOf(outerChunk, loop))
|
|
lines.any { it.contains("outer:") } shouldBe true
|
|
lines.any { it.contains("dbra d7,p8_label_gen_2") } shouldBe true
|
|
lines.any { it.contains("move.w #99,d7") } shouldBe true
|
|
}
|
|
|
|
test("dbra via IRLoopChunk preserves label on loop") {
|
|
val body = IRCodeChunk(null, null).also { it += IRInstructions.memoryOp(Opcode.INCM, IRDataType.WORD, IRMemory.direct("p8b_main.p8v_sum")) }
|
|
val loop = IRLoopChunk("p8_label_gen_2", 100, mutableListOf(body))
|
|
val lines = generateAsmChunks(tempRoot, listOf(loop))
|
|
lines.any { it.contains("p8_label_gen_2:") } shouldBe true
|
|
lines.any { it.contains("dbra d7,p8_label_gen_2") } shouldBe true
|
|
}
|
|
|
|
test("IRLoopChunk saves d7 around sqrt helper call") {
|
|
val body = IRCodeChunk(null, null).also {
|
|
it += IRInstructions.binary(Opcode.SQRT, IRDataType.BYTE, 1, 2)
|
|
}
|
|
val loop = IRLoopChunk("p8_label_gen_2", 5, mutableListOf(body))
|
|
val lines = generateAsmChunks(tempRoot, listOf(loop))
|
|
lines.any { it.contains("dbra d7,p8_label_gen_2") } shouldBe true
|
|
lines.any { it.contains("move.w d7,-(sp)") } shouldBe true
|
|
lines.any { it.contains("move.w (sp)+,d7") } shouldBe true
|
|
}
|
|
|
|
test("IRLoopChunk saves d7 around long multiply helper on 68000") {
|
|
val target = Amiga500Target()
|
|
val options = CompilationOptions.builder(target)
|
|
.output(OutputType.RAW)
|
|
.zeropage(ZeropageType.FLOATSAFE)
|
|
.floats(false)
|
|
.compilerVersion("test")
|
|
.memtopAddress(0xffffu)
|
|
.optimize(true)
|
|
.build()
|
|
val program = IRProgram("test", IRSymbolTable(), options, DummyStringEncoder)
|
|
val outputDir = tempRoot.resolve("test-m68k-loop-longmul")
|
|
program.options.outputDir = outputDir
|
|
val sub = IRSubroutine("test.start", emptyList(), emptyList(), Position.DUMMY)
|
|
val body = IRCodeChunk(null, null).also {
|
|
it += IRInstructions.binaryImmediate(Opcode.MUL, IRDataType.LONG, 1, 3)
|
|
}
|
|
sub.chunks.add(IRLoopChunk("p8_label_gen_2", 5, mutableListOf(body)))
|
|
val block = IRBlock("test", false, IRBlock.Options(), Position.DUMMY)
|
|
block.children.add(sub)
|
|
program.blocks.add(block)
|
|
|
|
outputDir.toFile().deleteRecursively()
|
|
outputDir.toFile().mkdirs()
|
|
AsmGen(program, target).generate()
|
|
val asmFile = outputDir.resolve("test.asm")
|
|
val lines = asmFile.readText().lines().map { it.trim() }
|
|
lines.any { it.contains("bsr p8_umult32") } shouldBe true
|
|
lines.any { it.contains("move.w d7,-(sp)") } shouldBe true
|
|
lines.any { it.contains("move.w (sp)+,d7") } shouldBe true
|
|
}
|
|
|
|
test("dbra peephole does not trigger when body reads the counter slot") {
|
|
val lines = mutableListOf(
|
|
" move.w #100,p8_regfile+4",
|
|
"p8_label_gen_2:",
|
|
" move.w p8_regfile+4,d0",
|
|
" subq.w #1,p8_regfile+4",
|
|
" bne p8_label_gen_2"
|
|
)
|
|
optimizeAssembly(lines)
|
|
lines.any { it.contains("dbra") } shouldBe false
|
|
lines.any { it.contains("move.w p8_regfile+4,d0") } shouldBe true
|
|
}
|
|
|
|
// === on..goto / on..call dispatch fusion (LOADX+JUMPI/CALLI) ===
|
|
|
|
test("fuses LOADX+JUMPI on qemu68k (68020 memory-indirect jmp)") {
|
|
val lines = mutableListOf(
|
|
" lea mytable,a0",
|
|
" move.l (a0,d0.w),d0",
|
|
" move.l d0,p8_regfile+6",
|
|
" jmp ([p8_regfile+6])"
|
|
)
|
|
optimizeAssembly(lines)
|
|
lines.any { it.contains("move.l (a0,d0.w), a0") } shouldBe true
|
|
lines.any { it.contains("move.l d0,p8_regfile+6") } shouldBe false
|
|
lines.any { it.contains("jmp ([p8_regfile+6])") } shouldBe false
|
|
lines.any { it.trim() == "jmp (a0)" } shouldBe true
|
|
}
|
|
|
|
test("fuses LOADX+JUMPI on amiga500 (68000 movea reload)") {
|
|
val lines = mutableListOf(
|
|
" lea mytable,a0",
|
|
" move.l (a0,d0.w),d0",
|
|
" move.l d0,p8_regfile+6",
|
|
" movea.l p8_regfile+6,a0",
|
|
" jmp (a0)"
|
|
)
|
|
optimizeAssembly(lines)
|
|
lines.any { it.contains("move.l (a0,d0.w), a0") } shouldBe true
|
|
lines.any { it.contains("move.l d0,p8_regfile+6") } shouldBe false
|
|
lines.any { it.contains("movea.l p8_regfile+6,a0") } shouldBe false
|
|
lines.count { it.trim() == "jmp (a0)" } shouldBe 1
|
|
}
|
|
|
|
test("fuses LOADX+CALLI on qemu68k (68020 memory-indirect jsr)") {
|
|
val lines = mutableListOf(
|
|
" lea mytable,a0",
|
|
" move.l (a0,d0.w),d0",
|
|
" move.l d0,p8_regfile+6",
|
|
" jsr ([p8_regfile+6])"
|
|
)
|
|
optimizeAssembly(lines)
|
|
lines.any { it.contains("move.l (a0,d0.w), a0") } shouldBe true
|
|
lines.any { it.contains("move.l d0,p8_regfile+6") } shouldBe false
|
|
lines.any { it.contains("jsr ([p8_regfile+6])") } shouldBe false
|
|
lines.any { it.trim() == "jsr (a0)" } shouldBe true
|
|
}
|
|
|
|
test("fuses LOADX+CALLI on amiga500 (68000 movea reload)") {
|
|
val lines = mutableListOf(
|
|
" lea mytable,a0",
|
|
" move.l (a0,d0.w),d0",
|
|
" move.l d0,p8_regfile+6",
|
|
" movea.l p8_regfile+6,a0",
|
|
" jsr (a0)"
|
|
)
|
|
optimizeAssembly(lines)
|
|
lines.any { it.contains("move.l (a0,d0.w), a0") } shouldBe true
|
|
lines.any { it.contains("move.l d0,p8_regfile+6") } shouldBe false
|
|
lines.any { it.contains("movea.l p8_regfile+6,a0") } shouldBe false
|
|
lines.any { it.trim() == "jsr (a0)" } shouldBe true
|
|
}
|
|
|
|
test("does not fuse LOADX+JUMPI when spill slot is a jump target") {
|
|
val lines = mutableListOf(
|
|
" lea mytable,a0",
|
|
" move.l (a0,d0.w),d0",
|
|
"target: move.l d0,p8_regfile+6",
|
|
" jmp ([p8_regfile+6])"
|
|
)
|
|
optimizeAssembly(lines)
|
|
// spill has label, must be kept
|
|
lines.any { it.contains("move.l d0,p8_regfile+6") } shouldBe true
|
|
lines.any { it.contains("move.l (a0,d0.w), a0") } shouldBe false
|
|
}
|
|
|
|
// === D0 peephole cache ===
|
|
|
|
test("skips redundant d0 load when the value is already cached") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-d0-cache-skip"),
|
|
listOf(
|
|
IRInstructions.binary(Opcode.ADDR, IRDataType.WORD, 3, 1),
|
|
IRInstructions.memoryOp(Opcode.ADDM, IRDataType.WORD, IRMemory.direct("p8b_test.p8v_value"), source = 1)
|
|
)
|
|
)
|
|
|
|
// ADDR loads r1 into d0 and leaves it there; ADDM reuses the cached r1.
|
|
lines.count { it.startsWith("move.w p8_regfile+") && it.endsWith(",d0") } shouldBe 1
|
|
}
|
|
|
|
test("re-emits d0 load after the cached slot is written directly") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-d0-cache-invalidate-slot"),
|
|
listOf(
|
|
IRInstructions.binary(Opcode.ADDR, IRDataType.WORD, 3, 1),
|
|
IRInstructions.unary(Opcode.INC, IRDataType.WORD, 1),
|
|
IRInstructions.memoryOp(Opcode.ADDM, IRDataType.WORD, IRMemory.direct("p8b_test.p8v_value"), source = 1)
|
|
)
|
|
)
|
|
|
|
// ADDR caches r1; INC writes r1 directly; ADDM must reload.
|
|
lines.count { it.startsWith("move.w p8_regfile+") && it.endsWith(",d0") } shouldBe 2
|
|
}
|
|
|
|
test("re-emits d0 load after d0 is clobbered by another register load") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-d0-cache-invalidate-d0"),
|
|
listOf(
|
|
IRInstructions.binary(Opcode.ADDR, IRDataType.WORD, 3, 1),
|
|
IRInstructions.binary(Opcode.ADDR, IRDataType.WORD, 4, 2),
|
|
IRInstructions.memoryOp(Opcode.ADDM, IRDataType.WORD, IRMemory.direct("p8b_test.p8v_value"), source = 1)
|
|
)
|
|
)
|
|
|
|
// ADDR caches r1, second ADDR overwrites d0 with r2; ADDM must reload r1.
|
|
lines.count { it.startsWith("move.w p8_regfile+") && it.endsWith(",d0") } shouldBe 3
|
|
}
|
|
|
|
test("d0 cache is invalidated at chunk boundaries") {
|
|
val chunk1 = IRCodeChunk(null, null)
|
|
chunk1.instructions.add(IRInstructions.binary(Opcode.ADDR, IRDataType.WORD, 3, 1))
|
|
val chunk2 = IRCodeChunk("test.next", null)
|
|
chunk2.instructions.add(IRInstructions.memoryOp(Opcode.ADDM, IRDataType.WORD, IRMemory.direct("p8b_test.p8v_value"), source = 1))
|
|
val lines = generateAsmChunks(
|
|
tempRoot.resolve("test-m68k-d0-cache-boundary"),
|
|
listOf(chunk1, chunk2)
|
|
)
|
|
|
|
// ADDR in chunk1 caches r1, but chunk2 is a new basic block.
|
|
lines.count { it.startsWith("move.w p8_regfile+") && it.endsWith(",d0") } shouldBe 2
|
|
}
|
|
|
|
// === centralized MemoryReference lowering (Direct/Indexed/Indirect) ===
|
|
|
|
test("indexed memory lowering scales the index in the addressing mode on 68020") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-indexed-68020"),
|
|
listOf(
|
|
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.WORD, 6, IRMemory.indexed("arr", 3, IRDataType.WORD, 2, 4)),
|
|
IRInstructions.storeMemory(Opcode.STOREX, IRDataType.WORD, 8, IRMemory.indexed("arr", 3, IRDataType.WORD, 4)),
|
|
IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.indexed("arr", 3, IRDataType.WORD, 8))
|
|
)
|
|
)
|
|
|
|
lines.any { it == "lea arr+4,a0" } shouldBe true
|
|
lines.any { it == "move.w (a0,d0.w*2),d0" } shouldBe true
|
|
// the stored value is staged in d1 because a0/d0 hold the effective address
|
|
lines.any { it == "move.w d1,(a0,d0.w*4)" } shouldBe true
|
|
lines.any { it == "clr.b (a0,d0.w*8)" } shouldBe true
|
|
}
|
|
|
|
test("indexed memory lowering scales the index by hand on 68000") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-indexed-68000"),
|
|
listOf(
|
|
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.WORD, 6, IRMemory.indexed("arr", 3, IRDataType.WORD, 2)),
|
|
IRInstructions.storeMemory(Opcode.STOREX, IRDataType.WORD, 8, IRMemory.indexed("arr", 3, IRDataType.WORD, 4)),
|
|
IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.indexed("arr", 3, IRDataType.WORD, 8))
|
|
),
|
|
target = Amiga500Target()
|
|
)
|
|
|
|
lines.any { it == "add.w d0,d0" } shouldBe true
|
|
lines.any { it == "lsl.w #2,d0" } shouldBe true
|
|
lines.any { it == "muls.w #8,d0" } shouldBe true
|
|
lines.count { it.contains("(a0,d0.w*") } shouldBe 0
|
|
}
|
|
|
|
test("loadx+jumpi fusion removes the regfile roundtrip and keeps the scaled index operand") {
|
|
// a pointer-array dispatch (on..goto / rewritten when) emits the scale in the addressing mode
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-fuse-loadx-jumpi-scaled"),
|
|
listOf(
|
|
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.POINTER, 6, IRMemory.indexed("table", 3, IRDataType.WORD, 4)),
|
|
IRInstructions.jumpIndirect(6)
|
|
)
|
|
)
|
|
|
|
lines.any { it == "move.l (a0,d0.w*4), a0" } shouldBe true
|
|
lines.any { it == "jmp (a0)" } shouldBe true
|
|
// the spill and the reload it feeds are gone
|
|
lines.count { it.startsWith("move.l d0,p8_regfile") } shouldBe 0
|
|
lines.count { it.startsWith("movea.l") } shouldBe 0
|
|
lines.any { it.startsWith("jmp ([p8_regfile") } shouldBe false
|
|
}
|
|
|
|
test("loadx+jumpi fusion keeps the unscaled index operand on 68000") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-fuse-loadx-jumpi-unscaled"),
|
|
listOf(
|
|
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.POINTER, 6, IRMemory.indexed("table", 3, IRDataType.WORD, 4)),
|
|
IRInstructions.jumpIndirect(6)
|
|
),
|
|
target = Amiga500Target()
|
|
)
|
|
|
|
// on 68000 the scale is applied to d0 by hand, so the addressing mode stays unscaled
|
|
lines.any { it == "lsl.w #2,d0" } shouldBe true
|
|
lines.any { it == "move.l (a0,d0.w), a0" } shouldBe true
|
|
lines.any { it == "jmp (a0)" } shouldBe true
|
|
lines.count { it.startsWith("move.l d0,p8_regfile") } shouldBe 0
|
|
}
|
|
|
|
test("indirect memory lowering picks displacement addressing or explicit pointer adjustment") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-indirect-displacement"),
|
|
listOf(
|
|
IRInstructions.loadMemory(Opcode.LOADI, IRDataType.WORD, 9, IRMemory.indirect(4, 0)),
|
|
IRInstructions.storeMemory(Opcode.STOREI, IRDataType.BYTE, 11, IRMemory.indirect(4, 6)),
|
|
IRInstructions.loadMemory(Opcode.LOADI, IRDataType.BYTE, 10, IRMemory.indirect(4, 40000))
|
|
)
|
|
)
|
|
|
|
lines.any { it.startsWith("movea.l p8_regfile") && it.endsWith(",a0") } shouldBe true
|
|
lines.any { it == "move.w (a0),d0" } shouldBe true
|
|
lines.any { it.endsWith(",(6,a0)") } shouldBe true
|
|
// displacements beyond the signed 16-bit range need an explicit adda
|
|
lines.any { it == "adda.l #40000,a0" } shouldBe true
|
|
}
|
|
|
|
test("float indexed memory lowering uses full width d0.l indexing") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-indexed-float"),
|
|
listOf(
|
|
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.FLOAT, 1, IRMemory.indexed("arr", 2, IRDataType.WORD, 4)),
|
|
IRInstructions.storeMemory(Opcode.STOREX, IRDataType.FLOAT, 1, IRMemory.indexed("arr", 2, IRDataType.WORD, 8))
|
|
)
|
|
)
|
|
|
|
lines.any { it == "lsl.l #2,d0" } shouldBe true
|
|
lines.any { it == "lsl.l #3,d0" } shouldBe true
|
|
lines.any { it == "fmove.s (0,a0,d0.l),fp0" } shouldBe true
|
|
lines.any { it == "fmove.s fp0,(0,a0,d0.l)" } shouldBe true
|
|
}
|
|
|
|
test("direct memory lowering resolves symbol displacements and absolute addresses") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-direct-memory"),
|
|
listOf(
|
|
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.WORD, 12, IRMemory.direct("var", 4)),
|
|
IRInstructions.storeMemory(Opcode.STOREM, IRDataType.WORD, 12, IRMemory.direct(0xdff180u)),
|
|
IRInstructions.storeImmediate(IRDataType.WORD, 1234, IRMemory.direct("var"))
|
|
)
|
|
)
|
|
|
|
lines.any { it == "move.w var+4,p8_regfile+0" } shouldBe true
|
|
lines.any { it == "move.w p8_regfile+0,\$00dff180" } shouldBe true
|
|
lines.any { it == "move.w #1234,var" } shouldBe true
|
|
}
|
|
|
|
// === extb.l gate hardening ===
|
|
|
|
test("M68020 sign-extends byte to long with extb.l") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-extbl-qemu"),
|
|
listOf(IRInstructions.binary(Opcode.EXTLS, IRDataType.BYTE, 1, 2))
|
|
)
|
|
|
|
lines.count { it == "extb.l d0" } shouldBe 1
|
|
lines.count { it == "ext.w d0" } shouldBe 0
|
|
lines.count { it == "ext.l d0" } shouldBe 0
|
|
}
|
|
|
|
test("M68000 sign-extends byte to long with ext.w plus ext.l") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-extbl-amiga"),
|
|
listOf(IRInstructions.binary(Opcode.EXTLS, IRDataType.BYTE, 1, 2)),
|
|
target = Amiga500Target()
|
|
)
|
|
|
|
lines.count { it == "extb.l d0" } shouldBe 0
|
|
lines.count { it == "ext.w d0" } shouldBe 1
|
|
lines.count { it == "ext.l d0" } shouldBe 1
|
|
}
|
|
|
|
test("M5 drops a lea when a0 already holds the same base") {
|
|
val lines = mutableListOf(
|
|
" lea arr,a0",
|
|
" move.w d1,(a0,d0.w)",
|
|
" lea arr,a0",
|
|
" clr.w (a0,d0.w)"
|
|
)
|
|
optimizeAssembly(lines)
|
|
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 1
|
|
lines.any { it.trimStart().startsWith("clr.w") } shouldBe true
|
|
}
|
|
|
|
test("M5 keeps a lea when a different base was loaded into a0") {
|
|
val lines = mutableListOf(
|
|
" lea arr,a0",
|
|
" move.w d1,(a0,d0.w)",
|
|
" lea other,a0",
|
|
" lea arr,a0",
|
|
" clr.w (a0,d0.w)"
|
|
)
|
|
optimizeAssembly(lines)
|
|
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 2
|
|
lines.count { it.trimStart().startsWith("lea other,a0") } shouldBe 1
|
|
}
|
|
|
|
test("M5 keeps a lea when a0 is written in between") {
|
|
val lines = mutableListOf(
|
|
" lea arr,a0",
|
|
" movea.l other,a0",
|
|
" lea arr,a0",
|
|
" clr.w (a0,d0.w)"
|
|
)
|
|
optimizeAssembly(lines)
|
|
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 2
|
|
}
|
|
|
|
test("M5 keeps a lea when a call intervenes") {
|
|
val lines = mutableListOf(
|
|
" lea arr,a0",
|
|
" bsr some.sub",
|
|
" lea arr,a0",
|
|
" clr.w (a0,d0.w)"
|
|
)
|
|
optimizeAssembly(lines)
|
|
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 2
|
|
lines.any { it.trimStart().startsWith("bsr") } shouldBe true
|
|
}
|
|
|
|
test("M5 forgets a0 across a label") {
|
|
val lines = mutableListOf(
|
|
" lea arr,a0",
|
|
"somewhere:",
|
|
" lea arr,a0",
|
|
" clr.w (a0,d0.w)"
|
|
)
|
|
optimizeAssembly(lines)
|
|
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 2
|
|
lines.any { it.trimStart().startsWith("somewhere:") } shouldBe true
|
|
}
|
|
|
|
test("M5 drops a lea across a conditional branch to a label") {
|
|
// A conditional branch does not modify a0, and its target is a label where the tracked
|
|
// value is dropped, so the fallthrough still knows a0 holds the base.
|
|
val lines = mutableListOf(
|
|
" lea arr,a0",
|
|
" move.w (a0,d0.w),d0",
|
|
" bge clamp.done",
|
|
" lea arr,a0",
|
|
" clr.w (a0,d0.w)",
|
|
"clamp.done:"
|
|
)
|
|
optimizeAssembly(lines)
|
|
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 1
|
|
lines.any { it.trimStart().startsWith("clr.w") } shouldBe true
|
|
}
|
|
|
|
test("qemu68k word right shifts avoid the memory forms its 68020 model mis-emulates") {
|
|
// QEMU's 68020 sign-extends the operand for both `asr.w <abs>` and
|
|
// `lsr.w (a0)`, so both memory forms give the wrong result. The
|
|
// register form round-trips through d0 and is correct.
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-qemu-word-rshift"),
|
|
listOf(
|
|
IRInstructions.memoryOp(Opcode.ASRM, IRDataType.WORD, IRMemory.direct("p8b_test.p8v_value")),
|
|
IRInstructions.memoryOp(Opcode.LSRM, IRDataType.WORD, IRMemory.direct("p8b_test.p8v_other"))
|
|
),
|
|
Qemu68kTarget()
|
|
)
|
|
|
|
lines.count { it.startsWith("asr.w") } shouldBe 1
|
|
lines.count { it.startsWith("lsr.w") } shouldBe 1
|
|
lines.count { it == "asr.w #1,d0" } shouldBe 1
|
|
lines.count { it == "lsr.w #1,d0" } shouldBe 1
|
|
lines.count { it == "move.w p8b_test.p8v_value,d0" } shouldBe 1
|
|
lines.count { it == "move.w d0,p8b_test.p8v_value" } shouldBe 1
|
|
lines.count { it.startsWith("lea") } shouldBe 0
|
|
}
|
|
|
|
test("amiga500 keeps the compact word right shift memory form") {
|
|
val lines = generateAsm(
|
|
tempRoot.resolve("test-m68k-amiga-word-rshift"),
|
|
listOf(
|
|
IRInstructions.memoryOp(Opcode.ASRM, IRDataType.WORD, IRMemory.direct("p8b_test.p8v_value")),
|
|
IRInstructions.memoryOp(Opcode.LSRM, IRDataType.WORD, IRMemory.direct("p8b_test.p8v_other"))
|
|
),
|
|
Amiga500Target()
|
|
)
|
|
|
|
lines.count { it.startsWith("asr.w p8b_test.p8v_value") } shouldBe 1
|
|
lines.count { it.startsWith("lsr.w p8b_test.p8v_other") } shouldBe 1
|
|
lines.count { it == "asr.w #1,d0" } shouldBe 0
|
|
lines.count { it == "lsr.w #1,d0" } shouldBe 0
|
|
}
|
|
})
|