package prog8tests.compiler

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.types.instanceOf
import prog8.ast.IFunctionCall
import prog8.ast.expressions.*
import prog8.ast.printProgram
import prog8.ast.statements.Assignment
import prog8.ast.statements.IfElse
import prog8.code.core.BaseDataType
import prog8.code.core.DataType
import prog8.code.core.Position
import prog8.code.target.C64Target
import prog8.code.target.VMTarget
import prog8tests.helpers.ErrorReporterForTests
import prog8tests.helpers.compileText


class TestTypecasts: FunSpec({

    val outputDir = tempdir().toPath()

    test("integer args for builtin funcs") {
        val text = """
            %import floats
            main {
                sub start() {
                    float fl
                    floats.print(lsb(fl))
                }
            }"""
        val errors = ErrorReporterForTests()
        val result = compileText(C64Target(), false, text, outputDir, writeAssembly = false, errors = errors)
        result shouldBe null
        errors.errors.size shouldBe 1
        errors.errors[0] shouldContain "type mismatch, was: float expected one of: [UWORD, WORD, LONG]"
    }

    test("not casting bool operands to logical operators") {
        val text = """
            main {
                sub start() {
                    bool @shared bb2=true
                    bool @shared bb3=false
                    bool @shared bb = bb2 and bb3
                }
            }"""
        val result = compileText(C64Target(), false, text, outputDir, writeAssembly = false)!!
        val stmts = result.compilerAst.entrypoint.statements
        stmts.size shouldBe 7
        val expr = (stmts[5] as Assignment).value as BinaryExpression
        expr.operator shouldBe "and"
        (expr.left as IdentifierReference).nameInSource shouldBe listOf("bb2")  // no cast
        (expr.right as IdentifierReference).nameInSource shouldBe listOf("bb3")  // no cast

        val result2 = compileText(C64Target(), true, text, outputDir, writeAssembly = true)!!
        val stmts2 = result2.compilerAst.entrypoint.statements
        stmts2.size shouldBe 7
        val expr2 = (stmts2[5] as Assignment).value as BinaryExpression
        expr2.operator shouldBe "and"
        (expr2.left as IdentifierReference).nameInSource shouldBe listOf("bb2")  // no cast
        (expr2.right as IdentifierReference).nameInSource shouldBe listOf("bb3")  // no cast
    }

    test("bool expressions with functioncalls") {
        val text = """
main {
    sub ftrue(ubyte arg) -> ubyte {
        arg++
        return 42
    }
    
    sub btrue(ubyte arg) -> bool {
        arg++
        return true
    }

    sub start() {
        bool @shared ub1 = true
        bool @shared ub2 = true
        bool @shared ub3 = true
        bool @shared bvalue

        bvalue = ub1 xor ub2 xor ub3 xor true
        bvalue = ub1 xor ub2 xor ub3 xor ftrue(99)!=0
        bvalue = ub1 and ub2 and ftrue(99)!=0
        bvalue = ub1 xor ub2 xor ub3 xor btrue(99)
        bvalue = ub1 and ub2 and btrue(99)        
    }
}"""
        val result = compileText(C64Target(), true, text, outputDir, writeAssembly = true)!!
        val stmts = result.compilerAst.entrypoint.statements
        printProgram(result.compilerAst)
        /*
        bool @shared ub1
        ub1 = true
        bool @shared ub2
        ub2 = true
        bool @shared ub3
        ub3 = true
        bool @shared bvalue
        bvalue =  not ((ub1 xor ub2) xor ub3)
        bvalue = (((ub1 xor ub2) xor ub3) xor (ftrue(99)!=0))
        bvalue = ((ub1 and ub2) and (ftrue(99)!=0))
        bvalue = (((ub1 xor ub2) xor ub3) xor btrue(99))
        bvalue = ((ub1 and ub2) and btrue(99))
        return
         */
        stmts.size shouldBe 13
        val assignValue1 = (stmts[7] as Assignment).value as PrefixExpression
        val assignValue2 = (stmts[8] as Assignment).value as BinaryExpression
        val assignValue3 = (stmts[9] as Assignment).value as BinaryExpression
        val assignValue4 = (stmts[10] as Assignment).value as BinaryExpression
        val assignValue5 = (stmts[11] as Assignment).value as BinaryExpression
        assignValue1.operator shouldBe "not"
        assignValue2.operator shouldBe "xor"
        assignValue3.operator shouldBe "and"
        assignValue4.operator shouldBe "xor"
        assignValue5.operator shouldBe "and"
        val right2 = assignValue2.right as BinaryExpression
        val right3 = assignValue3.right as BinaryExpression
        right2.operator shouldBe "!="
        right3.operator shouldBe "!="
        right2.left shouldBe instanceOf<IFunctionCall>()
        right2.right shouldBe NumericLiteral(BaseDataType.UBYTE, 0.0, Position.DUMMY)
        right3.left shouldBe instanceOf<IFunctionCall>()
        right3.right shouldBe NumericLiteral(BaseDataType.UBYTE, 0.0, Position.DUMMY)
        assignValue4.right shouldBe instanceOf<IFunctionCall>()
        assignValue5.right shouldBe instanceOf<IFunctionCall>()
    }

    test("correct handling of bool parameters") {
        val text = """
            main  {
            
                sub thing(bool b1, bool b2) -> bool {
                    return (b1 and b2) or b1
                }
            
                sub start() {
                    bool boolvalue1 = true
                    bool boolvalue2 = false
            
                    boolvalue1 = thing(true, false)
                    boolvalue2 = thing(false, true)
            
                    if boolvalue1 and boolvalue2
                        boolvalue1=false
                 }
            }"""
        val result = compileText(C64Target(), false, text, outputDir, writeAssembly = false)!!
        val stmts = result.compilerAst.entrypoint.statements
        stmts.size shouldBe 8
        val fcall1 = ((stmts[4] as Assignment).value as IFunctionCall)
        fcall1.args[0] shouldBe NumericLiteral(BaseDataType.BOOL, 1.0, Position.DUMMY)
        fcall1.args[1] shouldBe NumericLiteral(BaseDataType.BOOL, 0.0, Position.DUMMY)
        val fcall2 = ((stmts[5] as Assignment).value as IFunctionCall)
        fcall2.args[0] shouldBe NumericLiteral(BaseDataType.BOOL, 0.0, Position.DUMMY)
        fcall2.args[1] shouldBe NumericLiteral(BaseDataType.BOOL, 1.0, Position.DUMMY)
        val ifCond = (stmts[6] as IfElse).condition as BinaryExpression
        ifCond.operator shouldBe "and" // no asm writing so logical expressions haven't been replaced with bitwise equivalents yet
        (ifCond.left as IdentifierReference).nameInSource shouldBe listOf("boolvalue1")
        (ifCond.right as IdentifierReference).nameInSource shouldBe listOf("boolvalue2")
    }

    test("correct evaluation of words in boolean expressions") {
        val text = """
            main {
                sub start() {
                    uword camg
                    bool @shared interlaced
                    interlaced = (camg & $0004) != 0
                    cx16.r0L++
                    interlaced = ($0004 & camg) != 0
                    cx16.r0L++
                    uword @shared ww
                    ww = (camg & $0004)
                    ww++
                    ww = ($0004 & camg)
                    ubyte @shared value
                    bool @shared collected = (value >= $33) or (value >= $66) or (value >= $99) or (value >= ${'$'}CC)
                }
            }"""
        val result = compileText(C64Target(), false, text, outputDir, writeAssembly = true)!!
        val stmts = result.compilerAst.entrypoint.statements
        stmts.size shouldBeGreaterThan 10
    }

    test("word to byte casts") {
        val text = """
            %import textio
            main {
                sub func(ubyte arg) -> word {
                    return arg-99
                }
            
                sub start() {
                    txt.print_ub(func(0) as ubyte)
                    txt.print_uw(func(0) as ubyte)
                    txt.print_w(func(0) as ubyte)
                }
            }"""
        val result = compileText(C64Target(), false, text, outputDir, writeAssembly = false)!!
        val stmts = result.compilerAst.entrypoint.statements
        stmts.size shouldBe 4
    }

    test("ubyte to word casts") {
        val src = """
main {
    sub start() {
        ubyte @shared bb = 255
        cx16.r0s = (bb as byte) as word     ; should result in -1 word value
        cx16.r1s = (bb as word)             ; should result in 255 word value
    }
}"""

        val result = compileText(C64Target(), true, src, outputDir, writeAssembly = false)!!
        val stmts = result.compilerAst.entrypoint.statements
        stmts.size shouldBe 5
        val assign1tc = (stmts[2] as Assignment).value as TypecastExpression
        val assign2tc = (stmts[3] as Assignment).value as TypecastExpression
        assign1tc.type shouldBe DataType.WORD
        assign2tc.type shouldBe DataType.WORD
        assign2tc.expression shouldBe instanceOf<IdentifierReference>()
        val assign1subtc = (assign1tc.expression as TypecastExpression)
        assign1subtc.type shouldBe DataType.BYTE
        assign1subtc.expression shouldBe instanceOf<IdentifierReference>()
    }

    test("add missing & to function arguments") {
        val text = """
            main  {
            
                sub handler(uword fptr) {
                }
            
                sub start() {
                    uword variable
            
                    sys.pushw(variable)
                    sys.pushw(handler)
                    sys.pushw(&handler)
                    handler(variable)
                    handler(handler)
                    handler(&handler)
                }
            }"""
        val result = compileText(C64Target(), false, text, outputDir, writeAssembly = false)!!
        val stmts = result.compilerAst.entrypoint.statements
        stmts.size shouldBe 9
        val arg1 = (stmts[2] as IFunctionCall).args.single()
        val arg2 = (stmts[3] as IFunctionCall).args.single()
        val arg3 = (stmts[4] as IFunctionCall).args.single()
        val arg4 = (stmts[5] as IFunctionCall).args.single()
        val arg5 = (stmts[6] as IFunctionCall).args.single()
        val arg6 = (stmts[7] as IFunctionCall).args.single()
        arg1 shouldBe instanceOf<IdentifierReference>()
        arg2 shouldBe instanceOf<AddressOf>()
        arg3 shouldBe instanceOf<AddressOf>()
        arg4 shouldBe instanceOf<IdentifierReference>()
        arg5 shouldBe instanceOf<AddressOf>()
        arg6 shouldBe instanceOf<AddressOf>()
    }

    test("correct typecasts") {
        val text = """
            %import floats
            
            main {
                sub start() {
                    float @shared fl = 3.456
                    uword @shared uw = 5555
                    byte @shared bb = -44

                    bb = uw as byte
                    uw = bb as uword
                    fl = uw as float
                    fl = bb as float
                    bb = fl as byte
                    uw = fl as uword
                    uw = 8888 + (bb as ubyte)
                }
            }
        """
        val result = compileText(C64Target(), false, text, outputDir, writeAssembly = true)!!
        result.compilerAst.entrypoint.statements.size shouldBe 14
    }

    test("invalid typecasts of numbers") {
        val text = """
            %import floats
            
            main {
                sub start() {
                    ubyte @shared bb

                    bb = 5555 as ubyte
                    routine(5555 as ubyte)
                }
                
                sub routine(ubyte bb) {
                    bb++
                }
            }
        """
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, text, outputDir, writeAssembly = true, errors = errors) shouldBe null
        errors.errors.size shouldBe 2
        errors.errors[0] shouldContain "no cast"
        errors.errors[1] shouldContain "no cast"
    }

    test("allow explicit float literal cast to integer") {
        val text = """
            %option enable_floats
            main {
                sub start() {
                    cx16.r0 = 1234.5678 as uword
                    cx16.r1L = 99.333 as ubyte
                }
            }"""
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, text, outputDir, errors=errors) shouldNotBe null
        errors.errors.size shouldBe 0
    }

    test("refuse to truncate float inplace") {
        val text = """
            %option enable_floats
            main {
                sub start() {
                    float @shared fl = 3.456
                    fl++
                    fl = fl as uword
                }
            }"""
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, text, outputDir, errors = errors) shouldBe null
        errors.errors.size shouldBe 1
        errors.errors[0] shouldContain "in-place makes no sense"
    }

    test("correct implicit casts of signed number comparison and logical expressions") {
        val text = """
            %import floats
            
            main {
                sub start() {
                    byte @shared bb = -10
                    word @shared ww = -1000
                    
                    if bb>0 {
                        bb++
                    }
                    if bb < 0 {
                        bb ++
                    }
                    if bb & 1 !=0 {
                        bb++
                    }
                    if bb & 128 !=0 {
                        bb++
                    }
                    if bb & 255 !=0 {
                        bb++
                    }

                    if ww>0 {
                        ww++
                    }
                    if ww < 0 {
                        ww ++
                    }
                    if ww & 1 !=0 {
                        ww++
                    }
                    if ww & 32768 != 0 {
                        ww++
                    }
                    if ww & 65535 != 0 {
                        ww++
                    }
                }
            }
        """
        val result = compileText(C64Target(), false, text, outputDir, writeAssembly = true)!!
        val statements = result.compilerAst.entrypoint.statements
        statements.size shouldBeGreaterThan 10
    }

    test("cast to unsigned in conditional") {
        val text = """
            main {
                sub start() {
                    byte @shared bb
                    word @shared ww
            
                    bool @shared iteration_in_progress
                    uword @shared num_bytes

                    if not iteration_in_progress or num_bytes==0 {
                        num_bytes++
                    }
        
                    if bb as ubyte !=0  {
                        bb++
                    }
                    if ww as uword !=0 {
                        ww++
                    }
                }
            }"""
        val result = compileText(C64Target(), false, text, outputDir, writeAssembly = true)!!
        val statements = result.compilerAst.entrypoint.statements
        statements.size shouldBeGreaterThan 10
    }

    test("no infinite typecast loop in assignment asmgen") {
        val text = """
            main {
                sub start() {
                    word @shared qq = calculate(33)
                }
            
                sub calculate(ubyte row) -> word {
                    return (8-(row as byte))
                }
            }
        """
        compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("(u)byte extend to word parameters") {
        val text = """
            main {
                sub start() {
                    byte @shared ub1 = -50
                    byte @shared ub2 = -51
                    byte @shared ub3 = -52
                    byte @shared ub4 = 100
                    word @shared ww = func(ub1, ub2, ub3, ub4)
                    ww = func(ub4, ub2, ub3, ub1)
                    ww=afunc(ub1, ub2, ub3, ub4)
                    ww=afunc(ub4, ub2, ub3, ub1)
                }
            
                sub func(word x1, word y1, word x2, word y2) -> word {
                    return x1
                }
            
                asmsub afunc(word x1 @R0, word y1 @R1, word x2 @R2, word y2 @R3) -> word @AY {
                    %asm {{
                        lda  cx16.r0
                        ldy  cx16.r0+1
                        rts
                    }}
                }
            }"""
        compileText(C64Target(), true, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("lsb msb used as args with word types") {
        val text = """
            main {
                sub start() {
                    uword xx=${'$'}ea31
                    uword @shared ww = plot(lsb(xx), msb(xx))
                }
            
                inline asmsub  plot(uword plotx @R0, uword ploty @R1) -> uword @AY{
                    %asm {{
                        lda  cx16.r0
                        ldy  cx16.r1
                        rts
                    }}
                }
            }"""
        compileText(C64Target(), true, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("memory reads byte into word variable") {
        val text = """
            main {
                sub start() {
                    uword @shared ww
                    uword address = $1000
                    ww = @(address+100)
                    ww = @(address+1000)
                    cx16.r0 = @(address+100)
                    cx16.r0 = @(address+1000)
                }
            }"""
        compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("various floating point casts don't crash the compiler") {
        val text = """
            %import floats
            
            main {
                sub score() -> ubyte {
                    cx16.r15++
                    return 5
                }
            
                sub start() {
                    float @shared total = 0
                    ubyte bb = 5
            
                    cx16.r0 = 5
                    total += cx16.r0 as float
                    total += score() as float
                    uword ww = 5
                    total += ww as float
                    total += bb as float
                    float result = score() as float
                    total += result
                }
            }"""
        compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
        compileText(C64Target(), true, text, outputDir, writeAssembly = true) shouldNotBe null
        compileText(VMTarget(), false, text, outputDir, writeAssembly = true) shouldNotBe null
        compileText(VMTarget(), true, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("byte when choices silently converted to word for convenience") {
        val text = """
main {
  sub start() {
    uword z = 3
    when z {
        1-> z++
        2-> z++
        else -> z++
    }
  }
}"""
        compileText(C64Target(), false, text, outputDir, writeAssembly = false) shouldNotBe null
    }

    test("returning smaller dt than returndt is ok") {
        val text = """
main {
    sub start() {
        void test()
    }
    
    sub test() -> uword {
        return cx16.r0L
    }
}"""
        compileText(C64Target(), false, text, outputDir, writeAssembly = false) shouldNotBe null
    }

    test("returning bigger dt than returndt is not ok") {
        val text = """
main {
    sub start() {
        void test()
    }
    
    sub test() -> ubyte {
        return cx16.r0
    }
}"""
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, text, outputDir, writeAssembly = false, errors = errors) shouldBe null
        errors.errors.single() shouldContain "doesn't match"
    }

    test("long type okay in const expr but otherwise overflow") {
        val src = """
main {
    sub start() {
        const ubyte HEIGHT=240
        uword large = 320*240/8/8
        thing(large)
        thing(320*240/8/8)
        thing(320*HEIGHT/8/8)
        thing(320*HEIGHT)       ; overflow
        large = 12345678        ; overflow
    }

    sub thing(uword value) {
        value++
    }
}"""
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
        errors.errors.size shouldBe 3
        errors.errors[0] shouldContain ":9:"
        errors.errors[0] shouldContain "no cast"
        errors.errors[1] shouldContain ":10:"
        errors.errors[1] shouldContain "out of range"
        errors.errors[2] shouldContain ":10:"
        errors.errors[2] shouldContain "doesn't match"
    }

    test("various bool typecasts and type mismatches") {
        val src = """
%option enable_floats

main {
    sub start() {

        float @shared fl
        ubyte @shared flags
        byte @shared flagss
        uword @shared flagsw
        word @shared flagssw
        bool @shared bflags = 123
        cx16.r0++
        bflags = 123
        cx16.r0++
        bflags = 123 as bool

        flags = bflags
        flagss = bflags
        flagsw = bflags
        flagssw = bflags
        fl = bflags
        bflags = flags
        bflags = flagss
        bflags = flagsw
        bflags = flagssw
        bflags = fl

        flags = bflags as ubyte
        flagss = bflags as byte
        flagsw = bflags as uword
        flagssw = bflags as word
        fl = bflags as float
        bflags = flags as bool
        bflags = flagss as bool
        bflags = flagsw as bool
        bflags = flagssw as bool
        bflags = fl as bool
    }
}"""
        val errors = ErrorReporterForTests()
        compileText(VMTarget(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
        errors.errors.size shouldBe 12
        errors.errors.all { "value type" in it } shouldBe true
        errors.errors.all { "doesn't match" in it } shouldBe true
    }

    test("bool to byte cast in expression is not allowed") {
        val text = """
main {
    sub start() {
        ubyte[3] values
        func1(22 in values)
        func2(22 in values)
        ubyte @shared qq = 22 in values
        byte @shared ww = 22 in values
    }
    sub func1(ubyte arg) {
        arg++
    }
    sub func2(byte arg) {
        arg++
    }
}"""
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, text, outputDir, writeAssembly = true, errors = errors) shouldBe null
        errors.errors.size shouldBe 4
        errors.errors[0] shouldContain ("argument 1 type mismatch")
        errors.errors[1] shouldContain ("argument 1 type mismatch")
        errors.errors[2] shouldContain ("value type bool doesn't match target")
        errors.errors[3] shouldContain ("value type bool doesn't match target")
    }

    test("bool function parameters correct typing and no implicit casts to bool") {
        val src = """
main {
    sub start() {
        ^^word @shared pointer
        bool @shared bb = func(true)
        void func(true)
        void func(0)
        void func(1)
        void func(42)
        void func(65535)
        void func(655.444)
        void func(cx16.r0)
        void func(pointer)
        cx16.r0L = func(true)
    }

    sub func(bool draw) -> bool {
        cx16.r0++
        return true
    }
}"""
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
        errors.errors.size shouldBe 8
        errors.errors[0] shouldContain ("type mismatch")
        errors.errors[1] shouldContain ("type mismatch")
        errors.errors[2] shouldContain ("type mismatch")
        errors.errors[3] shouldContain ("type mismatch")
        errors.errors[4] shouldContain ("type mismatch")
        errors.errors[5] shouldContain ("type mismatch")
        errors.errors[6] shouldContain ("type mismatch")
        errors.errors[7] shouldContain ("value type bool doesn't match target")
    }

    test("no implicit bool-to-int cast in assignment") {
        val src = """
main {
    sub start() {
        cx16.r0L = true
        cx16.r0L = true as ubyte
    }
}"""
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
        errors.errors.size shouldBe 1
        errors.errors[0] shouldContain ("value type bool doesn't match target")
    }

    test("no implicit int-to-bool cast") {
        val src = """
main {
    sub start() {
        func1(true)
        func2(true)
        func1(true as ubyte)
        func2(true as uword)
        bool @shared bb1 = 1
        bool @shared bb2 = 12345
        bool @shared bb3 = 1 as bool
        bool @shared bb4 = 12345 as bool
    }

    sub func1(ubyte ub) {
        cx16.r0++
    }

    sub func2(uword uw) {
        cx16.r0++
    }
}"""
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
        errors.errors.size shouldBe 4
        errors.errors[0] shouldContain (":4:15: no implicit cast")
        errors.errors[1] shouldContain (":5:15: no implicit cast")
        errors.errors[2] shouldContain (":8:28: value type ubyte doesn't match target")
        errors.errors[3] shouldContain (":9:28: value type uword doesn't match target")
    }

    test("return 0 for str converted to uword") {
        val src = """
main {
    sub start() {
        cx16.r0 = test()
    }

    sub test() -> str {
        cx16.r0++
        if cx16.r0L==255
            return 0

        return 42
    }
}"""
        compileText(VMTarget(), true, src, outputDir, writeAssembly = true) shouldNotBe null
        compileText(C64Target(), true, src, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("bool to word cast") {
        val src = """
main {
    sub start() {
        bool @shared flag, flag2
        cx16.r0L = (flag and flag2) as ubyte
        cx16.r0 = (flag and flag2) as uword
    }
}"""

        compileText(VMTarget(), false, src, outputDir, writeAssembly = true) shouldNotBe null
        compileText(C64Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("undefined symbol error instead of type cast error") {
        val src = """
main {
    const ubyte foo = 0
    ubyte bar = 0
    sub start() {
        when foo {
            notdefined -> bar = 1
            else -> bar = 2
        }
    }
}"""
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
        errors.errors.size shouldBe 2
        errors.errors[1] shouldContain "undefined symbol"
    }

    test("return unsigned values for signed results ok if value fits") {
        val src = """
main {
    sub start() {
        void foo()
        void bar()
        void overflow1()
        void overflow2()
    }

    sub foo() -> byte {
        return 42
    }
    sub bar() -> word {
        return 12345
    }
    sub overflow1() -> byte {
        return 200
    }
    sub overflow2() -> word {
        return 44444
    }
}"""
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
        errors.errors.size shouldBe 2
        errors.errors[0] shouldContain "17:16: return value type ubyte doesn't match subroutine return type byte"
        errors.errors[1] shouldContain "20:16: return value type uword doesn't match subroutine return type word"
    }

    test("if-expression adjusts different value types to common type") {
        val src = """
main {
    sub start() {
        cx16.r0sL = if cx16.r0L < cx16.r1L -1 else 1
    }
}"""

        val result = compileText(C64Target(), false, src, outputDir, writeAssembly = false)!!
        val program = result.compilerAst
        val st = program.entrypoint.statements
        st.size shouldBe 2
        val assign = st[0] as Assignment
        assign.target.inferType(program).getOrUndef().base shouldBe BaseDataType.BYTE
        val ifexpr = assign.value as IfExpression
        ifexpr.truevalue.inferType(program).getOrUndef().base shouldBe BaseDataType.BYTE
        ifexpr.falsevalue.inferType(program).getOrUndef().base shouldBe BaseDataType.BYTE
        ifexpr.truevalue shouldBe instanceOf<NumericLiteral>()
        ifexpr.falsevalue shouldBe instanceOf<NumericLiteral>()
    }

    test("correct data types of numeric literals in word/byte scenario") {
        val src = """
main {
    sub start() {
        const uword WIDTH = 40
        const uword WIDER = 400
        cx16.r0 = cx16.r0-1+WIDTH
        cx16.r0 = cx16.r0-1+WIDER 
        cx16.r0 = cx16.r0L * 5               ; byte multiplication
        cx16.r0 = cx16.r0L * $0005      ; word multiplication
    }
}"""
        val result = compileText(C64Target(), false, src, outputDir, writeAssembly = false)!!
        val program = result.compilerAst
        val st = program.entrypoint.statements
        st.size shouldBe 7
        val v1 = (st[2] as Assignment).value as BinaryExpression
        v1.operator shouldBe "+"
        (v1.left as IdentifierReference).nameInSource shouldBe listOf("cx16", "r0")
        (v1.right as NumericLiteral).type shouldBe BaseDataType.UWORD
        (v1.right as NumericLiteral).number shouldBe 39

        val v2 = (st[3] as Assignment).value as BinaryExpression
        v2.operator shouldBe "+"
        (v2.left as IdentifierReference).nameInSource shouldBe listOf("cx16", "r0")
        (v2.right as NumericLiteral).type shouldBe BaseDataType.UWORD
        (v2.right as NumericLiteral).number shouldBe 399

        val v3 = (st[4] as Assignment).value as TypecastExpression
        v3.type shouldBe DataType.UWORD
        val v3e = v3.expression as BinaryExpression
        v3e.operator shouldBe "*"
        (v3e.left as IdentifierReference).nameInSource shouldBe listOf("cx16", "r0L")
        (v3e.right as NumericLiteral).type shouldBe BaseDataType.UBYTE
        (v3e.right as NumericLiteral).number shouldBe 5

        val v4 = (st[5] as Assignment).value as BinaryExpression
        v4.operator shouldBe "*"
        val v4t = v4.left as TypecastExpression
        v4t.type shouldBe DataType.UWORD
        (v4t.expression as IdentifierReference).nameInSource shouldBe listOf("cx16", "r0L")
        (v4.right as NumericLiteral).type shouldBe BaseDataType.UWORD
        (v4.right as NumericLiteral).number shouldBe 5
    }

    test("allow comparisons against constant values with different type") {
        val src = """
main {
    sub start() {
        const uword MAX_CAVE_WIDTH = 440             ; word here to avoid having to cast to word all the time

        if cx16.r0L > MAX_CAVE_WIDTH
            return
    }
}"""
        compileText(C64Target(), false, src, outputDir, writeAssembly = false) shouldNotBe null
    }

    test("common subtypes") {
        BinaryExpression.commonDatatype(DataType.WORD, DataType.UWORD, null, null).first shouldBe DataType.WORD

        BinaryExpression.commonDatatype(DataType.pointer(BaseDataType.BOOL), DataType.UBYTE, null, null).first shouldBe DataType.pointer(BaseDataType.BOOL)
        BinaryExpression.commonDatatype(DataType.pointer(BaseDataType.BOOL), DataType.UWORD, null, null).first shouldBe DataType.pointer(BaseDataType.BOOL)
        BinaryExpression.commonDatatype(DataType.pointer(BaseDataType.BOOL), DataType.BYTE, null, null).first shouldBe DataType.pointer(BaseDataType.BOOL)
        BinaryExpression.commonDatatype(DataType.pointer(BaseDataType.BOOL), DataType.WORD, null, null).first shouldBe DataType.pointer(BaseDataType.BOOL)

        BinaryExpression.commonDatatype(DataType.UBYTE, DataType.pointer(BaseDataType.BOOL), null, null).first shouldBe DataType.pointer(BaseDataType.BOOL)
        BinaryExpression.commonDatatype(DataType.UWORD, DataType.pointer(BaseDataType.BOOL), null, null).first shouldBe DataType.pointer(BaseDataType.BOOL)
        BinaryExpression.commonDatatype(DataType.BYTE, DataType.pointer(BaseDataType.BOOL), null, null).first shouldBe DataType.pointer(BaseDataType.BOOL)
        BinaryExpression.commonDatatype(DataType.WORD, DataType.pointer(BaseDataType.BOOL), null, null).first shouldBe DataType.pointer(BaseDataType.BOOL)
    }

    test("bitwise operator on signed values") {
        val src = """
main {
    sub start() {
        word[5] xpos

        xpos[4] &= ${'$'}fff8            
        xpos[4] &= ${'$'}fff8 as word    
        xpos[4] = xpos[4] & ${'$'}fff8   
        xpos[4] = xpos[4] & ${'$'}fff8 as word   

        xpos[4] &= $7000            
        xpos[4] &= $7000 as word    
        xpos[4] = xpos[4] & $7000   
        xpos[4] = xpos[4] & $7000 as word   

        xpos[4] |= $7000            
        xpos[4] |= $7000 as word    
        xpos[4] = xpos[4] | $7000   
        xpos[4] = xpos[4] | $7000 as word   

        ; the error doesn't occur with other operators:
        xpos[4] += $7000
        xpos[4] += $7000 as word
        xpos[4] = xpos[4] + $7000
        xpos[4] = xpos[4] + $7000 as word   
    }
}"""

        compileText(VMTarget(), false, src, outputDir, writeAssembly = false) shouldNotBe null
    }

    test("assign typecasted value to struct field") {
        val src = """
main {
    struct element {
        word  y
    }

    sub start() {
        ^^element zp_element = 20000
        zp_element.y = cx16.r0L as byte
    }
}"""
        compileText(VMTarget(), false, src, outputDir, writeAssembly = true) shouldNotBe null
        compileText(C64Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
    }
})
