Page 1 of 2

JavaScript semantic change

Posted: Sun Dec 12, 2010 5:42 pm
by JensAyton
Boo.

For a while now – call it a year and a half – I’ve been putting off upgrading Oolite’s JavaScript engine to the current version. The amount of work has of course grown constantly during that time, and there are now some compatibility issues.

In particular, it is no longer possible for Oolite to customize the behaviour of the ==/!= operators to usefully compare different objects with the same values. Doing this in the first place was an abuse of the engine which didn’t comply with JavaScript language semantics. This primarily affects vectors and quaternions.

I’m loathe to add equal() methods to vectors and quaternions, though, because they’re less meaningful than you’d expect. Because of the limited precision of floating-point numbers, calculations that are algebraically equivalent can produce slightly different results (or in odd cases, very different results). The correct way to compare vectors is to check whether the distance between them is below some threshold value:

Code: Select all

var equal = v.squaredDistanceTo(u) < thresholdSquared;
For quaternions, the dot product of two normalized quaternions will be near 1 if they are nearly the same.

Code: Select all

// Skip normalize() if they are known to be normalized already –
// entity orientations are always normalized.
var equal = q.normalize().dot(p.normalize()) > 0.9999;
As the functionality is no longer present in SpiderMonkey, it’s not possible to provide a transition period. ==/!= will not work for distinct Oolite objects in the next release, and will stop working in nightlies soon (probably tonight) even before the upgrade is complete.

Posted: Sun Dec 12, 2010 7:16 pm
by DaddyHoggy
Welcome back Ahruman - for those who script (not me) I wish it was with better news!

(Although compatibility, in the long run, is always a good thing)

Posted: Sun Dec 12, 2010 8:03 pm
by Svengali
Heija - welcome back, Ahruman.
Hope you've had a good time without hazzles.
Ahruman wrote:
As the functionality is no longer present in SpiderMonkey, it’s not possible to provide a transition period. ==/!= will not work for distinct Oolite objects in the next release, and will stop working in nightlies soon (probably tonight) even before the upgrade is complete.
Does it mean we can already test our OXPs in trunk? Or should we wait some days...?

Posted: Sun Dec 12, 2010 8:05 pm
by JensAyton
Svengali wrote:
Does it mean we can already test our OXPs in trunk? Or should we wait some days...?
The equalspocalypse landed in r3849 an hour ago.

Posted: Sun Dec 12, 2010 9:44 pm
by Switeck
Will it break this?:

if (!system.isInterstellarSpace && !player.ship.docked && (player.ship.speed > 10*player.ship.maxSpeed || timeAccelerationFactor > 4.0) && !player.alertHostiles && system.shipsWithRole("station", player.ship, 150E3).length < 1 && system.shipsWithRole("station", player.ship, 1500E3).length > 0 && missionVariables.deep_space_pirate_kills < (30 + system.countShipsWithRole("pirate-cove")*20 - system.government*3 ) )
this.addPirate()

(...from my modified+hacked-to-death Deep Space Pirates OXP)

Re: JavaScript semantic change

Posted: Sun Dec 12, 2010 10:03 pm
by Eric Walch
Ahruman wrote:
The correct way to compare vectors is to check whether the distance between them is below some threshold value:

Code: Select all

var equal = v.squaredDistanceTo(u) < thresholdSquared;
There is no comparable metric for quaternions in the current scripting interface. If you’re comparing quaternions, tell me why.
I have compared quaternions for the player.ship to see if it did not change orientation for a certain time. Instead of that I could compare two vectors to achieve the same goal. e.g. vectorUp and vectorRight.

I did add those vectors as ship properties so there is no need to re-calculate them from a quaternion for ships.

Posted: Sun Dec 12, 2010 11:28 pm
by Commander McLane
First of all: Glad to hearing from you after a long hiatus, Ahruman! :D


Switeck wrote:
Will it break this?:

if (!system.isInterstellarSpace && !player.ship.docked && (player.ship.speed > 10*player.ship.maxSpeed || timeAccelerationFactor > 4.0) && !player.alertHostiles && system.shipsWithRole("station", player.ship, 150E3).length < 1 && system.shipsWithRole("station", player.ship, 1500E3).length > 0 && missionVariables.deep_space_pirate_kills < (30 + system.countShipsWithRole("pirate-cove")*20 - system.government*3 ) )
this.addPirate()
Well, it seems to contain neither "==" nor "!=" (which are the only comparators explicitly mentioned). So I would guess the answer is no.

However, I can't claim that I've already understood what exactly will change has changed.
Ahruman wrote:
In particular, it is no longer possible for Oolite to customize the behaviour of the ==/!= operators to usefully compare different objects with the same values. Doing this in the first place was an abuse of the engine which didn’t comply with JavaScript language semantics. This primarily affects vectors and quaternions.

<snip>

==/!= will not work for distinct Oolite objects
I need some clarification here. Am I right in assuming that comparing vectors and quaternions with "==" and "!=" will no longer work (what about "===" and "!=="?), but something like "system.ID == 7" is still okay? Or not, because it's an Oolite object as well?

My confusion is with the exact meaning of "compare different objects" / "distinct Oolite objects". Does it mean that ==/!= is now illegal for use in any Oolite object class (therefore basically everywhere, except in general JS objects like Math), or does it mean that ==/!= is only illegal when comparing two different object with each other (like a vector to a string)?

Sorry for my confusion. I am not enough of a programmer to immediately grasp the concept.

Posted: Sun Dec 12, 2010 11:30 pm
by DaddyHoggy
Commander McLane wrote:
Switeck wrote:
Will it break this?:

if (!system.isInterstellarSpace && !player.ship.docked && (player.ship.speed > 10*player.ship.maxSpeed || timeAccelerationFactor > 4.0) && !player.alertHostiles && system.shipsWithRole("station", player.ship, 150E3).length < 1 && system.shipsWithRole("station", player.ship, 1500E3).length > 0 && missionVariables.deep_space_pirate_kills < (30 + system.countShipsWithRole("pirate-cove")*20 - system.government*3 ) )
this.addPirate()
Well, it seems to contain neither "==" nor "!=" (which are the only comparators explicitly mentioned). So I would guess the answer is no.
I did think the same myself, but declined to point out the obvious flaw in Switeck's post :wink:

Posted: Mon Dec 13, 2010 3:41 am
by Switeck
I guess the other parts of DSP OXP are closer to this, such as checking to see if the player's ship is "within" the space-lanes. But even that seems to lack == or != for its checks.

Posted: Mon Dec 13, 2010 8:05 am
by JensAyton
Commander McLane wrote:
I need some clarification here. Am I right in assuming that comparing vectors and quaternions with "==" and "!=" will no longer work (what about "===" and "!=="?), but something like "system.ID == 7" is still okay? Or not, because it's an Oolite object as well?

My confusion is with the exact meaning of "compare different objects" / "distinct Oolite objects".
Argh, I just lost twenty minutes of detailed explanation, so here’s the short form.

First, brush up on the difference between == and ===.

The old version of SpiderMonkey allowed us to expand the definition of “equivalence” for ==. In particular:

Code: Select all

var v = new Vector3D(1, 2, 3);
var u = v;
var w = new Vector3D(1, 2, 3);

v === u; // True, they refer to the same object.
v == u;  // True, an object is always equal to itself.
v === w; // False, they’re distinct objects.
v == w;  // Formerly true as a magic special case, now false.
Other than vectors and quaternions, Oolite defined special comparisons for JS objects that wrap native Objective-C objects, such that if two JS objects wrapped to the same ObjC object, they were equal. I don’t think this situation can actually arise, it was just a safety net.

Posted: Mon Dec 13, 2010 9:37 am
by Commander McLane
:idea: I think I got it now (which required understanding what "object" means :oops: ).

You cannot compare two vectors to each other anymore (like, let's say, the vector origin->witchpoint to the vector origin->playership, in order to determine whether the playership is located exactly at the witchpoint). This comparison will in future always return false. This will make comparing positions a little trickier, because each position is in fact a (distinct) vector. "player.ship.position == system.mainStation.position" will in future return false when docked (provided the player ship coordinates are indeed the same as the station's coordinates when docked; but I think that is so).

So instead of comparing the two positions with each other, one should in future for instance construct the vector between these positions, and compare that vector's .length property, which still will be "== 0" or "!= 0". (And probably—due to rounding errors—it will never be "== 0", but only "< 0.01".)

But we still can compare properties of objects to whatever we want, using "==" and "!=", like in the small example above.

Right?

Posted: Mon Dec 13, 2010 4:14 pm
by JensAyton
Commander McLane wrote:
But we still can compare properties of objects to whatever we want, using "==" and "!=", like in the small example above.
No, not really.

To fully understand this, you need to distinguish between values and references. A value is, er, a thing, and a reference is a name for a thing. There are essentially two types of reference in JavaScript: variables and properties. There are also two fundamental types of values: primitives and objects. Any reference can refer to a primitive or an object; in particular, properties often refer to other objects.

The primitives are numbers, strings, true, false, null and undefined. Everything else is an object. (Just to confuse matters, there are Number, String and Boolean objects as well as primitive types, but let’s pretend we didn’t hear that.)

One of the special attributes of primitives is that there is conceptually only one primitive of a given type with a given value. For instance, there is only one true and one false, so === comparisons make sense for booleans:

Code: Select all

var a = false;
var b = (0 == 1);

a === b;   // True
Similarly, there is only one number primitive with the value 5:

Code: Select all

var a = 5;
var b = 2 + 3;

a === b;   // True
Another special property of primitives, which goes hand in hand with this, is that they are immutable. Attempts to set a property of a primitive are ignored:

Code: Select all

true.flavour = "strawberry";
true.flavour; // Warning: reference to undefined property true.flavour
(Offhand, I believe this is an error in ECMAScript 5 strict mode.)

The other type of value is objects. An object is a collection of properties, and by default is mutable (i.e. its properties can be changed). Two objects with identical sets of properties and values are still two distinct objects, and changing one does not affect the other:

Code: Select all

var a = new Object;
var b = new Object;

a == b;    // False; the objects are structurally identical, but == doesn’t test this.
a === b;   // False

var c = a;
a == c;    // True
a === c;   // True

a.flavour = "banana"; // The objects are no longer structurally identical.

a.flavour; // "banana"
b.flavour; // undefined
c.flavour; // "banana" — a and c refer to the same object.
Now, if life were simple we’d be able to say that an object never compares equal to anything else, but it isn’t and we can’t. Remember those objects corresponding to primitives I tried to sweep under the carpet above? The Powers That Be decided to try to hide their existence by allowing objects to be compared to numbers and strings. In particular, if you try to compare an object to a number or string primitive, the JS engine will try to call the object’s valueOf() method, then its toString() method, to get a primitive value. (In ECMAScript 5th Edition, which I expect the next release to conform to, toString() is called first when comparing to a string.) Example:

Code: Select all

var strPrimitive = "foo";
var strObjA = new String(strPrimitive);
var strObjB = new String(strPrimitive);
var customObject = { toString: function() { return "foo"; } }

// === is false for all combinations.

strPrimitive == strObjA;      // True
strPrimitive == strObjB;      // True
strObjA == strObjB;           // False!
customObject == strPrimitive; // True!
The old version of SpiderMonkey implements this using a hook that host objects (i.e., objects defined by the program running the JavaScript engine, in this case Oolite) can override. The new version does not.

So to return to an earlier question: system.ID == 7 is OK, because 7 is a number primitive, one of the types for which == is meaningful. player.ship.position == Vector3D(5, 7, 22) isn’t, because == isn’t meaningful for Vector3D objects. This is similar to how you can add numbers using +, but have to use the add() method for vectors. The next version of JavaScript may address this by adding “value types”, but that won’t be out until 2013, so we’ll be at least at test release 1.78 by then. ;-)

Posted: Mon Dec 13, 2010 8:04 pm
by Thargoid
One other small clarification request. Will the usage of == for vectors and quaternions now generate a script error in the logs/console, or just an "incorrect" result when compared to its past usage?

I think I can follow the meaning of the explanation above, but for upgrading OXPs do we need to check that no script errors are reported using suitable trunk versions, or is it a case of explicitly checking things are still operating correctly too in every detail?

Posted: Mon Dec 13, 2010 10:32 pm
by JensAyton
Thargoid wrote:
One other small clarification request. Will the usage of == for vectors and quaternions now generate a script error in the logs/console, or just an "incorrect" result when compared to its past usage?
The comparison will always evaluate to false, unless you happen to have two references to the same vector. There is no exception or error message; comparing objects is valid, just not useful.

Re: JavaScript semantic change

Posted: Sat Dec 18, 2010 6:53 pm
by JensAyton
Eric Walch wrote:
Ahruman wrote:
The correct way to compare vectors is to check whether the distance between them is below some threshold value:

Code: Select all

var equal = v.squaredDistanceTo(u) < thresholdSquared;
There is no comparable metric for quaternions in the current scripting interface. If you’re comparing quaternions, tell me why.
I have compared quaternions for the player.ship to see if it did not change orientation for a certain time. Instead of that I could compare two vectors to achieve the same goal. e.g. vectorUp and vectorRight.
I’m going through Quaternion’s properties to write unit tests, and wondered what the quaternion dot product was actually good for. It turns out that it’s a good measure of difference; similar normalized quaternions have a dot product near 1. For example, oldOrientation.dot(currentOrientation) > 0.999 will detect changes of more than about five degrees around any axis.