Orvid / Caprica

A compiler for the Papyrus scripting language used by the Creation Engine.
MIT License
84 stars 15 forks source link

Comparison of bools in structs fails under certain circumstances #12

Open EyeDeck opened 6 years ago

EyeDeck commented 6 years ago

So, I ran into a confusing thing earlier, where one line of code in one of my scripts wasn't working as expected. I investigated, and here's some test code that demonstrates the bug:

scriptname boolTestB

struct testStruct
    bool b
    int i
    float f
    ObjectReference O
endStruct

Function testBoolStruct() Global
    testStruct structA = new testStruct
    testStruct structB = new testStruct

    structA.b = false
    structA.i = 0
    structA.f = 0
    structA.O = Game.GetForm(0x14) as ObjectReference ; PlayerRef

    structB.b = true
    structB.i = 1
    structB.f = 1
    structB.O = Game.GetForm(0x22B94) as ObjectReference ; some arbitrary rock somewhere

    bool bFalse = false
    bool bTrue = true

    int i0 = 0
    int i1 = 1

    float f0 = 0
    float f1 = 1

    ObjectReference PlayerRef = Game.GetForm(0x14) as ObjectReference
    ObjectReference Rock = Game.GetForm(0x22B94) as ObjectReference

    Debug.Trace("##############################")
    ; these work properly
    Debug.Trace("structA: " + structA.b)
    Debug.Trace("structB: " + structB.b)
    Debug.Trace("structA.b || structB.b: " + (structA.b || structB.b))
    Debug.Trace("structB.b || structA.b: " + (structB.b || structA.b))
    Debug.Trace("false || (bTrue != bFalse): " + (false || (bTrue != bFalse)))
    Debug.Trace("false || (i0 != i1): " + (false || (i0 != i1)))
    Debug.Trace("false || (f0 != f1): " + (false || (f0 != f1)))
    Debug.Trace("false || (PlayerRef != Rock): " + (false || (PlayerRef != Rock)))
    Debug.Trace("false || (structA.i != structB.i): " + (false || (structA.i != structB.i)))
    Debug.Trace("false || (structA.f != structB.f): " + (false || (structA.f != structB.f)))
    Debug.Trace("false || (structA.O != structB.O): " + (false || (structA.O != structB.O)))

    ; this one doesn't, with Caprica
    Debug.Trace("false || (structA.b != structB.b): " + (false || (structA.b != structB.b)))

    Debug.Trace("##############################")
EndFunction

I compiled two separate scripts, identical except that one was compiled with the Bethesda compiler, and the other with Caprica (boolTestB and boolTestC respectively), and here's the log output, again in the same order:

[04/17/2018 - 07:46:57PM] ############################## [04/17/2018 - 07:46:57PM] structA: False [04/17/2018 - 07:46:57PM] structB: True [04/17/2018 - 07:46:57PM] structA.b || structB.b: True [04/17/2018 - 07:46:57PM] structB.b || structA.b: True [04/17/2018 - 07:46:57PM] false || (bTrue != bFalse): True [04/17/2018 - 07:46:57PM] false || (i0 != i1): True [04/17/2018 - 07:46:57PM] false || (f0 != f1): True [04/17/2018 - 07:46:57PM] false || (PlayerRef != Rock): True [04/17/2018 - 07:46:57PM] false || (structA.i != structB.i): True [04/17/2018 - 07:46:57PM] false || (structA.f != structB.f): True [04/17/2018 - 07:46:57PM] false || (structA.O != structB.O): True [04/17/2018 - 07:46:57PM] false || (structA.b != structB.b): True [04/17/2018 - 07:46:57PM] ##############################

[04/17/2018 - 07:47:00PM] ############################## [04/17/2018 - 07:47:00PM] structA: False [04/17/2018 - 07:47:00PM] structB: True [04/17/2018 - 07:47:00PM] structA.b || structB.b: True [04/17/2018 - 07:47:00PM] structB.b || structA.b: True [04/17/2018 - 07:47:00PM] false || (bTrue != bFalse): True [04/17/2018 - 07:47:00PM] false || (i0 != i1): True [04/17/2018 - 07:47:00PM] false || (f0 != f1): True [04/17/2018 - 07:47:00PM] false || (PlayerRef != Rock): True [04/17/2018 - 07:47:00PM] false || (structA.i != structB.i): True [04/17/2018 - 07:47:00PM] false || (structA.f != structB.f): True [04/17/2018 - 07:47:00PM] false || (structA.O != structB.O): True [04/17/2018 - 07:47:00PM] false || (structA.b != structB.b): False [04/17/2018 - 07:47:00PM] ##############################

Also, here's the assembly after running each through Bethesda's disassembler:

                        JumpT ::temp15 _label9                                   ;@line 51
                        StructGet ::temp13 structA b                             ;@line 51
                        StructGet ::temp2 structB b                              ;@line 51
                        CompareEQ ::temp12 ::temp13 ::temp2                      ;@line 51
                        Not ::temp12 ::temp12                                    ;@line 51
                        Cast ::temp15 ::temp12                                   ;@line 51
                    _label9:
                        Cast ::temp10 ::temp15                                   ;@line 51
                        StrCat ::temp11 "false || (structA.b != structB.b): " ::temp10  ;@line 51
                        CallStatic debug Trace ::nonevar ::temp11 0              ;@line 51
                        JumpT ::temp3 _label9                                    ;@line 51
                        StructGet ::temp3 structA b                              ;@line 51
                        StructGet ::temp3 structB b                              ;@line 51
                        CompareEQ ::temp3 ::temp3 ::temp3                        ;@line 51
                        Not ::temp3 ::temp3                                      ;@line 51
                        Assign ::temp3 ::temp3                                   ;@line 51
                    _label9:
                        Cast ::temp4 ::temp3                                     ;@line 51
                        StrCat ::temp4 "false || (structA.b != structB.b): " ::temp4  ;@line 51
                        CallStatic debug Trace ::nonevar ::temp4 0               ;@line 51

It appears that Caprica gets really confused and starts assigning everything to ::temp3 for some reason. This also makes Champollion fail miserably on the Caprica script, it just gives up when it gets to that section. This was compiled with the now 2-year-old v0.2.0 release binary from the Nexus, without the -O flag since I know it's borked in that version.

Also, here's a link to an archive with the sources, compiled scripts, Caprica .pas output (for boolTestC), and disassembled .pas outputs for both, in case it helps any.

This is easy enough to work around, but really confusing if you run into it like I did. Any ideas?

cadpnq commented 6 years ago

Some extra weirdness. Commenting out some of the previous calls to Debug.trace results in what looks like proper output (I have not ran it)

Function testBoolStruct() Global
    testStruct structA = new testStruct
    testStruct structB = new testStruct

    structA.b = false
    structA.i = 0
    structA.f = 0
    structA.O = Game.GetForm(0x14) as ObjectReference ; PlayerRef

    structB.b = true
    structB.i = 1
    structB.f = 1
    structB.O = Game.GetForm(0x22B94) as ObjectReference ; some arbitrary rock somewhere

    bool bFalse = false
    bool bTrue = true

    int i0 = 0
    int i1 = 1

    float f0 = 0
    float f1 = 1

    ObjectReference PlayerRef = Game.GetForm(0x14) as ObjectReference
    ObjectReference Rock = Game.GetForm(0x22B94) as ObjectReference

    Debug.Trace("##############################")
    ; these work properly
    Debug.Trace("structA: " + structA.b)
    Debug.Trace("structB: " + structB.b)
;   Debug.Trace("structA.b || structB.b: " + (structA.b || structB.b))
;   Debug.Trace("structB.b || structA.b: " + (structB.b || structA.b))
;   Debug.Trace("false || (bTrue != bFalse): " + (false || (bTrue != bFalse)))
;   Debug.Trace("false || (i0 != i1): " + (false || (i0 != i1)))
;   Debug.Trace("false || (f0 != f1): " + (false || (f0 != f1)))
;   Debug.Trace("false || (PlayerRef != Rock): " + (false || (PlayerRef != Rock)))
;   Debug.Trace("false || (structA.i != structB.i): " + (false || (structA.i != structB.i)))
;   Debug.Trace("false || (structA.f != structB.f): " + (false || (structA.f != structB.f)))
;   Debug.Trace("false || (structA.O != structB.O): " + (false || (structA.O != structB.O)))

    ; this one doesn't, with Caprica
    Debug.Trace("false || (structA.b != structB.b): " + (false || (structA.b != structB.b)))

    Debug.Trace("##############################")
EndFunction

The assembly:

            ASSIGN ::temp2 False 
            JUMPT ::temp2 label0 
            STRUCTGET ::temp2 structA b 
            STRUCTGET ::temp4 structB b 
            COMPAREEQ ::temp4 ::temp2 ::temp4 
            NOT ::temp4 ::temp4 
            ASSIGN ::temp2 ::temp4 
            label0:
            CAST ::temp3 ::temp2 
            STRCAT ::temp3 "false || (structA.b != structB.b): " ::temp3 
            CALLSTATIC debug Trace ::nonevar ::temp3 0

The two STRUCTGET instructions store to different temporaries.

EyeDeck commented 6 years ago

Hmm, it appears to be related to reuse of earlier temp variables created for the same function.

scriptname boolTestC

struct testStruct
    bool b
endStruct

Function TestBoolStruct()
    testStruct structA = new testStruct
    testStruct structB = new testStruct

    structA.b = false
    structB.b = true

    Debug.Trace("##############################")
    Debug.Trace(structA.b != structB.b)
    Debug.Trace(structA.b != structB.b)
    Debug.Trace("##############################")
EndFunction

compiles down to

.function TestBoolStruct
    .userFlags 0    ; Flags: 0x00000000
    .docString ""
    .return None
    .paramTable
    .endParamTable
    .localTable
        .local structA booltestc#teststruct
        .local ::temp0 booltestc#teststruct
        .local structB booltestc#teststruct
        .local ::nonevar None
        .local ::temp1 Bool
        .local ::temp2 Bool
        .local ::temp3 String
    .endLocalTable
    .code
        StructCreate ::temp0                                     ;@line 8
        Assign structA ::temp0                                   ;@line 8
        StructCreate ::temp0                                     ;@line 9
        Assign structB ::temp0                                   ;@line 9
        StructSet structA b False                                ;@line 11
        StructSet structB b True                                 ;@line 12
        CallStatic debug Trace ::nonevar "##############################" 0  ;@line 14
        StructGet ::temp1 structA b                              ;@line 15
        StructGet ::temp2 structB b                              ;@line 15
        CompareEQ ::temp2 ::temp1 ::temp2                        ;@line 15
        Not ::temp2 ::temp2                                      ;@line 15
        Cast ::temp3 ::temp2                                     ;@line 15
        CallStatic debug Trace ::nonevar ::temp3 0               ;@line 15
        StructGet ::temp2 structA b                              ;@line 16
        StructGet ::temp2 structB b                              ;@line 16
        CompareEQ ::temp2 ::temp2 ::temp2                        ;@line 16
        Not ::temp2 ::temp2                                      ;@line 16
        Cast ::temp3 ::temp2                                     ;@line 16
        CallStatic debug Trace ::nonevar ::temp3 0               ;@line 16
        CallStatic debug Trace ::nonevar "##############################" 0  ;@line 17
    .endCode
.endFunction

I haven't tried running this variation either, but presumably the first line would correctly return true, and the second one, incorrectly, false.