Files

895 lines
37 KiB
Kotlin

package prog8tests.codegencpu6502
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import prog8.code.StMemVar
import prog8.code.SymbolTable
import prog8.code.ast.PtProgram
import prog8.code.core.BaseDataType
import prog8.code.core.DataType
import prog8.code.target.C64Target
import prog8.code.target.VMTarget
import prog8.codegen.cpu6502.hasInstr
import prog8.codegen.cpu6502.haslabel
import prog8.codegen.cpu6502.isBranch
import prog8.codegen.cpu6502.isLoadReg
import prog8.codegen.cpu6502.isStoreReg
import prog8.codegen.cpu6502.isStoreRegOrZero
import prog8.codegen.cpu6502.keeplabel
import prog8.codegen.cpu6502.modifiesYRegister
import prog8.codegen.cpu6502.optimizeAssembly
class TestAsmOptimizer: FunSpec({
val machine = C64Target()
val program = PtProgram("test", VMTarget())
val symbolTable = SymbolTable(program)
// Add test variables with known non-IO addresses so the optimizer can resolve them
// Non-IO addresses in typical C64 RAM range
symbolTable.add(StMemVar("myvar", DataType.UBYTE, 0x0300u, null, null))
symbolTable.add(StMemVar("var", DataType.UBYTE, 0x0301u, null, null))
symbolTable.add(StMemVar("array", DataType.arrayFor(BaseDataType.UBYTE, machine), 0x0302u, 16u, null))
symbolTable.add(StMemVar("wordarray", DataType.arrayFor(BaseDataType.UWORD, machine), 0x0310u, 32u, null))
symbolTable.add(StMemVar("nswordarray", DataType.arrayFor(BaseDataType.UWORD, machine), 0x0320u, 32u, null))
symbolTable.add(StMemVar("var1", DataType.UWORD, 0x0400u, null, null))
symbolTable.add(StMemVar("var2", DataType.UWORD, 0x0402u, null, null))
symbolTable.add(StMemVar("ptr", DataType.UWORD, 0x0404u, null, null))
symbolTable.add(StMemVar("P8ZP_SCRATCH_PTR", DataType.UWORD, 0x02u, null, null))
symbolTable.add(StMemVar("P8ZP_SCRATCH_W1", DataType.UWORD, 0x04u, null, null))
symbolTable.add(StMemVar("A1", DataType.UWORD, 0x0410u, null, null))
symbolTable.add(StMemVar("A2", DataType.UWORD, 0x0412u, null, null))
fun optimize(lines: MutableList<String>) {
optimizeAssembly(lines, machine, symbolTable)
}
// --- optimizeIncDec ---
test("optimizeIncDec: removes iny+dey sequence") {
val lines = mutableListOf(" iny", " dey", " rts", " nop")
optimize(lines)
lines shouldBe listOf(" rts", " nop")
}
test("optimizeIncDec: removes iny+dey sequence with comments") {
val lines = mutableListOf(" iny ; increment Y", " dey ; decrement Y", " rts", " nop")
optimize(lines)
lines shouldBe listOf(" rts", " nop")
}
test("optimizeIncDec: removes inx+dex sequence") {
val lines = mutableListOf(" inx", " dex", " rts", " nop")
optimize(lines)
lines shouldBe listOf(" rts", " nop")
}
test("optimizeIncDec: removes dey+iny sequence") {
val lines = mutableListOf(" dey", " iny", " rts", " nop")
optimize(lines)
lines shouldBe listOf(" rts", " nop")
}
test("optimizeIncDec: removes dex+inx sequence") {
val lines = mutableListOf(" dex", " inx", " rts", " nop")
optimize(lines)
lines shouldBe listOf(" rts", " nop")
}
test("optimizeIncDec: preserves non-canceling sequences") {
val lines = mutableListOf(" iny", " inx", " rts", " nop")
optimize(lines)
lines shouldBe listOf(" iny", " inx", " rts", " nop")
}
// --- optimizeStoreLoadSame ---
test("optimizeStoreLoadSame: removes sta X + lda X") {
val lines = mutableListOf(" lda #42", " sta myvar", " lda myvar", " rts")
optimize(lines)
lines shouldBe listOf(" lda #42", " sta myvar", " rts")
}
test("optimizeStoreLoadSame: removes sta X + lda X with comments") {
val lines = mutableListOf(" nop", " sta myvar", " lda myvar", " rts ; return")
optimize(lines)
lines shouldBe listOf(" nop", " sta myvar", " rts ; return")
}
test("optimizeStoreLoadSame: removes pha + pla") {
val lines = mutableListOf(" nop", " pha", " pla", " rts")
optimize(lines)
lines shouldBe listOf(" nop", " rts")
}
test("optimizeStoreLoadSame: removes phx + plx") {
val lines = mutableListOf(" nop", " phx", " plx", " rts")
optimize(lines)
lines shouldBe listOf(" nop", " rts")
}
test("optimizeStoreLoadSame: removes phy + ply") {
val lines = mutableListOf(" nop", " phy", " ply", " rts")
optimize(lines)
lines shouldBe listOf(" nop", " rts")
}
test("optimizeStoreLoadSame: removes lda X + sta X") {
val lines = mutableListOf(" nop", " lda myvar", " sta myvar", " rts")
optimize(lines)
lines shouldBe listOf(" nop", " lda myvar", " rts")
}
// --- optimizeJsrRtsAndOtherCombinations ---
test("optimizeJsrRts: converts jsr+rts to jmp") {
val lines = mutableListOf(" jsr mysub", " rts", " nop", " nop")
optimize(lines)
lines shouldBe listOf(" jmp mysub", " nop", " nop")
}
test("optimizeJsrRts: converts jsr+rts to jmp with comments") {
val lines = mutableListOf(" jsr mysub ; call subroutine", " rts ; return", " nop", " nop")
optimize(lines)
lines shouldBe listOf(" jmp mysub ; call subroutine", " nop", " nop")
}
test("optimizeJsrRts: removes rts+jmp") {
val lines = mutableListOf(" rts", " jmp somewhere", " nop", " nop")
optimize(lines)
lines shouldBe listOf(" rts", " nop", " nop")
}
test("optimizeJsrRts: removes rts+bra") {
val lines = mutableListOf(" rts", " bra somewhere", " nop", " nop")
optimize(lines)
lines shouldBe listOf(" rts", " nop", " nop")
}
test("optimizeJsrRts: removes lda+cmp #0") {
val lines = mutableListOf(" lda myvar", " cmp #0", " beq skip", " nop")
optimize(lines)
lines shouldBe listOf(" lda myvar", " beq skip", " nop")
}
test("optimizeJsrRts: removes lda+cmp #0 with comments") {
val lines = mutableListOf(" lda myvar ; load variable", " cmp #0 ; compare to zero", " beq skip ; branch if equal", " nop")
optimize(lines)
lines shouldBe listOf(" lda myvar ; load variable", " beq skip ; branch if equal", " nop")
}
test("optimizeJsrRts: removes bra+jmp") {
val lines = mutableListOf(" bra somewhere", " jmp elsewhere", " nop", " nop")
optimize(lines)
lines shouldBe listOf(" bra somewhere", " nop", " nop")
}
test("optimizeJsrRts: preserves jsr+rts for floats.pushFAC") {
val lines = mutableListOf(" jsr floats.pushFAC", " rts", " nop", " nop")
optimize(lines)
lines shouldBe listOf(" jsr floats.pushFAC", " rts", " nop", " nop")
}
test("optimizeJsrRts: three consecutive jsr only tail jsr+rts becomes jmp") {
val lines = mutableListOf($$" jsr $4000", $$" jsr $5000", $$" jsr $6000", " rts", " nop", " nop")
optimize(lines)
lines shouldBe listOf($$" jsr $4000", $$" jsr $5000", $$" jmp $6000", " nop", " nop")
}
test("optimizeJsrRts: single line with embedded newlines only tail converted defensively") {
val lines = mutableListOf($$" jsr $4000\n jsr $5000\n jsr $6000", " rts", " nop", " nop")
optimize(lines)
// Before fix this became jmp/jmp/jmp via replace("jsr","jmp"); after fix only one jmp via replaceMnemonic
val jmpCount = lines.joinToString("\n").split("jmp").size - 1
jmpCount shouldBe 1
}
// --- optimizeUselessPushPopStack ---
test("optimizeUselessPushPop: removes phy+ldy+pla") {
val lines = mutableListOf(" phy", " ldy #5", " pla", " rts")
optimize(lines)
lines shouldBe listOf(" tya", " ldy #5", " rts")
}
test("optimizeUselessPushPop: removes phx+ldx+pla") {
val lines = mutableListOf(" phx", " ldx #5", " pla", " rts")
optimize(lines)
lines shouldBe listOf(" txa", " ldx #5", " rts")
}
test("optimizeUselessPushPop: removes pha+lda+pla") {
// Note: optimizer removes all three: pha, lda, and pla
val lines = mutableListOf(" pha", " lda #5", " pla", " rts")
optimize(lines)
lines shouldBe listOf(" rts")
}
test("optimizeUselessPushPop: removes pha+tya+tay+pla") {
val lines = mutableListOf(" pha", " tya", " tay", " pla", " rts")
optimize(lines)
lines shouldBe listOf(" rts")
}
// --- optimizeUnneededTempvarInAdd ---
test("optimizeUnneededTempvarInAdd: removes scratch variable in add") {
val lines = mutableListOf(" sta P8ZP_SCRATCH_W1", " lda #5", " clc", " adc P8ZP_SCRATCH_W1", " rts")
optimize(lines)
lines shouldBe listOf(" clc", " adc #5", " rts")
}
test("optimizeUnneededTempvarInAdd: removes scratch variable in add with comments") {
// Note: comments don't affect the optimization
val lines = mutableListOf(" sta P8ZP_SCRATCH_W1", " lda #5", " clc", " adc P8ZP_SCRATCH_W1", " rts ; return")
optimize(lines)
lines shouldBe listOf(" clc", " adc #5", " rts ; return")
}
// --- optimizeTSBtoRegularOr ---
test("optimizeTSBtoRegularOr: converts lda/tsb/lda to lda/ora/sta") {
val lines = mutableListOf(" lda var2", " tsb var1", " lda var1", " rts")
optimize(lines)
lines shouldBe listOf(" lda var1", " ora var2", " sta var1", " rts")
}
test("optimizeTSBtoRegularOr: converts lda/tsb/lda to lda/ora/sta with comments") {
// Note: comments don't affect the optimization
val lines = mutableListOf(" lda var2", " tsb var1", " lda var1", " rts ; return")
optimize(lines)
lines shouldBe listOf(" lda var1", " ora var2", " sta var1", " rts ; return")
}
// --- optimizeSameAssignments ---
test("optimizeSameAssignments: removes duplicate lda/ldy/sta/sty word init") {
val lines = mutableListOf(
" lda #1", " ldy #0", " sta var1", " sty var1+1",
" lda #1", " ldy #0", " sta var2", " sty var2+1",
" rts", " nop", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines shouldBe listOf(
" lda #1", " ldy #0", " sta var1", " sty var1+1",
" sta var2", " sty var2+1",
" rts", " nop", " nop", " nop", " nop", " nop"
)
}
// --- optimizeSamePointerIndexingAndUselessBeq ---
test("optimizeSamePointerIndexing: removes redundant ldy in pointer indexing") {
val lines = mutableListOf(
" ldy #0", " lda (ptr),y", " ora #3", " ldy #0", " sta (ptr),y", " rts",
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines shouldBe listOf(" ldy #0", " lda (ptr),y", " ora #3", " sta (ptr),y", " rts",
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop")
}
// --- optimizeSamePointerIndexing: guards on the index reload ---
fun padded(vararg body: String): MutableList<String> =
(body.toList() + List(14 - body.size) { " nop" }).toMutableList()
/** Same, but one line shorter, for cases where the optimizer is expected to drop a line. */
fun paddedAfterDrop(vararg body: String): MutableList<String> =
(body.toList() + List(13 - body.size) { " nop" }).toMutableList()
test("samePointerIndexing: KEEPS reload when the index variable is written in between") {
val lines = padded(" ldy i", " lda arr,y", " inc i", " ldy i", " sta arr,y", " rts")
optimize(lines)
lines shouldBe padded(" ldy i", " lda arr,y", " inc i", " ldy i", " sta arr,y", " rts")
}
test("samePointerIndexing: KEEPS reload when the index variable is decremented in between") {
val lines = padded(" ldy i", " lda arr,y", " dec i", " ldy i", " sta arr,y", " rts")
optimize(lines)
lines shouldBe padded(" ldy i", " lda arr,y", " dec i", " ldy i", " sta arr,y", " rts")
}
test("samePointerIndexing: KEEPS reload across a jsr") {
val lines = padded(" ldy #0", " lda (ptr),y", " jsr someSub", " ldy #0", " sta (ptr),y", " rts")
optimize(lines)
lines shouldBe padded(" ldy #0", " lda (ptr),y", " jsr someSub", " ldy #0", " sta (ptr),y", " rts")
}
test("samePointerIndexing: KEEPS reload across a label") {
val lines = padded(" ldy #0", " lda (ptr),y", "someLabel:", " ldy #0", " sta (ptr),y", " rts")
optimize(lines)
lines shouldBe padded(" ldy #0", " lda (ptr),y", "someLabel:", " ldy #0", " sta (ptr),y", " rts")
}
test("samePointerIndexing: KEEPS reload when an indirect store may alias the index") {
val lines = padded(" ldy i", " lda arr,y", " sta (vptr),y", " ldy i", " lda arr2,y")
optimize(lines)
lines shouldBe padded(" ldy i", " lda arr,y", " sta (vptr),y", " ldy i", " lda arr2,y")
}
test("samePointerIndexing: KEEPS reload when an intervening instruction modifies Y") {
val lines = padded(" ldy #0", " lda (ptr),y", " tay", " ldy #0", " sta (ptr),y", " rts")
optimize(lines)
lines shouldBe padded(" ldy #0", " lda (ptr),y", " tay", " ldy #0", " sta (ptr),y", " rts")
}
test("samePointerIndexing: KEEPS reload for an IO index operand") {
val lines = padded($$" ldy c64.$d020", " lda arr,y", " lda arr2,y", $$" ldy c64.$d020", " lda arr3,y")
optimize(lines)
lines shouldBe padded($$" ldy c64.$d020", " lda arr,y", " lda arr2,y", $$" ldy c64.$d020", " lda arr3,y")
}
test("samePointerIndexing: still drops reload when a store to an unrelated var intervenes") {
val lines = padded(" ldy i", " lda arr,y", " sta other", " ldy i", " sta arr,y", " rts")
optimize(lines)
lines shouldBe paddedAfterDrop(" ldy i", " lda arr,y", " sta other", " sta arr,y", " rts")
}
test("samePointerIndexing: KEEPS reload when the two ldy load DIFFERENT immediates") {
// regression: seed[1] then seed[0] - the reload selects a different element
val lines = padded(" ldy #1", " lda seed_msb,y", " sta x", " ldy #0", " lda seed_msb,y", " sta y", " rts")
optimize(lines)
lines shouldBe padded(" ldy #1", " lda seed_msb,y", " sta x", " ldy #0", " lda seed_msb,y", " sta y", " rts")
}
test("samePointerIndexing: KEEPS reload when the two ldy load different variables") {
val lines = padded(" ldy i", " lda arr,y", " sta x", " ldy j", " lda arr,y", " sta y", " rts")
optimize(lines)
lines shouldBe padded(" ldy i", " lda arr,y", " sta x", " ldy j", " lda arr,y", " sta y", " rts")
}
test("samePointerIndexing: KEEPS reload when the two ldy differ by an offset") {
val lines = padded(" ldy w+0", " lda arr,y", " sta x", " ldy w+1", " lda arr,y", " sta y", " rts")
optimize(lines)
lines shouldBe padded(" ldy w+0", " lda arr,y", " sta x", " ldy w+1", " lda arr,y", " sta y", " rts")
}
test("samePointerIndexing: matched load/store arm also requires the same index") {
val lines = padded(" ldy i", " lda arr,y", " clc", " ldy j", " sta arr,y", " rts")
optimize(lines)
lines shouldBe padded(" ldy i", " lda arr,y", " clc", " ldy j", " sta arr,y", " rts")
}
test("samePointerIndexing: relaxed arm drops reload for two indexed loads") {
val lines = padded(" ldy sp", " lda cx,y", " sta cxv", " ldy sp", " lda cy,y", " sta cyv", " rts")
optimize(lines)
lines shouldBe paddedAfterDrop(" ldy sp", " lda cx,y", " sta cxv", " lda cy,y", " sta cyv", " rts")
}
test("samePointerIndexing: relaxed arm does not fire when Y is modified") {
val lines = padded(" ldy sp", " lda cx,y", " dey", " ldy sp", " lda cy,y", " rts")
optimize(lines)
lines shouldBe padded(" ldy sp", " lda cx,y", " dey", " ldy sp", " lda cy,y", " rts")
}
test("samePointerIndexing: relaxed arm keeps reload when index is written in between") {
val lines = padded(" ldy i", " lda arr,y", " inc i", " ldy i", " lda arr2,y", " rts")
optimize(lines)
lines shouldBe padded(" ldy i", " lda arr,y", " inc i", " ldy i", " lda arr2,y", " rts")
}
// --- optimizeAddWordToSameVariableOrExtraRegisterLoadInWordStore ---
test("optimizeAddWordToSameVariable: optimizes P8ZP_SCRATCH_PTR += AY") {
val lines = mutableListOf(
" clc", " adc P8ZP_SCRATCH_PTR", " pha", " tya",
" adc P8ZP_SCRATCH_PTR+1", " tay", " pla",
" sta P8ZP_SCRATCH_PTR", " sty P8ZP_SCRATCH_PTR+1", " rts",
" nop", " nop", " nop", " nop"
)
optimize(lines)
lines shouldBe listOf(
" clc", " adc P8ZP_SCRATCH_PTR", " sta P8ZP_SCRATCH_PTR",
" tya", " adc P8ZP_SCRATCH_PTR+1", " sta P8ZP_SCRATCH_PTR+1", " rts",
" nop", " nop", " nop", " nop"
)
}
test("optimizeExtraRegisterLoadInWordStore: removes ldx+sta+txa+ldy+sta") {
val lines = mutableListOf(" ldx #42", " sta myvar", " txa", " ldy #1", " sta myvar+1", " rts",
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop")
optimize(lines)
lines shouldBe listOf(" sta myvar", " lda #42", " ldy #1", " sta myvar+1", " rts",
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop")
}
// --- optimizeFoldIndexOffset (byte arrays) ---
test("optimizeFoldIndexOffset: folds byte array lda+clc+adc+tay+lda to ldy+lda") {
val lines = mutableListOf(
" lda myvar", " clc", " adc #3", " tay", " lda array,y",
" rts", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines[0] shouldBe " ldy myvar"
lines[1] shouldBe " lda array+3,y"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffset: folds with x register") {
val lines = mutableListOf(
" lda var", " clc", " adc #5", " tax", " lda array,x",
" rts", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines[0] shouldBe " ldx var"
lines[1] shouldBe " lda array+5,x"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffset: works with sta instead of lda") {
val lines = mutableListOf(
" lda myvar", " clc", " adc #1", " tay", " sta array,y",
" rts", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines[0] shouldBe " ldy myvar"
lines[1] shouldBe " sta array+1,y"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffset: does not fold indirect indexed (ptr),y") {
val lines = mutableListOf(
" lda myvar", " clc", " adc #3", " tay", " lda (ptr),y",
" rts", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines.size shouldBe 10
}
test("optimizeFoldIndexOffset: preserves hex constant format") {
val lines = mutableListOf(
" lda myvar", " clc", " adc #$0f", " tay", " lda array,y",
" rts", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines[0] shouldBe " ldy myvar"
lines[1] shouldBe " lda array+$0f,y"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffset: does not fold when 5th op is not absolute indexed") {
val lines = mutableListOf(
" lda myvar", " clc", " adc #3", " tay", " nop",
" rts", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines.size shouldBe 10
}
test("optimizeFoldIndexOffset: works with cmp instruction") {
val lines = mutableListOf(
" lda myvar", " clc", " adc #7", " tay", " cmp array,y",
" rts", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines[0] shouldBe " ldy myvar"
lines[1] shouldBe " cmp array+7,y"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffset: preserves label on first instruction") {
val lines = mutableListOf(
"mylabel: lda myvar", " clc", " adc #3", " tay", " lda array,y",
" rts", " nop", " nop", " nop", " nop"
)
optimize(lines)
lines[0] shouldBe "mylabel: ldy myvar"
lines[1] shouldBe " lda array+3,y"
lines.size shouldBe 7
}
// --- optimizeFoldIndexOffsetSplitWord (split word arrays) ---
test("optimizeFoldIndexOffsetSplitWord: folds split word array access with y") {
val lines = mutableListOf(
" lda var", " clc", " adc #3", " tay",
" lda wordarray_lsb,y", " ldx wordarray_msb,y",
" rts", " nop", " nop", " nop"
)
optimize(lines)
lines[0] shouldBe " ldy var"
lines[1] shouldBe " lda wordarray_lsb+3,y"
lines[2] shouldBe " ldx wordarray_msb+3,y"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffsetSplitWord: folds split word array access with x") {
val lines = mutableListOf(
" lda var", " clc", " adc #3", " tax",
" lda wordarray_lsb,x", " ldx wordarray_msb,x",
" rts", " nop", " nop", " nop"
)
optimize(lines)
lines[0] shouldBe " ldx var"
lines[1] shouldBe " lda wordarray_lsb+3,x"
lines[2] shouldBe " ldx wordarray_msb+3,x"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffsetSplitWord: does not fold mismatched registers") {
val lines = mutableListOf(
" lda var", " clc", " adc #3", " tay",
" lda wordarray_lsb,y", " ldx wordarray_msb,x",
" rts", " nop", " nop", " nop"
)
optimize(lines)
lines.size shouldBe 10
}
test("optimizeFoldIndexOffsetSplitWord: does not fold non-lsb-msb pair") {
val lines = mutableListOf(
" lda var", " clc", " adc #3", " tay",
" lda wordarray_lsb,y", " ldx other,y",
" rts", " nop", " nop", " nop"
)
optimize(lines)
lines.size shouldBe 10
}
// --- optimizeFoldIndexOffsetNonSplitWord (non-split word arrays) ---
test("optimizeFoldIndexOffsetNonSplitWord: folds non-split word array access") {
val lines = mutableListOf(
" lda var", " clc", " adc #3", " asl a", " tay",
" lda nswordarray,y", " ldx nswordarray+1,y",
" rts", " nop"
)
optimize(lines)
lines[0] shouldBe " lda var"
lines[1] shouldBe " asl a"
lines[2] shouldBe " tay"
lines[3] shouldBe " lda nswordarray+6,y"
lines[4] shouldBe " ldx nswordarray+1+6,y"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffsetNonSplitWord: scales hex constants") {
val lines = mutableListOf(
" lda var", " clc", " adc #$02", " asl a", " tay",
" lda nswordarray,y", " ldx nswordarray+1,y",
" rts", " nop"
)
optimize(lines)
lines[3] shouldBe " lda nswordarray+$04,y"
lines[4] shouldBe " ldx nswordarray+1+$04,y"
lines.size shouldBe 7
}
test("optimizeFoldIndexOffsetNonSplitWord: does not fold without asl a") {
val lines = mutableListOf(
" lda var", " clc", " adc #3", " tay",
" lda nswordarray,y", " ldx nswordarray+1,y",
" rts", " nop", " nop"
)
optimize(lines)
lines.size shouldBe 9
}
// --- Edge cases ---
test("handles empty input") {
val lines = mutableListOf<String>()
optimize(lines)
lines shouldBe emptyList()
}
test("handles single line input") {
val lines = mutableListOf(" rts")
optimize(lines)
lines shouldBe listOf(" rts")
}
test("multiple optimizations in one pass") {
val lines = mutableListOf(" iny", " dey", " lda #1", " sta var", " lda var", " rts")
optimize(lines)
lines shouldBe listOf(" lda #1", " sta var", " rts")
}
// --- Helper function tests ---
// hasInstr tests
test("hasInstr: exact match") {
"iny".hasInstr("iny") shouldBe true
"pla".hasInstr("pla") shouldBe true
"rts".hasInstr("rts") shouldBe true
}
test("hasInstr: starts with mnemonic and space") {
"lda #1".hasInstr("lda") shouldBe true
"sta myvar".hasInstr("sta") shouldBe true
"ldy #0".hasInstr("ldy") shouldBe true
"inc a".hasInstr("inc") shouldBe true
}
test("hasInstr: contains mnemonic after label") {
"mylabel: lda #1".hasInstr("lda") shouldBe true
"loop: iny".hasInstr("iny") shouldBe true
"skip: rts".hasInstr("rts") shouldBe true
}
test("hasInstr: does not match named labels without colon") {
// Named labels require a colon to distinguish from instructions
"mylabel lda #1".hasInstr("lda") shouldBe false
"loop iny".hasInstr("iny") shouldBe false
"skip rts".hasInstr("rts") shouldBe false
}
test("hasInstr: handles 64tass anonymous labels with colon") {
"+: lda #1".hasInstr("lda") shouldBe true
"++: lda #1".hasInstr("lda") shouldBe true
"-: lda #1".hasInstr("lda") shouldBe true
"--: lda #1".hasInstr("lda") shouldBe true
"+: rts".hasInstr("rts") shouldBe true
"+++: iny".hasInstr("iny") shouldBe true
}
test("hasInstr: handles 64tass anonymous labels without colon") {
"+ lda #1".hasInstr("lda") shouldBe true
"++ lda #1".hasInstr("lda") shouldBe true
"- lda #1".hasInstr("lda") shouldBe true
"-- lda #1".hasInstr("lda") shouldBe true
"+ rts".hasInstr("rts") shouldBe true
"+++ iny".hasInstr("iny") shouldBe true
}
test("hasInstr: handles scoped labels with periods") {
"mysub.loop: lda #1".hasInstr("lda") shouldBe true
"_private: rts".hasInstr("rts") shouldBe true
// Without colon, named labels are not recognized
"mysub.loop lda #1".hasInstr("lda") shouldBe false
"_private rts".hasInstr("rts") shouldBe false
}
test("hasInstr: does not match numbers as labels") {
"1 lda #1".hasInstr("lda") shouldBe false
"+1 lda #1".hasInstr("lda") shouldBe false
"-1 lda #1".hasInstr("lda") shouldBe false
}
test("hasInstr: does not match partial mnemonics") {
"lda #1".hasInstr("ld") shouldBe false
"sta var".hasInstr("st") shouldBe false
"iny".hasInstr("in") shouldBe false
}
test("hasInstr: does not match mnemonic in operand") {
// Note: hasInstr uses contains(" mnemonic") which can match operands
// This is a known limitation - the optimizer only calls hasInstr on
// trimmed lines where the instruction is at the start or after a label
"lda #iny".hasInstr("iny") shouldBe false
// "sta pla" would match due to contains(" pla") - this is expected behavior
// in the optimizer context where lines are properly formatted
}
test("hasInstr: handles inc a variants") {
"inc a".hasInstr("inc a") shouldBe true
"inc a".hasInstr("inc a") shouldBe true
"inc var".hasInstr("inc a") shouldBe false
}
test("hasInstr: handles lines with trailing comments") {
"iny ; increment Y".hasInstr("iny") shouldBe true
"lda #1 ; load immediate".hasInstr("lda") shouldBe true
"mylabel: rts ; return".hasInstr("rts") shouldBe true
// Note: hasInstr expects trimmed lines, so leading whitespace must be removed first
"dey ; decrement Y".hasInstr("dey") shouldBe true
}
test("hasInstr: handles dec a variants") {
"dec a".hasInstr("dec a") shouldBe true
"dec a".hasInstr("dec a") shouldBe true
"dec var".hasInstr("dec a") shouldBe false
}
// isBranch tests
test("isBranch: matches branch instructions") {
"beq skip".isBranch() shouldBe true
"bne loop".isBranch() shouldBe true
"bcc target".isBranch() shouldBe true
"bcs target".isBranch() shouldBe true
"bmi neg".isBranch() shouldBe true
"bpl pos".isBranch() shouldBe true
"bvc noov".isBranch() shouldBe true
"bvs ov".isBranch() shouldBe true
"bra somewhere".isBranch() shouldBe true
}
test("isBranch: matches branch instructions with trailing comments") {
"beq skip ; branch if equal".isBranch() shouldBe true
"bne loop ; branch if not equal".isBranch() shouldBe true
"bra target ; always branch".isBranch() shouldBe true
}
test("isBranch: does not match non-branch instructions") {
"lda #1".isBranch() shouldBe false
"sta var".isBranch() shouldBe false
"rts".isBranch() shouldBe false
"nop".isBranch() shouldBe false
}
// isStoreReg tests
test("isStoreReg: matches store instructions") {
"sta var".isStoreReg() shouldBe true
"sty var".isStoreReg() shouldBe true
"stx var".isStoreReg() shouldBe true
}
test("isStoreReg: matches store instructions with trailing comments") {
"sta var ; store accumulator".isStoreReg() shouldBe true
"sty myvar ; store Y".isStoreReg() shouldBe true
"stx counter ; store X".isStoreReg() shouldBe true
}
test("isStoreReg: does not match non-store instructions") {
"lda var".isStoreReg() shouldBe false
"ldy var".isStoreReg() shouldBe false
"ldx var".isStoreReg() shouldBe false
"sta".isStoreReg() shouldBe false // no space after
}
// isStoreRegOrZero tests
test("isStoreRegOrZero: matches store and stz") {
"sta var".isStoreRegOrZero() shouldBe true
"sty var".isStoreRegOrZero() shouldBe true
"stx var".isStoreRegOrZero() shouldBe true
"stz var".isStoreRegOrZero() shouldBe true
}
test("isStoreRegOrZero: does not match non-store") {
"lda var".isStoreRegOrZero() shouldBe false
"stz".isStoreRegOrZero() shouldBe false // no space after
}
// isLoadReg tests
test("isLoadReg: matches load instructions") {
"lda var".isLoadReg() shouldBe true
"ldy var".isLoadReg() shouldBe true
"ldx var".isLoadReg() shouldBe true
}
test("isLoadReg: matches load instructions with trailing comments") {
"lda var ; load accumulator".isLoadReg() shouldBe true
"ldy #5 ; load Y".isLoadReg() shouldBe true
"ldx counter ; load X".isLoadReg() shouldBe true
}
test("isLoadReg: does not match non-load instructions") {
"sta var".isLoadReg() shouldBe false
"sty var".isLoadReg() shouldBe false
"stx var".isLoadReg() shouldBe false
"lda".isLoadReg() shouldBe false // no space after
}
// haslabel tests
test("haslabel: detects labels") {
haslabel("mylabel:") shouldBe true
haslabel("loop:") shouldBe true
haslabel("mylabel: lda #1") shouldBe true
haslabel("loop: iny") shouldBe true
}
test("haslabel: detects labels with trailing comments") {
haslabel("mylabel: ; this is a label") shouldBe true
haslabel("loop: lda #1 ; load immediate") shouldBe true
haslabel("skip: ; skip ahead") shouldBe true
}
test("haslabel: does not detect non-labels") {
haslabel(" lda #1") shouldBe false
haslabel(" iny") shouldBe false
haslabel("; comment") shouldBe false
haslabel("") shouldBe false
}
test("haslabel: handles labels with leading whitespace") {
haslabel(" label:") shouldBe true
haslabel("\tlabel:") shouldBe true
}
// keeplabel tests
test("keeplabel: extracts label from line") {
keeplabel("mylabel: lda #1") shouldBe "mylabel:"
keeplabel("loop: iny") shouldBe "loop:"
keeplabel("mylabel:") shouldBe "mylabel:"
}
test("keeplabel: extracts label from line with trailing comment") {
keeplabel("mylabel: lda #1 ; load immediate") shouldBe "mylabel:"
keeplabel("loop: ; just a label") shouldBe "loop:"
keeplabel("skip: rts ; return") shouldBe "skip:"
}
test("keeplabel: returns colon-only for non-label lines with leading whitespace") {
// keeplabel splits on first whitespace and returns first part + ":"
// For " lda #1", first part is empty string, so returns ":"
keeplabel(" lda #1") shouldBe ":"
keeplabel(" iny") shouldBe ":"
}
test("keeplabel: handles tab-separated labels") {
keeplabel("mylabel\tlda #1") shouldBe "mylabel:"
}
// ========================================================================
// KNOWN BUGS - These tests demonstrate current optimizer defects.
// See AsmOptimizer.kt header for the full list.
// ========================================================================
test("float copy init to different destinations should not be removed") {
val lines = mutableListOf(
// Lines 0-6: first float copy
" lda #1", " ldy #0", " sta floatA", " sty floatA+1",
" lda #1", " ldy #0", " jsr floats.copy_float",
// Lines 7-13: second float copy to DIFFERENT destination
" lda #1", " ldy #0", " sta floatB", " sty floatB+1",
" lda #1", " ldy #0", " jsr floats.copy_float"
)
val beforeOpt = lines.toList()
optimize(lines)
lines shouldBe beforeOpt
}
test("variable name containing 'y' should not trigger false positive") {
val lines = mutableListOf(
" ldy #0", " lda (ptr),y", " ora myvar", // 'y' in "myvar" no longer triggers false positive
" ldy #0", " sta (ptr),y", " rts",
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop"
)
// The second ldy should be removed since "ora" doesn't modify Y
optimize(lines)
lines shouldBe listOf(" ldy #0", " lda (ptr),y", " ora myvar", " sta (ptr),y", " rts",
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop")
}
test("actual Y-modifying instructions should still be detected") {
val lines = mutableListOf(
" ldy #0", " lda (ptr),y", " tay", // tay modifies Y, so ldy cannot be removed
" ldy #0", " sta (ptr),y", " rts",
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop"
)
// The second ldy should NOT be removed since "tay" modifies Y
optimize(lines)
lines shouldBe listOf(
" ldy #0", " lda (ptr),y", " tay",
" ldy #0", " sta (ptr),y", " rts",
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop")
}
test("modifiesYRegister: correctly identifies Y-modifying instructions") {
" ldy #0".modifiesYRegister() shouldBe true
" tay".modifiesYRegister() shouldBe true
" iny".modifiesYRegister() shouldBe true
" dey".modifiesYRegister() shouldBe true
" tya".modifiesYRegister() shouldBe true
" phy".modifiesYRegister() shouldBe true
" ply".modifiesYRegister() shouldBe true
" sty var".modifiesYRegister() shouldBe true
" ora myvar".modifiesYRegister() shouldBe false // false positive fixed
" lda #0".modifiesYRegister() shouldBe false
" cpy #0".modifiesYRegister() shouldBe false // compare doesn't modify
" jsr func".modifiesYRegister() shouldBe false
}
test("compiler-prefixed memory-mapped IO variable accesses are not optimized away") {
symbolTable.add(StMemVar("p8b_main.p8v_io_data", DataType.UBYTE, 0xd021u, null, null))
symbolTable.add(StMemVar("p8b_main.p8v_result", DataType.UBYTE, 0x0300u, null, null))
val lines = mutableListOf(
" lda p8b_main.p8v_io_data", " sta p8b_main.p8v_result",
" lda p8b_main.p8v_io_data", " sta p8b_main.p8v_result",
" lda p8b_main.p8v_io_data", " sta p8b_main.p8v_result",
" rts", " nop"
)
optimize(lines)
lines.size shouldBe 8 // all instructions preserved
lines shouldBe listOf(
" lda p8b_main.p8v_io_data", " sta p8b_main.p8v_result",
" lda p8b_main.p8v_io_data", " sta p8b_main.p8v_result",
" lda p8b_main.p8v_io_data", " sta p8b_main.p8v_result",
" rts", " nop"
)
}
})