Creating A Better Assert Function
The assert
function is found in many languages to provide a way for you to check for errors only in debug builds of your code. For release/production builds, the asserts are removed to make the compiled code smaller and remove all of the overhead of the error checking. Flash doesn’t come with such a feature built-in, but can we build one ourselves? Today’s article will try to do just that using nothing but Adobe’s modern AS3 compiler: ASC 2.0.
Let’s start out with a “naïve” assert:
public static function assert(truth:*): void { if (!truth) { throw new Error("Assertion failed!"); } }
Using this function is easy, just pass an expression that should evaluate to true
:
// Make sure an index int is in a valid range for a Vector assert(index >= 0 && index < myVector.length); // Make sure a name String isn't null or empty assert(name);
If either expression is false
, an Error
is thrown to alert you to the problem. You might want to make your own custom AssertionError
derivative class of Error
, but that’s beside the point of this article. For our current purposes, the biggest problem with the above function is that is always runs the error check and always throws the Error
if it fails. We only want it to run in debug builds, not release builds, so this doesn’t quite suit our needs.
The next step in creating the “naïve” assert is to put in an if
statement:
// Only set the flag when running in a debug version of Flash Player or AIR. // You might want to set this manually or by some other criteria. private static const ASSERT_ENABLED:Boolean = Capabilities.isDebugger; public static function assert(truth:*): void { if (ASSERT_ENABLED && !truth) { throw new Error("Assertion failed!"); } }
By checking the ASSERT_ENABLED
flag, we’ve created a function that effectively throws Error
objects in debug builds and doesn’t throw them in release/production builds. Due to short-circuiting, it doesn’t even evaluate the truth
parameter. However, this code does have several major drawbacks.
First, the ASSERT_ENABLED
flag is checked every single time assert
is called in release builds, which may be slow if you call it a lot. Second, the assert
function itself is called in release builds, which is definitely slow as AS3 function are notoriously expensive. Third, the assert
function is still built into release SWFs and therefore increases the file size. Lastly, the expression to pass to assert
is still evaluated, which may be slow if it is complex.
How can we address all of these issues? Well, one big step is to use a compile-time constant to knock out the body of the assert
function from even getting compiled into the SWF, let alone executed. This should take care of the first drawback and help reduce, but not eliminate, the SWF size. Here’s a first attempt at that:
public static function assert(truth:*): void { ASSERT::enabled { if (!truth) { throw new Error("Assertion failed!"); } } }
Plus a compiler parameter:
# Debug builds -define=ASSERT::enabled,true # Release/production builds -define=ASSERT::enabled,false
In a debug build, the function will look like this:
public static function assert(truth:*): void { if (!truth) { throw new Error("Assertion failed!"); } }
So we’ve effectively removed the ASSERT_ENABLED
check in debug builds, which is a nice speedup. Here’s how the function looks in release builds:
public static function assert(truth:*): void { }
This is very good progress and a big improvement over the previous version. But can we do anything about the actual function call to assert
? That’s where ASC 2.0’s [Inline]
metadata comes in:
[Inline] public static function assert(truth:*): void { ASSERT::enabled { if (!truth) { throw new Error("Assertion failed!"); } } }
Now when we call the function in a debug build the bytecode looks like this: (annotations are mine)
// push "hello" to the stack 4 pushstring "hello" // set "hello" from the stack to local variable #1 5 setlocal1 // get "hello" from local variable #1 6 getlocal1 // if "hello" is true, go to bb2 block 7 iftrue bb2 bb1 succs=[] // "hello" was not true, the assertion failed, find the Error class 8 finddef Error // push "Assertion failed!" to the stack 9 pushstring "Assertion failed!" // construct the Error with "Assertion failed!" as its argument 10 constructprop // throw the Error 11 throw bb2 succs=[] // "hello" was true, the assertion did not fail 12 returnvoid
All of this is even better for the debug build because there is no longer any overhead for the function call to assert
. But let’s take a look at the release version:
// find the assert function 4 findpropstrict assert // push "hello" 5 pushstring "hello" // call the assert function "hello" as the argument 6 callpropvoid // done with the function 7 returnvoid
Unfortunately, the compiler (version 2.0.0 build 354071 with -inline
and -optimize
) does not inline the function call for some reason. So we need to trick it by adding some pointless code:
[Inline] public static function assert(truth:*): void { ASSERT::enabled { if (!truth) { throw new Error("Assertion failed!"); } } return; // <-- added }
Now how does the release version look?
// push "hello" 4 pushstring "hello" // set "hello" to local variable #1 5 setlocal1 // don't call the function or do any assert work, just move on 6 returnvoid
With this trick the release build no longer calls assert
or does any of the work that assert
does, like test for the error case or throw an Error
object. The assert
function is still built into the SWF, but its size is now extremely small. Here’s the bytecode, which shows just an empty function:
public static function assert(*):void { // derivedName assert // method_info 1 // max_stack 1 // max_regs 2 // scope_depth 0 // max_scope 1 // code_length 1 bb0 succs=[] 0 returnvoid }
The last hurdle to overcome is that the expression passed to assert
is still evaluated. One potential way of fixing this is to use the -omit-trace-statements
compiler parameter and wrap all our assert
calls in trace
calls like this:
trace(assert(index >= 0 && index < myVector.length));
Theoretically, the whole line should be removed by the compiler. Unfortunately, here’s the bytecode:
15 pushbyte 16 setlocal1 17 getlex trace 18 getglobalscope 19 getlocal1 20 pushbyte 21 greaterequals 22 dup 23 iffalse bb2 bb1 succs=[bb2] 24 pop 25 getlocal1 26 getlocal2 27 getproperty length 28 lessthan bb2 succs=[] 29 setlocal3 30 pushundefined 31 call 1 32 returnvoid
I haven’t annotated it, but you can clearly see that all of the expression is still being evaluated and now even have an additional, expensive call to trace
. Unfortunately again, it seems that the only way to fully eliminate the expression is to wrap the calling code as well in an ASSERT::enabled
block:
ASSERT::enabled { assert(index >= 0 && index < myVector.length); }
The bytecode is now, finally, empty.
However, it can easily be argued that there’s a lot of typing that needs to happen now for each and every assert
call. This is true, and probably both annoying to develop with and to code. There don’t seem to be any truly effective alternatives. However, you can mitigate the extra typing in a couple of ways. First, if you’re calling assert
multiple times, you only need one ASSERT::enabled
block:
ASSERT::enabled { assert(index >= 0 && index < myVector.length); assert(name); assert(!error); }
You can also set either the first or second parts of the compile-time constant to shorter strings to lessen the typing load. Of course you’ll want to still preserve readability, which probably means avoiding really short names like A::E
. However, some compromise is probably acceptable. For example, ASSERT::on
is a bit shorter, DEBUG::on
is one character shorter than that, and DBG::on
is probably as far as you can go.
In conclusion, we’ve created an assert
and come up with a strategy for calling it that is almost completely removed in release/production builds of our code. The error expression isn’t evaluated, the function isn’t called, the truth of the expression isn’t checked, and no Error
is thrown. The only tiny remnant is an empty function in the SWF, which should be acceptable to all but the tiniest Flash apps such as banner ads.
Spot a bug? Have a way of improving assert
? Post a comment!
#1 by Deril on November 18th, 2013 ·
My take on assert class:
https://github.com/MindScriptAct/mvcExpress-framework/blob/version2/src/mvcexpress/utils/AssertExpress.as
(I use CONFIG::debug for debug code in my apps.)
Thanks for article.
#2 by jackson on November 18th, 2013 ·
That’s a nice set of assert functions. Perhaps if you were to augment them with some of the debug build-only strategies from this article they could be even better. :)
#3 by Deril on November 19th, 2013 ·
Thanks! I tried to simplify it, I really hate standard asUnit assert functions style.
I will add [Inline] and return in next mvcExpress release.
Regarding CONFIG::debug compile argument: I am thinking on adding it on whole class! instead of adding it inside of functions. This will force you to exclude all calls to this class with compile argument too – keeping release clean.