Caprica icon indicating copy to clipboard operation
Caprica copied to clipboard

Comparison of bools in structs fails under certain circumstances

Open EyeDeck opened this issue 6 years ago • 2 comments

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?

EyeDeck avatar Apr 18 '18 00:04 EyeDeck

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.

cadpnq avatar Apr 18 '18 18:04 cadpnq

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.

EyeDeck avatar Apr 18 '18 21:04 EyeDeck