Files
prog8/compiler/test/codegeneration/TestIfExpressionOptimizations.kt
T
2026-09-08 22:36:54 +02:00

425 lines
18 KiB
Kotlin

package prog8tests.codegeneration
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import prog8.code.target.Cx16Target
import prog8tests.helpers.compileText
import prog8tests.helpers.shouldContainInOrder
import kotlin.io.path.readText
class TestIfExpressionOptimizations : FunSpec({
val outputDir = tempdir().toPath()
test("if-expression operand swapping 1000 == w") {
val text = """
main {
sub start() {
uword @shared w = cx16.r0
ubyte result = if 1000 == w then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// 1000 == w should be swapped to w == 1000
// and generate optimized word comparison using AY registers:
// lda p8v_w | cmp #<1000 | bne false | ldy p8v_w+1 | cpy #>1000 | bne false
asm.shouldContainInOrder("ldy", "p8v_w+1", "lda", "p8v_w", "cmp", "#<1000", "bne", "ifexpr_false", "cpy", "#>1000", "bne", "ifexpr_false")
}
test("if-expression direct branching for byte") {
val text = """
main {
sub start() {
ubyte @shared b = cx16.r0L
ubyte result = if b < 20 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// b < 20 should use lda b | cmp #20 | bcs false
asm.shouldContainInOrder("lda", "p8v_b", "cmp", "#20", "bcs", "ifexpr_false")
}
test("if-expression bitwise AND optimization (BIT instruction)") {
val text = """
main {
sub start() {
ubyte @shared b = cx16.r0L
ubyte result = if (b & 128) != 0 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// b & 128 should use bit p8v_b | bpl false
asm.shouldContainInOrder("bit", "p8v_b", "bpl", "ifexpr_false")
}
test("if-expression pointer dereference condition") {
val text = """
main {
sub start() {
ubyte @shared x = 42
^^ubyte p = &&x
ubyte result = if p^^ == 0 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// p^^ should load indirect byte from pointer and branch on zero
asm.shouldContainInOrder("lda", "(p8v_p)", "cmp", "#0")
}
test("if-expression for word equals number") {
val text = """
main {
sub start() {
long @shared l = 0
l = l + 1
ubyte result = if l != 0 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// long != 0 should use ora chain
asm.shouldContainInOrder("lda", "p8v_l", "ora", "p8v_l+1", "ora", "p8v_l+2", "ora", "p8v_l+3", "beq", "ifexpr_false")
}
test("if-expression long comparison optimization") {
val text = """
main {
sub start() {
long @shared l = 0
l = l + 1
ubyte result = if l < 200000 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// 200000 = 0x030d40
// Signed long comparison uses sbc with lowercase hex literals and signed branch sequence
asm.shouldContainInOrder(
"sec",
"lda", "p8v_l", "sbc", $$"#$40",
"lda", "p8v_l+1", "sbc", $$"#$0d",
"lda", "p8v_l+2", "sbc", $$"#$03",
"lda", "p8v_l+3", "sbc", $$"#$00",
"bvc", "eor", "#128", "bpl", "ifexpr_false"
)
}
test("if-expression long == 0 optimization") {
val text = """
main {
sub start() {
long @shared l = 0
l = l + 1
ubyte result = if l == 0 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// long == 0 should use ora chain + bne false
asm.shouldContainInOrder("lda", "p8v_l", "ora", "p8v_l+1", "ora", "p8v_l+2", "ora", "p8v_l+3", "bne", "ifexpr_false")
}
test("if-expression long == constant optimization") {
val text = """
main {
sub start() {
long @shared l = 100000
ubyte result = if l == 100000 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// 100000 = $0186a0 -> $a0, $86, $01, $00
asm.shouldContainInOrder(
"lda", "p8v_l", "cmp", $$"#$a0", "bne", "ifexpr_false",
"lda", "p8v_l+1", "cmp", $$"#$86", "bne", "ifexpr_false",
"lda", "p8v_l+2", "cmp", $$"#$01", "bne", "ifexpr_false",
"lda", "p8v_l+3", "cmp", $$"#$00", "bne", "ifexpr_false"
)
}
test("if-expression long < 0 optimization") {
val text = """
main {
sub start() {
long @shared l = -1
ubyte result = if l < 0 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// long < 0 should use lda l+3 | bpl false
asm.shouldContainInOrder("lda", "p8v_l+3", "bpl", "ifexpr_false")
}
test("if-expression long >= 0 optimization") {
val text = """
main {
sub start() {
long @shared l = 1
ubyte result = if l >= 0 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// long >= 0 should use lda l+3 | bmi false
asm.shouldContainInOrder("lda", "p8v_l+3", "bmi", "ifexpr_false")
}
test("if-expression long > 0 optimization") {
val text = """
main {
sub start() {
long @shared l = 1
ubyte result = if l > 0 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// long > 0 should use lda l+3 | bmi false | ora l+2 | ora l+1 | ora l | beq false
asm.shouldContainInOrder("lda", "p8v_l+3", "bmi", "ifexpr_false", "ora", "p8v_l+2", "ora", "p8v_l+1", "ora", "p8v_l", "beq", "ifexpr_false")
}
test("if-expression word indexed < 0 optimization") {
val text = """
main {
word[10] @shared words
sub start() {
ubyte @shared i = 5
ubyte @shared result = if words[i] < 0 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// words[i] < 0 should use lda words_msb,y | bpl false
// it uses ldy p8v_i directly because it's a split array (no scaling needed)
asm.shouldContainInOrder("ldy", "p8v_i", "lda", "p8v_words_msb,y", "bpl", "ifexpr_false")
}
test("if-expression long indexed > 0 optimization") {
val text = """
main {
long[10] @shared longs
sub start() {
ubyte @shared i = 2
ubyte @shared result = if longs[i] > 0 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// longs[i] > 0 should use direct indexed access:
// lda longs+3,y | bmi false | ora longs+2,y | ora longs+1,y | ora longs,y | beq false
asm.shouldContainInOrder("lda", "p8v_longs+3,y", "bmi", "ifexpr_false", "ora", "p8v_longs+2,y", "ora", "p8v_longs+1,y", "ora", "p8v_longs,y", "beq", "ifexpr_false")
}
test("if-expression long from function < 0 optimization") {
val text = """
main {
sub getlong() -> long { return -1 }
sub start() {
ubyte result = if getlong() < 0 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// getlong() result will be in R14R15 for long return
asm.shouldContainInOrder("jsr", "p8s_getlong", "lda", "cx16.r14+3", "bpl", "ifexpr_false")
}
test("if-expression word == word optimization") {
val text = """
main {
sub start() {
uword @shared w1 = cx16.r0
uword @shared w2 = cx16.r1
ubyte result = if w1 == w2 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// w1 == w2 should use optimized direct comparison using AY registers
asm.shouldContainInOrder("cmp", "p8v_w2", "bne", "ifexpr_false", "cpy", "p8v_w2+1", "bne", "ifexpr_false")
}
test("if-expression word indexed == 0 optimization") {
val text = """
main {
uword[10] @shared words
sub start() {
ubyte @shared i = 5
ubyte @shared result = if words[i] == 0 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
asm.shouldContainInOrder("ldy", "p8v_i", "lda", "_lsb,y", "ora", "_msb,y", "bne", "ifexpr_false")
}
test("if-expression word split array indexed != 0 optimization") {
val text = """
main {
word[10] @shared words
sub start() {
ubyte @shared i = 2
ubyte @shared result = if words[i] != 0 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// word arrays are split by default
asm.shouldContainInOrder("ldy", "p8v_i", "lda", "_lsb,y", "ora", "_msb,y", "beq", "ifexpr_false")
}
test("if-expression word indexed <= 0 optimization") {
val text = """
main {
word[10] @shared words
sub start() {
ubyte i = 1
ubyte result = if words[i] <= 0 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
asm.shouldContainInOrder("lda", "_msb,y", "bmi +", "ora", "_lsb,y", "bne", "ifexpr_false", "+")
}
test("if-expression word indexed > 0 optimization") {
val text = """
main {
word[10] @shared words
sub start() {
ubyte i = 1
ubyte result = if words[i] > 0 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
asm.shouldContainInOrder("lda", "_msb,y", "bmi", "ifexpr_false", "ora", "_lsb,y", "beq", "ifexpr_false")
}
test("if-expression addressOf == 0 optimization") {
val text = """
main {
ubyte @shared x
sub start() {
ubyte result = if (&&x) == 0 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
asm.shouldContainInOrder("lda", "#<", "p8v_x", "ora", "#>", "p8v_x", "bne", "ifexpr_false")
}
test("if-expression word < word optimization") {
val text = """
main {
sub start() {
uword @shared w1 = cx16.r0
uword @shared w2 = cx16.r1
ubyte result = if w1 < w2 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// w1 < w2 should use cmp + bcs for unsigned or signed comparison
asm.shouldContainInOrder("lda", "p8v_w1", "cmp", "p8v_w2", "bcs", "ifexpr_false")
}
test("if-expression word equals zero optimization") {
val text = """
main {
sub start() {
uword @shared w = cx16.r0
ubyte result = if w == 0 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// w == 0 should use ora + bne for simple zero check
asm.shouldContainInOrder("ora", "p8v_w+1", "bne", "ifexpr_false")
}
test("if-expression long == long optimization") {
val text = """
main {
sub start() {
long @shared l1 = 12345
long @shared l2 = 67890
ubyte result = if l1 == l2 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// l1 == l2 should use optimized variable comparison (branch to skip when differ)
asm.shouldContainInOrder("cmp", "p8v_l2", "bne", "+", "cmp", "p8v_l2+1", "bne", "+", "cmp", "p8v_l2+2", "bne", "+", "cmp", "p8v_l2+3", "+")
}
test("if-expression long != long optimization") {
val text = """
main {
sub start() {
long @shared l1 = 12345
long @shared l2 = 67890
ubyte result = if l1 != l2 then 1 else 0
cx16.r1L = result
}
}
""".trimIndent()
val result = compileText(Cx16Target(), false, text, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
// l1 != l2 should use optimized variable comparison with skip label pattern:
// cmp p8v_l2, bne skip, ..., cmp p8v_l2+3, beq ifexpr_false, skip:
asm.shouldContainInOrder("cmp p8v_l2", "bne +", "cmp p8v_l2+3", "beq", "ifexpr_false")
}
})