Logical Or Assignment Bug in ASC2

So, here’s something to keep an eye on if switching to AIR 3.8 (or 3.7 with the ASC2 compiler).

Last week at work we decided to finally make the jump from AIR 3.5 to AIR 3.8; something I was pretty excited about and had been looking forward to for awhile. After applying the update, though, I noticed some unit tests failing and sat down to investigate. Several places in our app, we map loaded xml files to data objects – a pretty common thing to do. In the parse methods we may use typical logical or assignment to fill properties with default values if they aren’t present in the xml. So it’s not uncommon to run across a line like this:

this.name = xml.@name || “Default Name”;

Now, in the ASC1 compiler this always worked fine. After switching to the ASC2 compiler in AIR 3.8, though, this is where our unit tests began falling down. In the case above, rather than assigning the “Default Name”, the name property was being assigned an empty string. So I decided to have a little look under the hood and see exactly what was going on. So here’s a real quick test class file I compiled to examine closer:

ActionScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

package

{

importflash.display.Sprite;

importflash.events.Event;

publicclassMainextendsSprite

{

publicfunctionMain():void

{

if(stage)init();

elseaddEventListener(Event.ADDED_TO_STAGE,init);

}

privatefunctioninit(event:Event=null):void

{

removeEventListener(Event.ADDED_TO_STAGE,init);

testXML();

}

privatefunctiontestXML():void

{

varxml:XML=<test/>

varname:String=xml.@name||"Default";

trace("Name=("+name+")");

// output:

// ASC1: Name=(Default)

// ASC2: Name=()

}

}

}

As you can see in the comments, compiling with ASC1, I got the results I was looking for and used to. In ASC2 though, I got an empty string. Using Adobe’s SWF Investigator, I decompiled the two swf files to the Actionscript Bytecode (ABC). With the ASC1, here is what’s going on at the point you try to get the xml’s name attribute:

Assembly (x86)

1

2

3

4

5

6

7

8

9

10

11

12

13

32getpropertyname//nameIndex=10

34coerce_s

35dup

36convert_b

37iftrueL1

41pop

42pushstring"Default"//stringIndex=27

44coerce_s

L1:

45coerce_s

46setlocal2

Now, I’m no expert in ABC, but it’s easy enough to follow along with what’s happening. We get the property name from the xml and type it to a String (‘coerce_s’). Since there is no property ‘name’, that will be coerced to an empty String. We then convert that to a Boolean (‘convert_b’). Of course an empty string converted to a boolean is false so the ‘iftrue’ fails, we push the string “Default” on to the stack, type it as a string (‘coerce_s’), then set the local variable ‘name’ to that value.

Compiled in ASC2 though, we see a different story:

Assembly (x86)

1

2

3

4

5

6

7

8

9

10

11

28getpropertyname//nameIndex=9

30dup

31iftrueL1

35pop

36pushstring"Default"//stringIndex=31

38coerce_a

L1:

39coerce_s

40setlocal1

This time, we get the property name but do no coercion or conversion – we just wind up with an an untyped ‘something’. But an untyped ‘something’ is not a ‘nothing’, so the iftrue passes, we jump to the L1 block where we then coerce that ‘something’ into an empty string and set set the local variable ‘name’ to that.

So, we wind up with two very different results which may not be what you actually want. In this case, though, we only get an empty string where we may expect some default text. Worst case scenario, our app displays some blank text. Annoying, yeah, but not the end of the world.

But wait, it gets worse.

If you use logical or assignment with interfaces, things get really crazy. Here’s another test class file to test:

ActionScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

package

{

importflash.display.Sprite;

importflash.events.Event;

publicclassMainextendsSprite

{

publicfunctionMain():void

{

if(stage)init();

elseaddEventListener(Event.ADDED_TO_STAGE,init);

}

privatefunctioninit(event:Event=null):void

{

removeEventListener(Event.ADDED_TO_STAGE,init);

varf:Test=newTest("blah");

testClass(f);

}

privatefunctiontestClass(someArg:ITest=null):void

{

someArg||=newTest("yadda");

trace("someArg=("+someArg+")");

// output:

// ASC1: someArg=(Test [foobar=blah])

// ASC2: someArg=(-2.1750519003895844e-311)

varname:String=someArg.foobar;

trace("Name=("+name+")");

// output:

// ASC1: Name=(blah)

// ASC2: Null Object Error is thrown at line above

}

}

}

interfaceITest

{

functiongetfoobar():String;

functiontoString():String;

}

classTestimplementsITest

{

privatevar_foobar:String;

publicfunctionTest(foobar:String)

{

_foobar=foobar;

}

publicfunctiongetfoobar():String{return_foobar;}

publicfunctiontoString():String

{

return"Test [foobar="+this._foobar+"]";

}

}

Let’s look at the byte code for ASC1 again.

Assembly (x86)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

18getlocal1

19coerceprivate::ITest//nameIndex=10

21dup

22convert_b

23iftrueL1

27pop

28findpropstrictprivate::Test//nameIndex=8

30pushstring"yadda"//stringIndex=23

32constructpropprivate::Test(1)//nameIndex=8

35coerceprivate::ITest//nameIndex=10

L1:

37coerceprivate::ITest//nameIndex=10

39setlocal1

40debugline28

42findpropstricttrace//nameIndex=11

44pushstring"someArg=("//stringIndex=25

46getlocal1

47add

48pushstring")"//stringIndex=26

We get our local ‘someArg’ variable, type it as an ITest object (‘coerce’), and convert that to a Boolean. Of course that boolean is true, so the ‘iftrue’ passes and we jump to the L1 block, type the variable to an ITest once again, set our local variable again, then do the trace.

But again, with the ASC2 compiler we see something very different:

Assembly (x86)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

14getlocal1

15iftrueL1

19findpropstrictprivate::Test//nameIndex=12

21debugline26

23pushstring"yadda"//stringIndex=36

25constructpropprivate::Test(1)//nameIndex=12

28setlocal1

L1:

29getlextrace//nameIndex=17

31getglobalscope

32debugline28

34pushstring"someArg=("//stringIndex=38

36getlocal1

37add

38pushstring")"//stringIndex=39

Again, with ASC2 we get our local variable but do no coercion or conversion, so again we wind up with an ‘untyped something’. But, again, an ‘untyped something’ is not nothing, so the ‘iftrue’ passes and we jump to the L1 block. This time though, we still don’t coerce the ‘untyped something’ into an object and instead jump right into the trace statement and we see that our ‘untyped something’ is actually represented as an infinitesimally small number in memory (see the output comments in the actionscript above). But as you can see in the comments above, when we try to access properties of this ‘untyped something’ we wind up with a null object error.

As you can imagine, compared to the minor annoyance of an empty string, a null object error is a real ball deal breaker.

I’ve filed a bug report on this behavior with Adobe which you can check out here. In the meantime, though, if you’re making the jump into AIR 3.8 (or AIR 3.7 with the ASC2 compiler), I highly recommend avoiding using OR assignment like the plague.

6 Comments:

Thanks for this information. I voted for your bug.

Since Adobe does not accept the usual premise that a defective product needs to be fixed, rather they sit back and wait to see how many people report the bug, and only expend resources after some cockamamie voting process, I would appreciate your looking at my bug, and voting for it, as well, otherwise, I am sure I will never see a fix.

Thank you for the vote. I went over to check out your bug report, but I couldn’t figure out the error. I’m obviously not understanding something. Are you saying that class A cannot be set as the document class? If I try to compile a class with a package that doesn’t actually mirror the file structure (i.e. if I have a class with package any that isn’t inside a directory named ‘any’), I get the error you describe in ASC2, but I get a similar error in ASC1: “Error: A file found in a source-path must have the same package structure ”, as the definition’s package, ‘any’.” I have a feeling I’m just not understanding the problem correctly. Maybe you could re-explain it.