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

1152 lines
32 KiB
Kotlin

package prog8tests.codegeneration
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.ints.shouldBeGreaterThan
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldStartWith
import io.kotest.matchers.types.instanceOf
import io.kotest.matchers.types.shouldBeInstanceOf
import prog8.code.StNodeType
import prog8.code.ast.*
import prog8.code.core.AssemblyError
import prog8.code.core.BaseDataType
import prog8.code.core.DataType
import prog8.code.target.*
import prog8tests.helpers.*
import kotlin.io.path.readText
class TestVariousCodeGen: FunSpec({
val outputDir = tempdir().toPath()
test("void subroutine returning the result of a void function call compiles on 6502") {
// Regression test: "return voidfunc(args)" inside a void subroutine is valid Prog8.
// The optimizer only rewrites this to "voidfunc(args); return" when it can inline the
// caller, so in general it reaches the 6502 codegen as a Return whose value expression is a
// void function call. The 6502 backend must call it for its side effects and then return.
val text = """
main {
ubyte flag
sub start() {
flag = 1
benchmark(flag)
}
sub benchmark(ubyte f) {
when f {
1 -> return helper(f)
else -> {
}
}
}
sub helper(ubyte x) {
}
}
"""
compileText(Cx16Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
compileText(Cx16Target(), true, text, outputDir, writeAssembly = true) shouldNotBe null
}
test("nested scoping") {
val text="""
main {
sub start() {
testscope.duplicate()
cx16.r0L = testscope.duplicate2()
}
}
testscope {
sub sub1() {
ubyte @shared duplicate
ubyte @shared duplicate2
}
sub duplicate() {
; do nothing
}
sub duplicate2() -> ubyte {
return cx16.r0L
}
}"""
compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
}
test("word array indexing") {
val text="""
main {
sub start() {
uword[3] seed
cx16.r0 = seed[0] + seed[1] + seed[2]
}
}"""
compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
}
test("ast result from compileText") {
val text="""
main {
sub start() {
uword[3] @nosplit seed
cx16.r0 = seed[0] + seed[1] + seed[2]
}
}"""
val result = compileText(C64Target(), false, text, outputDir, writeAssembly = true)!!
result.compilerAst.name shouldStartWith "on_the_fly"
val ast = result.codegenAst!!
ast.name shouldBe result.compilerAst.name
ast.children.size shouldBeGreaterThan 2
val start = ast.entrypoint()!!
start.name shouldBe "p8s_start"
start.children.size shouldBeGreaterThan 2
val seed = start.children[1] as PtVariable
seed.name shouldBe "p8v_seed"
seed.value shouldBe null
seed.type shouldBe DataType.arrayFor(BaseDataType.UWORD, DummyMemsizer)
val assign = start.children[2] as PtAssignment
assign.target.identifier!!.name shouldBe "cx16.r0"
assign.value shouldBe instanceOf<PtBinaryExpression>()
val st = result.codegenSymboltable!!
st.flat.size shouldBeGreaterThan 100
st.flat["cbm.CHROUT"]?.type shouldBe StNodeType.EXTSUB
st.lookup("cbm.CHROUT")?.type shouldBe StNodeType.EXTSUB
}
test("peek and poke argument types") {
val text="""
main {
sub start() {
uword[3] arr
ubyte @shared i = 42
uword @shared ww = peekw(arr[i])
ubyte @shared xx = peek(arr[i])
xx = @(arr[i])
@(arr[i]) = 42
poke(arr[i], 42)
pokew(arr[i], 4242)
}
}"""
compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
}
test("assigning memory byte into arrays works") {
val text="""
main {
sub start() {
uword @shared factor1
ubyte[3] bytearray
uword[3] wordarray
@(factor1) = bytearray[0]
bytearray[0] = @(factor1)
@(factor1) = lsb(wordarray[0])
wordarray[0] = @(factor1)
@(5000) = bytearray[0]
@(5000) = lsb(wordarray[0])
bytearray[0] = @(5000)
wordarray[0] = @(5000)
}
}"""
compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
}
test("reading memory from unknown var gives proper error") {
val text="""
main {
sub start() {
cx16.r0L = @(doesnotexist)
}
}"""
val errors = ErrorReporterForTests()
compileText(C64Target(), false, text, outputDir, writeAssembly = true, errors = errors)
errors.errors.size shouldBe 2
errors.errors[0] shouldContain "invalid address type"
errors.errors[1] shouldContain "undefined symbol: doesnotexist"
}
test("shifting by word value is ok") {
val text= $$"""
main {
sub start() {
ubyte @shared c = 1
@(15000 + c<<$0003) = 42
@(15000 + (c<<$0003)) = 42
@(15000 + c*$0008) = 42 ; *8 becomes a shift after opt
uword @shared qq = 15000 + c<<$0003
qq = 15000 + (c<<$0003)
qq = 16000 + c*$0008
}
}"""
compileText(C64Target(), true, text, outputDir, writeAssembly = true) shouldNotBe null
compileText(VMTarget(), true, text, outputDir, writeAssembly = true) shouldNotBe null
}
test("builtin func in float expression") {
val src="""
%import floats
main {
sub start() {
float @shared fl =25.1
fl = abs(fl)+0.5
fl = sqrt(fl)+0.5
}
}"""
compileText(C64Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("string vars in inlined subroutines are ok") {
val src="""
main {
sub start() {
void block2.curdir()
void block2.other()
}
}
block2 {
str result="zzzz"
sub curdir() -> str {
return result
}
sub other() -> str {
return "other"
}
}"""
compileText(C64Target(), true, src, outputDir, writeAssembly = true) shouldNotBe null
compileText(VMTarget(), true, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("array with pointers") {
val src = """
main {
sub start() {
str localstr = "hello"
ubyte[] otherarray = [1,2,3]
uword[] words = [1111,2222,"three",&localstr,&otherarray]
uword @shared zz = &words
bool @shared result = 2222 in words
zz = words[2]
zz++
zz = words[3]
}
}"""
val othertarget = Cx16Target()
compileText(othertarget, true, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("case sensitive symbols") {
val src = """
main {
sub start() {
ubyte bytevar = 11 ; var at 0
ubyte byteVAR = 22 ; var at 1
ubyte ByteVar = 33 ; var at 2
ubyte @shared total = bytevar+byteVAR+ByteVar ; var at 3
goto skipLABEL
SkipLabel:
return
skipLABEL:
bytevar = 42
}
}"""
val target = Cx16Target()
compileText(target, true, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("addresses from labels/subroutines") {
val src = """
main {
sub start() {
mylabel:
ubyte @shared variable
uword @shared pointer1 = &main.start
uword @shared pointer2 = &start
uword @shared pointer3 = &main.start.mylabel
uword @shared pointer4 = &mylabel
uword[] @shared ptrs = [&variable, &start, &main.start, &mylabel, &main.start.mylabel]
}
}
"""
compileText(Cx16Target(), true, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("duplicate symbols okay other block and variable") {
val src = """
main {
ubyte derp
sub start() {
derp++
foo.bar()
}
}
foo {
sub bar() {
derp.print()
}
}
derp {
sub print() {
cx16.r0++
cx16.r1++
}
}"""
compileText(VMTarget(), false, src, outputDir, writeAssembly = true) shouldNotBe null
compileText(Cx16Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("ambiguous symbol name variable vs block") {
val src = """
main {
sub start() {
uword module
module++
module.test++
}
}
module {
ubyte @shared test
}
"""
val errors=ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "ambiguous symbol"
}
test("prefix expressions with typecasting") {
val src="""
main
{
sub start()
{
uword @shared uw = 54321
ubyte @shared ub = 123
word @shared sw = -12345
byte @shared sb = -123
func_uw(~ub as uword)
func_ub(~uw as ubyte)
func_uw(~sb as uword)
func_ub(~sw as ubyte)
func_w(-sb as word)
func_b(-sw as byte)
}
sub func_uw(uword arg) {
}
sub func_w(word arg) {
}
sub func_ub(ubyte arg) {
}
sub func_b(byte arg) {
}
}"""
compileText(VMTarget(), false, src, outputDir, writeAssembly = true) shouldNotBe null
compileText(Cx16Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("inlining sub with 2 statements") {
val src="""
main {
sub start() {
init()
}
sub init() {
init_handler()
return
}
sub init_handler() {
cx16.r0++
}
}"""
compileText(VMTarget(), true, src, outputDir, writeAssembly = false) shouldNotBe null
compileText(Cx16Target(), true, src, outputDir, writeAssembly = false) shouldNotBe null
}
test("syslib correctly available for raw outputs") {
val text = $$"""
%output raw
%launcher none
%address $2000
main {
sub start() {
cx16.r0++
sys.clear_carry()
}
}
"""
compileText(Cx16Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
compileText(C128Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
compileText(PETTarget(), false, text, outputDir, writeAssembly = true) shouldNotBe null
compileText(VMTarget(), false, text, outputDir, writeAssembly = true) shouldNotBe null
}
test("for loops with just 1 iteration") {
val src="""
main {
sub start() {
for cx16.r0L in 100 downto 100 {
cx16.r1++
}
for cx16.r0L in 100 to 100 {
cx16.r1++
}
for cx16.r0 in 2222 downto 2222 {
cx16.r1++
}
for cx16.r0 in 2222 to 2222 {
cx16.r1++
}
for cx16.r0L in 100 downto 100 step -5 {
cx16.r1++
}
for cx16.r0L in 100 to 100 step 5 {
cx16.r1++
}
for cx16.r0 in 2222 downto 2222 step -5 {
cx16.r1++
}
for cx16.r0 in 2222 to 2222 step 5 {
cx16.r1++
}
}
}"""
compileText(VMTarget(), true, src, outputDir, writeAssembly = true) shouldNotBe null
compileText(VMTarget(), false, src, outputDir, writeAssembly = true) shouldNotBe null
compileText(Cx16Target(), true, src, outputDir, writeAssembly = true) shouldNotBe null
compileText(Cx16Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("dynamic byte and word for loops use legacy-6502-safe code") {
val src = """
main {
sub start() {
byte b
byte @shared bfrom = 1
byte @shared bto = 5
byte @shared bstep = 2
for b in bfrom to bto step bstep {
b++
}
uword w
uword @shared wfrom = 1
uword @shared wto = 5
uword @shared wstep = 2
for w in wfrom to wto step wstep {
w++
}
}
}
""".trimIndent()
listOf(C64Target(), C128Target(), PETTarget()).forEach { target ->
val result = compileText(target, false, src, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
asm.contains("for_modified") shouldBe false
asm.lines().map { it.trim() }.none { it == "stz" || it.startsWith("stz ") } shouldBe true
asm.lines().map { it.trim() }.none { it == "bra" || it.startsWith("bra ") } shouldBe true
}
}
test("dynamic long for loop step is rejected by the legacy backend") {
val src = """
main {
sub start() {
long i
for i in 0 to 10 step dynamicStep() {
}
}
sub dynamicStep() -> long {
return 1
}
}
""".trimIndent()
shouldThrow<AssemblyError> {
compileText(C64Target(), false, src, outputDir, writeAssembly = true)
}
}
test("multiple status flags return values from asmsub") {
val src="""
main {
extsub 5000 = carryAndNegativeAndByteAndWord() -> bool @Pc, bool @Pn, ubyte @X, uword @AY
sub start() {
ubyte @shared x
uword @shared w
bool @shared flag1
bool @shared flag2
flag1, flag2, x, w = carryAndNegativeAndByteAndWord()
flag1, void, void, w = carryAndNegativeAndByteAndWord()
void, void, void, void = carryAndNegativeAndByteAndWord()
void carryAndNegativeAndByteAndWord()
}
}"""
compileText(Cx16Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("extsub with float return in multi-assign") {
val src="""
%import floats
main {
extsub 5000 = get_float_and_byte() -> float @FAC1, ubyte @X
sub start() {
float @shared f
ubyte @shared b
f, b = get_float_and_byte()
}
}"""
val errors = ErrorReporterForTests()
compileText(Cx16Target(), false, src, outputDir, writeAssembly = true, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
}
test("extsub with mixed returns including float") {
// Test with multiple return types including FP, status flags, and CPU registers
val src="""
%import floats
main {
extsub 5000 = get_multi() -> bool @Pc, float @FAC1, ubyte @X, uword @AY
sub start() {
bool @shared flag
float @shared f
ubyte @shared b
uword @shared w
flag, f, b, w = get_multi()
void, f, void, void = get_multi()
}
}"""
val errors = ErrorReporterForTests()
compileText(Cx16Target(), false, src, outputDir, writeAssembly = true, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
}
test("regular sub with float return in multi-assign") {
// Test that regular Prog8 subroutines with float returns work correctly
// Float can be in any position now - the compiler ensures FAC1 is assigned last
// to avoid being clobbered by other return value computations
val src="""
%import floats
main {
sub start() {
bool flag
ubyte b
float f
flag, b, f = multi_test(42.0)
}
sub multi_test(float input) -> bool, ubyte, float {
return true, input as ubyte, 3.14
}
}"""
val errors = ErrorReporterForTests()
compileText(Cx16Target(), false, src, outputDir, writeAssembly = true, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
}
test("regular sub with float in middle of multi-return") {
// Test float as the middle return value - this used to fail because
// computing the ubyte return value clobbered FAC1
val src="""
%import floats
main {
sub start() {
bool flag
float f
ubyte b
flag, f, b = multi_test(42.0)
}
sub multi_test(float input) -> bool, float, ubyte {
return true, 3.14, input as ubyte
}
}"""
val errors = ErrorReporterForTests()
compileText(Cx16Target(), false, src, outputDir, writeAssembly = true, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
}
test("regular sub with float first in multi-return") {
// Test float as the first return value
val src="""
%import floats
main {
sub start() {
float f
bool flag
ubyte b
f, flag, b = multi_test(42.0)
}
sub multi_test(float input) -> float, bool, ubyte {
return 3.14, true, input as ubyte
}
}"""
val errors = ErrorReporterForTests()
compileText(Cx16Target(), false, src, outputDir, writeAssembly = true, errors = errors) shouldNotBe null
errors.errors.size shouldBe 0
}
test("missing rts in asmsub") {
val src="""
main {
sub start() {
test()
test2()
}
asmsub test() {
%asm {{
nop
nop
}}
}
inline asmsub test2() {
%asm {{
nop
nop
}}
}
}"""
val errors = ErrorReporterForTests()
compileText(C64Target(), false, src, outputDir, writeAssembly = true, errors = errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "asmsub seems to never return"
}
test("missing rts in asmsub suppressed") {
val src="""
main {
sub start() {
test()
}
asmsub test() {
%asm {{
nop
nop
; !notreached!
}}
}
}"""
compileText(C64Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("inline asmsub is inlined at call site, not called via jsr") {
val src="""
%zeropage basicsafe
%option no_sysinit
main {
sub start() {
test2()
}
inline asmsub test2() {
%asm {{
lda #42
}}
}
}"""
val result = compileText(C64Target(), false, src, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
val lines = asm.lines().map { it.trim() }
// inline asmsub body should appear directly in the caller
lines.any { it == "lda #42" } shouldBe true
// should NOT have a separate subroutine definition for the inline asmsub
lines.count { it.contains("p8s_test2") && it.contains(".proc") } shouldBe 0
// should NOT have a jsr to the inline asmsub
lines.count { it.startsWith("jsr") && it.contains("p8s_test2") } shouldBe 0
}
test("regular asmsub is called via jsr, not inlined") {
val src="""
%zeropage basicsafe
%option no_sysinit
main {
sub start() {
test()
}
asmsub test() {
%asm {{
lda #99
rts
}}
}
}"""
val result = compileText(C64Target(), false, src, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
val lines = asm.lines().map { it.trim() }
// regular asmsub should have its own .proc block
lines.any { it.contains("p8s_test") && it.contains(".proc") } shouldBe true
// should have a jsr to the asmsub
lines.any { it.startsWith("jsr") && it.contains("p8s_test") } shouldBe true
}
test("inline asm pipe operator is preserved as bitwise OR") {
val src="""
%zeropage basicsafe
%option no_sysinit
main {
sub start() {
%asm {{
lda #%00010000 | $1234>>16
}}
}
}"""
val result = compileText(C64Target(), false, src, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
val lines = asm.lines().map { it.trim() }
// the '|' must remain on the same line as the lda, not be split into a second line
lines.any { it == "lda #%00010000 | $1234>>16" } shouldBe true
lines.any { it == "$1234>>16" } shouldBe false
}
test("subroutine with register-mapped parameter is not inlined") {
val src= $$"""
%zeropage basicsafe
%option no_sysinit
main {
sub start() {
PlayVideo_STD(mkword($10, $20))
}
sub PlayVideo_STD(uword params @R0) {
TrampolineCallSTD($a000)
}
asmsub TrampolineCallSTD(uword addr @AY) clobbers(A, Y) {
%asm {{
rts
}}
}
}"""
val result = compileText(C64Target(), false, src, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
val lines = asm.lines().map { it.trim() }
// the R0 argument setup must be preserved because the inlined body doesn't
// explicitly reference the register-mapped parameter
lines.any { it == "sta cx16.r0" } shouldBe true
lines.any { it == "sty cx16.r0+1" } shouldBe true
lines.any { it.startsWith("jsr") && it.contains("p8s_PlayVideo_STD") } shouldBe true
}
test("if not without else is not swapped") {
val src="""
main {
sub start() {
if not thing()
cx16.r0++
}
sub thing() -> bool {
cx16.r0++
return false
}
}"""
compileText(C64Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = true)!!
val st = result.codegenAst!!.entrypoint()!!.children
st.size shouldBe 3
val ifelse = st[1] as PtIfElse
ifelse.hasElse() shouldBe false
(ifelse.condition as PtPrefix).operator shouldBe "not"
}
test("if not with else is swapped") {
val src="""
main {
sub start() {
if not thing()
cx16.r0++
else
cx16.r1++
}
sub thing() -> bool {
cx16.r0++
return false
}
}"""
compileText(C64Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = true)!!
val st = result.codegenAst!!.entrypoint()!!.children
st.size shouldBe 3
val ifelse = st[1] as PtIfElse
ifelse.hasElse() shouldBe true
ifelse.condition shouldBe instanceOf<PtFunctionCall>()
}
test("bit instruction is used for testing bits 6 and 7 of a byte") {
val text = $$"""
main {
sub start() {
if cx16.r0L & $80 != 0
return
if cx16.r1L & $80 == 0
return
if cx16.r2L & $40 != 0
return
if cx16.r3L & $40 == 0
return
cx16.r9L = if cx16.r4L & $80 != 0 11 else 22
cx16.r10L = if cx16.r5L & $40 == 0 11 else 22
}
}"""
val result = compileText(C64Target(), true, text, outputDir, writeAssembly = true)!!
val assemblyFile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm")
val assembly = assemblyFile.readText()
assembly.shouldContainInOrder(
"bit cx16.r0L",
"bit cx16.r1L",
"bit cx16.r2L",
"bit cx16.r3L",
"bit cx16.r4L",
"bit cx16.r5L"
)
val resultIR = compileText(VMTarget(), true, text, outputDir, writeAssembly = true)!!
val irFile = resultIR.compilationOptions.outputDir.resolve(resultIR.compilerAst.name + ".p8ir")
val ir = irFile.readText()
ir.shouldContainInOrder(
"bittst.b", // bit 7 test (r0)
"bittst.b", // bit 7 test (r1)
"bittst.b", // bit 6 test (r2)
"bittst.b", // bit 6 test (r3)
"bittst.b", // bit 7 test (r4)
"bittst.b" // bit 6 test (r5)
)
}
test("strings in if expression") {
val src="""
main {
sub start() {
str foo = "foo"
str bar = "bar"
bool @shared flag = true
uword @shared foobar = if flag foo else bar
}
}"""
compileText(C64Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
compileText(VMTarget(), false, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("word bitshift with byte operand") {
val src="""
main{
sub start() {
cx16.r0 >>= 4
cx16.r1 <<= 4
}
}"""
compileText(C64Target(), false, src, outputDir) shouldNotBe null
compileText(VMTarget(), false, src, outputDir) shouldNotBe null
compileText(C64Target(), true, src, outputDir) shouldNotBe null
compileText(VMTarget(), true, src, outputDir) shouldNotBe null
}
test("symbol prefixing inconsistency with struct types and merged blocks") {
// see https://github.com/irmen/prog8/issues/198
val src="""
plane {
struct Point {
ubyte x
ubyte y
}
}
mytxt {
%option no_symbol_prefixing
sub print_ub(ubyte value) {
cx16.r0L++
}
}
mytxt {
%option merge
sub print_pt(^^plane.Point p) {
mytxt.print_ub(p.x)
mytxt.print_ub(p.y)
}
}
main {
sub start() {
^^plane.Point origin = ^^plane.Point:[0,0]
mytxt.print_pt(origin)
}
}"""
compileText(C64Target(), false, src, outputDir) shouldNotBe null
}
test("long argument to asmsub") {
val src="""
main {
sub start() {
long @shared lv = 1234567
test(999999)
test(lv)
test(0)
}
asmsub test(long arg @R0R1) {
%asm {{
rts
}}
}
}"""
compileText(C64Target(), false, src, outputDir, writeAssembly = true)!!
compileText(Cx16Target(), false, src, outputDir, writeAssembly = true)!!
}
test("long argument and another to asmsub") {
val src= $$"""
main {
sub start() {
extcommand_print_ulhex($12345678, true)
extcommand_print_ulhex($abcdef99, false)
}
asmsub extcommand_print_ulhex(long value @R0R1, bool prefix @A) clobbers(A,X,Y) {
%asm {{
rts
}}
}
}"""
compileText(C64Target(), false, src, outputDir, writeAssembly = true)!!
compileText(Cx16Target(), false, src, outputDir, writeAssembly = true)!!
}
test("returning multivalue functioncall that can be short-circuited") {
val src= """
main {
sub start() {
ubyte a,b = multi()
}
sub multi() -> ubyte, ubyte {
cx16.r0++
return multi2()
}
sub multi2() -> ubyte, ubyte {
cx16.r0++
return 42, 99
}
}"""
compileText(Cx16Target(), false, src, outputDir) shouldNotBe null
compileText(VMTarget(), false, src, outputDir) shouldNotBe null
}
test("returning multivalue functioncall that cannot be short-circuited") {
val src= """
main {
sub start() {
ubyte a,b = multi()
}
sub multi() -> ubyte, ubyte {
cx16.r0++
return multi2(99)
}
sub multi2(ubyte x) -> ubyte, ubyte {
x++
return 42, 99
}
}"""
compileText(Cx16Target(), false, src, outputDir) shouldNotBe null
compileText(VMTarget(), false, src, outputDir) shouldNotBe null
}
test("efficient 6502 array indexing code for s[index+const]") {
val source = """
%zeropage basicsafe
main {
sub rstrip(str s) {
s[cx16.r0L+1] = 0
}
sub start() {
rstrip("hello")
}
}
""".trimIndent()
val result = compileText(Cx16Target(), true, source, outputDir)!!
// 1. Check SimpleAst (codegenAst)
// We expect operand order optimization to have swapped 's' and '(cx16.r0L + 1)'
// so that the pointer 's' is on the right, because that is the least complex operand
val additions = mutableListOf<PtBinaryExpression>()
walkAst(result.codegenAst!!) { node, _ ->
if (node is PtBinaryExpression && node.operator == "+" && !node.position.file.contains("library:")) {
additions.add(node)
}
true
}
// In the SimpleAst for this program, we expect:
// 1. cx16.r0L + 1
// 2. (cx16.r0L + 1) + main.rstrip.s
additions.size shouldBe 2
val outerAddition = additions.find { it.type.isPointer || it.type.isUnsignedWord }!!
withClue("Pointer 's' should be on the right due to operand order optimization") {
val right = outerAddition.right
right.shouldBeInstanceOf<PtIdentifier>()
right.name shouldContain "p8v_s"
}
// 2. Check Assembly
val asmFile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm")
val asm = asmFile.readText()
withClue("Should use efficient sta (zp),y instruction") {
asm.shouldContainInOrder("sta (p8v_s),y")
}
}
test("IfExpression string to pointer typecast") {
val src = """
%option no_sysinit
%launcher none
%address $1000
main {
sub start() {
str a = "hello"
str b = "world"
bool @shared flag = true
^^ubyte p = if flag then a else b
}
}
""".trimIndent()
// This should not crash during compilation
val compileResult = compileText(Cx16Target(), false, src, outputDir)
compileResult shouldNotBe null
// Also test with virtual target
val compileResultVM = compileText(VMTarget(), false, src, outputDir)
compileResultVM shouldNotBe null
}
test("BranchConditionExpression string to pointer typecast") {
val src = """
%option no_sysinit
%launcher none
%address $1000
main {
sub start() {
str a = "hello"
str b = "world"
^^ubyte p = if_z then a else b
}
}
""".trimIndent()
// This should not crash during compilation
val compileResult = compileText(Cx16Target(), false, src, outputDir)
compileResult shouldNotBe null
}
test("pointer assignment to variable in 6502 backend") {
val src = """
%option no_sysinit
%launcher none
%address $1000
main {
ubyte value = 42
^^ubyte @shared ptr = 0
sub start() {
ptr = &value
}
}
""".trimIndent()
// This should not crash during compilation
val compileResult = compileText(Cx16Target(), false, src, outputDir)
compileResult shouldNotBe null
// Verify it actually works by simulating
compileResult!!.simulate()
}
test("pointer assignment to array element in 6502 backend") {
val src = """
%option no_sysinit
%launcher none
%address $1000
main {
ubyte value = 42
^^ubyte[2] ptrs
sub start() {
ptrs[1] = &value
}
}
""".trimIndent()
val compileResult = compileText(Cx16Target(), false, src, outputDir)
compileResult shouldNotBe null
}
})