May 17, 2004

Design-By-Contract, Object Constructors, and Error Stacks in JavaScript

A couple years ago, I read an article in Dr. Dobb's Journal on a next-generation programming language called D. It was probably the only article in DDJ that I fully understood...

One of the concepts in the D language is “design-by-contract”. It was the original inspiration for my assert() function, which I rewrote to take advantage of Error.prototype.stack (more on this in a minute). Any function you write may have a set of “preconditions” which must be satisfied before the main function executes, and/or “postconditions” which must be satisfied after the main function executes.

The goal of DBC is to add sanity checks for a particular function. Quality Assurance personnel and developers can add assertions that demand the code act correctly (or at least, consistently). If a precondition, postcondition, or standard assert() fails, there is a bug: either in the actual function, or in the failed contract. (But of course, the code doesn't know which!)

With a debug flag enabled, the code executing the function runs the precondition and postcondition functions. Without the debug flag, it skips them.

The example from DDJ looked something like:


function getSquareRoot(x, rv) {
in {
assert(x >= 0);
}
out {
assert(rv * rv == x);
}
body {
rv = Math->sqrt(x);
return 0;
}
}

JavaScript doesn't natively support this capability. Mozilla C++ code simulates it with macros (NS_PRECONDITION, NS_ASSERTION?). So what I needed to work this correctly was (1) an assert function that actually could run in the scope of the function it was operating in, and (2) a standard set of methods for each function which could optionally run before and after the main function, but with the same arguments.

Tackling the first was relatively easy; I'd done something like it with a earlier assert() function (also blogged here). The catch was that I never got the scope to operate in the main function. It had to pass the expression to check as a string, it had to pass a contextual “this” object, etc. I cut that out, and simply made the first argument a Boolean expression, like the condition in an if() {} statement.

Of course, there is also the added challenge of making sure the JavaScript Console gets the right filename and line number information. Constructed errors always have the Error() constructor's filename and line number. (There's a bug to have these properties set with the original throw statement, but in my case they match.) So if I was going to throw any constructed Error(), the properties of that Error() would need resetting.

Enter the stack. Before the Mozilla 1.0 release, I came up with the idea that JavaScript errors should have stacks for debugging. I floated the idea past Boris Zbarsky and a couple others, and with their general support I filed bug 123177 for it. Brendan Eich leaped on it (I very rarely see so much enthusiasm to add a feature), and implemented Error.prototype.stack to give a basic breakdown of the functions by arguments, files and lines which were running when the error was thrown.

Assume I have the following:


function foo() {
assert(1 == 0);
}

function assert(mustBeTrue) {
if (mustBeTrue) return;
try {
throw new Error(“bar error”);
}
catch(e) {
dump(e.stack + “\n”);
}
}
foo();


The dump output looks like:

Error(“bar error”)@:0
assert(false)@chrome://foo/content/foo.xml:13
foo()@chrome://foo/content/foo.xml:7
@chrome://foo/content/foo.xml:19

The first two lines in this stack are (in this case only) useless to me as a debugger. But the third line is very useful. It tells the real file and line number where the assert() function was called.

So, to reset the Error() object's properties appropriately, I wrote this little function:


Error.prototype.shiftStack = function(aShiftLines) {
var aStackArray = this.stack.split("\n").splice(aShiftLines);
var lastColon = aStackArray[0].lastIndexOf(":");
this.fileName = aStackArray[0].substring(aStackArray[0].indexOf("@") + 1, lastColon);
this.lineNumber = aStackArray[0].substr(lastColon + 1);
this.stack = aStackArray.join("\n");
}

The shiftStack method modifies the stack, filename, and line number based on the number of lines the caller wishes to “lose”. Based on the original stack, it is most helpful to lose the top two lines of the stack. So my assert() function now throws an exception e, catches it, and calls e.shiftStack(2) to clean up the garbage. It then sets the e.name to “AssertionError” (let's be clear this isn't your typical error). If a third argument is set to true, it actually rethrows the exception (after resetting the message). If the third argument is false or omitted, it tries to send a warning to the console via my chrome warn() function.

Thus, when I write:


assert(1 == 0, “1 does not equal 0.”, true)

I can get a useful JavaScript Console message that says:

AssertionError: 1 does not equal 0.
Filename: chrome://foo/content/foo.xml Line: 7

So with a valid assert() function, I can turn to implementing a design-by-contract arrangement.

A good DBC code, in JavaScript, might look like this:


function getSquareRoot(x) {
return Math.sqrt(x);
}
getSquareRoot.precondition = function(x) {
assert(x >= 0, “x must be a real number greater than zero!”, true);
}
getSquareRoot.postcondition = function(x, rv) {
assert(rv * rv == x, “The returned value times itself does not equal the original argument!”, true);
}

So how do you execute all three functions in sequence? The postcondition argument has an extra rv argument to indicate the returned value to check.

I created an applyContract() function:


function applyContract(aFunction, thisObj) {
var args = [];
for (var x = 2; x < arguments.length; x++) {
args[x - 2] = arguments[x]; // we're just setting up arguments to pass.
}
if (typeof aFunction.precondition == 'function') {
aFunction.precondition.apply(thisObj, args);
}
var rv = aFunction.apply(thisObj, args);
if (typeof aFunction.postcondition == 'function') {
args.push(rv);
aFunction.postcondition.apply(thisObj, args);
}
return rv;
}

It's pretty simple. You call:

var root_3 = applyContract(getSquareRoot, this, 3); // == Math.sqrt(3) in the end...

The second argument, “this”, is required as the apply() method of functions also requires it.

What if you want to construct an object, and run that through preconditions and postconditions?

First, you need a function that can construct the object when called by the apply() method of functions. (Figuring out how to do so is obvious only in hindsight.)


function constructNew(aFunction) {
var args = [];
var argString = "";
for (var x = 1; x < arguments.length; x++) {
args[x - 1] = arguments[x];
argString += ",args[" + (x - 1) + "]";
}
argString = argString.substr(1);
alert("var rv = new aFunction(" + argString + ")");
eval("var rv = new aFunction(" + argString + ")");
return rv;
}

Thus,
var x = constructNew(Array, 2, 4, 6);

is the same as saying

x = new Array(2, 4, 6);

The major difference is I can call constructNew.apply(thisObj, [Array, 2, 4, 6]). The thisObj variable gets ignored!

So, to apply the contract while constructing an object:


function applyContractConstructor(aFunction) {
var args = [];
for (var x = 1; x < arguments.length; x++) {
args[x - 1] = arguments[x];
}
if (typeof aFunction.precondition == 'function') {
aFunction.precondition.apply(this, args);
}
args.unshift(aFunction);
var rv = constructNew.apply(thisObj, args);
args.shift(aFunction);
if (typeof aFunction.postcondition == 'function') {
args.push(rv);
aFunction.postcondition.apply(this, args);
}
return rv;
}

Finally, because I want design-by-contract and assert() to only work when I'm debugging script, I added a ecmaDebug Boolean value check. The result is a new ecmaDebug.js file.

I'll say this, though: DBC and assert() are coming in very handy in developing Abacus! As I said earlier, if a contract fails, there's a bug in either the executing code or the contract, and it behooves me to fix it!

Posted by WeirdAl at May 17, 2004 4:20 PM
Comments

constructNew is a bit wordy, what about create or build or something else that matches up with new?

Posted by: Eric Hodel at May 18, 2004 8:23 AM

The name itself matters a little, but not too much... if you want build, that sounds all right to me.

Posted by: Alex Vincent at May 18, 2004 3:44 PM