Closed Bug 179965 Opened 22 years ago Closed 22 years ago

JavaScript logic bug: sometimes (a == !a) == true

Categories

(Core :: JavaScript Engine, defect)

x86
Linux
defect
Not set
normal

Tracking

()

VERIFIED WONTFIX

People

(Reporter: nate, Assigned: rogerl)

References

()

Details

User-Agent:       Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.2b) Gecko/20021029 Phoenix/0.4
Build Identifier: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.2b) Gecko/20021029 Phoenix/0.4

I found some strange behaviour of the return value of window.getSelection() when
there is no current selection.  I think this is a logic bug, and not a DOM matter.


Reproducible: Always

Steps to Reproduce:
Unsure if HTML is allowed here, but this should be clear anyway.
Try these little scraps of JavaScript:

<a href="javascript:selection = ''; alert(selection==!selection)">
selection = ''; alert(selection==!selection)
</a>

In this case, you get (and should get) false.  Then try this, with and without
some text selected:

<a href="javascript:selection=window.getSelection();alert(selection==!selection)">
selection=window.getSelection(); alert(selection == !selection) 
</a>




Actual Results:  
I find that without a current selection, I get true: (selection == !selection).


Expected Results:  
Normally, (a == !a) should be false.
Confirming report with Mozilla trunk binary 20021107xx on WinNT.

In the comparison |selection == !selection|, note that the left side
is an object, but the right side is the Boolean value |false|.

Why? For any JavaScript object |obj|, the not operator |!obj| returns
(logical opposite of) |Boolean(obj)| (see ECMA-262 Ed.3 Section 11.4.9).
But |Boolean(obj)| is |true| for any JavaScript object (see Section 9.2),
hence |!obj| is the Boolean value |false|.

That is all working correctly here: |Boolean(selection)| is |true|,
and |!selection| is |false|. So this report reduces to the fact
that we are getting |true| for the comparison |selection == false|.

The == operator does not work by converting both sides to Booleans;
there is a specified order of type conversions in the ECMA-262 spec.
See http://www.mozilla.org/js/language/ for ECMA-262 Edition 3:

11.9.3 The Abstract Equality Comparison Algorithm
The comparison x == y, where x and y are values, produces true or false.
Such a comparison is performed as follows:

1. If Type(x) is different from Type(y), go to step 14.
2. If Type(x) is Undefined, return true.
3. If Type(x) is Null, return true.
4. If Type(x) is not Number, go to step 11.
5. If x is NaN, return false.
6. If y is NaN, return false.
7. If x is the same number value as y, return true.
8. If x is +0 and y is -0, return true.
9. If x is -0 and y is +0, return true.
10. Return false.
11. If Type(x) is String, then return true if x and y are exactly the same 
sequence of characters (same length and
same characters in corresponding positions). Otherwise, return false.
12. If Type(x) is Boolean, return true if x and y are both true or both false. 
Otherwise, return false.
13. Return true if x and y refer to the same object or if they refer to objects 
joined to each other (section 13.1.2).
Otherwise, return false.
14. If x is null and y is undefined, return true.
15. If x is undefined and y is null, return true.
16. If Type(x) is Number and Type(y) is String,
return the result of the comparison x == ToNumber(y).
17. If Type(x) is String and Type(y) is Number,
return the result of the comparison ToNumber(x) == y.
18. If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y.
19. If Type(y) is Boolean, return the result of the comparison x == ToNumber(y).
20. If Type(x) is either String or Number and Type(y) is Object,
return the result of the comparison x == ToPrimitive(y).
21. If Type(x) is Object and Type(y) is either String or Number,
return the result of the comparison ToPrimitive(x) == y.
22. Return false.



Here, since |selection| and |!selection| have different types,
we fall immediately into step 14, then into step 19, where we
are now comparing:

                  |selection == 0|

This brings us to step 21, where we are now comparing:

                  |ToPrimitive(selection) == 0|


Now note that |selection| has an empty toString() value, and
that is what is causing this behavior, I think. Just try this:

 javascript:selection=window.getSelection();alert(selection.toString())



Now look at this session from the standalone JS shell:

----------------------- A NORMAL OBJECT -------------------
js> var obj = {};
js> obj.toString();
[object Object]
js> obj == false;
false
js> obj == !obj;
false


----------------- NOW MAKE |obj.toString()| THE EMPTY STRING -------------
js> obj.toString = function() {return ''}
js> obj.toString();

js> obj == false;
true
js> obj == !obj;
true
So what should ToPrimitive() be doing?  It looks like everything is working
correctly and this is expected behavior for any object which has a ToString() of ""?
Again, from the ECMA-262 Ed.3 spec:

-----
9.1 ToPrimitive
The operator ToPrimitive takes a Value argument and an optional argument 
PreferredType. The operator ToPrimitive converts its value argument to a 
non-Object type. If an object is capable of converting to more than one
primitive type, it may use the optional hint PreferredType to favour that type. 
Conversion occurs according to the following table:

Input Type Result  etc.

Object
Return a default value for the Object. The default value of an object
is retrieved by calling the internal [[DefaultValue]] method of the
object, passing the optional hint PreferredType. The behaviour of the
[[DefaultValue]] method is defined by this specification for all native
ECMAScript objects (section 8.6.2.6).


8.6.2.6 [[DefaultValue]] (hint)
When the [[DefaultValue]] method of O is called with hint String,
the following steps are taken:

1. Call the [[Get]] method of object O with argument "toString".
2. If Result(1) is not an object, go to step 5.
3. Call the [[Call]] method of Result(1), with O as the this value
   and an empty argument list.
4. If Result(3) is a primitive value, return Result(3).
5. Call the [[Get]] method of object O with argument "valueOf".
6. If Result(5) is not an object, go to step 9.
7. Call the [[Call]] method of Result(5), with O as the this value
   and an empty argument list.
8. If Result(7) is a primitive value, return Result(7).
9. Throw a TypeError exception.


When the [[DefaultValue]] method of O is called with hint Number,
the following steps are taken:

1. Call the [[Get]] method of object O with argument "valueOf".
2. If Result(1) is not an object, go to step 5.
3. Call the [[Call]] method of Result(1), with O as the this value
   and an empty argument list.
4. If Result(3) is a primitive value, return Result(3).
5. Call the [[Get]] method of object O with argument "toString".
6. If Result(5) is not an object, go to step 9.
7. Call the [[Call]] method of Result(5), with O as the this value
   and an empty argument list.
8. If Result(7) is a primitive value, return Result(7).
9. Throw a TypeError exception.


When the [[DefaultValue]] method of O is called with no hint, then it
behaves as if the hint were Number, unless O is a Date object (section 15.9),
in which case it behaves as if the hint were String.
-----


So we fall into the hint = Number case, where we first must check
the |valueOf| the object. In the case of |selection| above, I'm assuming
there is no |valueOf| property. So we fall into step 5 and call |toString|.

Since |selection.toString()| is the empty string, this is returned as
the [[DefaultValue]] of |selection|, hence |ToPrimitive(selection)| = ''.

So, the comparison we left off with in the previous comment:

                  |ToPrimitive(selection) == 0|
is seen to be     |'' == 0|


Now, it is well-known that |'' == 0 | evaluates to |true|. Just
refer again to 11.9.3 The Abstract Equality Comparison Algorithm
and see Section9.3.1, ToNumber Applied to the String Type.


So the JavaScript Engine is doing the comparison correctly.
If an object has no |valueOf| property, the result of the 
comparison |obj == false| reduces to |obj.toString() == 0|,
so if |obj.toString()| is the empty string, we get |true|.

As explained at the top of the previous comment, this means
that for such objects, |obj == !obj| will evaluate to |true|, 
since |!obj| is always the Boolean value |false|.
I spoke with waldemar@netscape.com about |obj == !obj| === |true|
in this case. He says it is an unfortunate consquence of the spec,
but not the only surprising behavior of the == comparison operator.

For example, although it is symmetric (|x == y| if and only if |y == x|),
it is not reflexive:

1. |x == x| is not always true! Just let |x| be the value |NaN|,
   it is well-known in JavaScript that |NaN == NaN| evaluates to |false|

and it is not transitive:

2. From ECMA-262 Ed.3, Sect. 11.9.3 The Abstract Equality Comparison Algorithm:
----
The equality operator is not always transitive. For example, there might be
two distinct String objects, each representing the same string value; each 
String object would be considered equal to the string value by the == operator, 
but the two String objects would not be equal to each other.
----


We cannot alter the spec without destroying backward compatibility.
So, I'm going to reassign this to the DOM for consideration:

Why does |window.getSelection()| have an empty toString() value?
Shouldn't |window.getSelection().toString() === '[object Selection]'|
(or something like that)?
Assignee: rogerl → jst
Status: UNCONFIRMED → NEW
Component: JavaScript Engine → DOM Level 0
Ever confirmed: true
QA Contact: pschwartau → desale
No, window.getSelection().toString() should be whatever text is currently
selected.  If there is no text selected, it should be the empty string.  See
http://lxr.mozilla.org/seamonkey/source/content/base/public/nsISelection.idl#167
We can sum everything up by trying the javascript:URL for this bug
on any page that has non-empty content:

 javascript:selection=window.getSelection();alert(selection==!selection)

If you haven't selected anything on the page, |selection.toString()|
is the empty string, and we get "true" from the alert.

If you do select something, then |selection.toString()| is non-empty,
and we get "false" from the alert.

This is according to the ECMA-262 Ed.3 spec for ECMAScript, and 
so we're going to have to mark this as WONTFIX. Any changes to the 
spec on this point will destroy backward compatibility.

Since Boris has answered the DOM question, I'm going to reassign 
to JavaScript Engine before resolving this as WONTFIX -
Assignee: jst → rogerl
Component: DOM Level 0 → JavaScript Engine
QA Contact: desale → pschwartau
Summary: javascript logic bug: sometimes (a == !a) == true → JavaScript logic bug: sometimes (a == !a) == true
Resolving as WONTFIX, per waldemar@netscape.com

Nathan: thank you for this report; it raises an issue I had never
encountered before. "Most" objects have a non-empty toString() value -
Status: NEW → RESOLVED
Closed: 22 years ago
Resolution: --- → WONTFIX
Marking Verified -
Status: RESOLVED → VERIFIED
Wow, that's service:  stunningly clear, fast, efficient.  Undeniably, this
behaviour is according to spec. Thanks to all for your prompt replies and
explanations.
phil, you rock.
You need to log in before you can comment on or make changes to this bug.