Clojure

<= is incorrect when args include Double/NaN

Details

  • Type: Defect Defect
  • Status: Closed Closed
  • Priority: Minor Minor
  • Resolution: Completed
  • Affects Version/s: Release 1.3
  • Fix Version/s: Release 1.3
  • Component/s: None
  • Labels:
    None
  • Environment:
    Mac OS X, Java 6
  • Patch:
    Code and Test

Description

user> (<= 10 Double/NaN 1)
true

(on both 1.2 and 1.3 alpha 5).

  1. 738.diff
    26/Aug/11 11:33 AM
    4 kB
    Luke VanderHart
  2. 738-tests.diff
    26/Aug/11 11:33 AM
    3 kB
    Luke VanderHart

Activity

Hide
Jason Wolfe added a comment -

The source of the issue seems to be incorrect treatment of boxed NaN:

user> (<= 1000 (Double. Double/NaN))
true
user> (<= 1000 (double Double/NaN))
false

Show
Jason Wolfe added a comment - The source of the issue seems to be incorrect treatment of boxed NaN: user> (<= 1000 (Double. Double/NaN)) true user> (<= 1000 (double Double/NaN)) false
Hide
Alexander Taggart added a comment - - edited

Primitive comparisons use java's primitive operators directly, which always return false for NaN, even when testing equality between two NaNs.

In clojure, Number comparisons are all logical variations around calls to Numbers.Ops.lt(Number, Number). So a call to (<= x y) is actually a call to (not (< y x)), which eventually uses the primitive < operator. Alas that logical premise doesn't hold when dealing with NaN:

user=> (<= 1 Double/NaN)
false
user=> (not (< Double/NaN 1))
true

So the bug is not that boxed NaN is treated incorrectly, but rather:

user> (<= 1000 (Double. Double/NaN)) ; becomes !(NaN < 1000) 
true
user> (<= 1000 (double Double/NaN))  ; becomes (1000 <= NaN)
false

In the original example, since there are more than two args, the primitive looking args were boxed:

user=> (<= 10 Double/NaN 1) ; equivalent to logical-and of the following
true
user=> (<= 10 (Double. Double/NaN))  ; becomes !(NaN < 10)
true
user=> (<= (Double. Double/NaN) 1)   ; becomes !(1 < NaN)
true

Note however that java object comparisons for NaNs behave differently: NaN is the largest Double, and NaNs equal each other (see the javadoc).

If we make object NaN comparisons always return false, we would need to add the rest of the comparison methods to Numbers.Ops. Yet doing so could also make collection sorting algorithms behave oddly, deviating from sorting written in java. Besides, (= NaN NaN) => false is annoying.

Clojure already throws out the notion of error-free dividing by zero (which for doubles would otherwise result in NaN or Infinity, depending on the dividend). Perhaps we could similarly error on NaNs passed to clojure numeric ops. They seem to be more trouble than they're worth. That said, people smarter than me thought they were useful.

Then there's that -0.0 nonsense...

Show
Alexander Taggart added a comment - - edited Primitive comparisons use java's primitive operators directly, which always return false for NaN, even when testing equality between two NaNs. In clojure, Number comparisons are all logical variations around calls to Numbers.Ops.lt(Number, Number). So a call to (<= x y) is actually a call to (not (< y x)), which eventually uses the primitive < operator. Alas that logical premise doesn't hold when dealing with NaN:
user=> (<= 1 Double/NaN)
false
user=> (not (< Double/NaN 1))
true
So the bug is not that boxed NaN is treated incorrectly, but rather:
user> (<= 1000 (Double. Double/NaN)) ; becomes !(NaN < 1000) 
true
user> (<= 1000 (double Double/NaN))  ; becomes (1000 <= NaN)
false
In the original example, since there are more than two args, the primitive looking args were boxed:
user=> (<= 10 Double/NaN 1) ; equivalent to logical-and of the following
true
user=> (<= 10 (Double. Double/NaN))  ; becomes !(NaN < 10)
true
user=> (<= (Double. Double/NaN) 1)   ; becomes !(1 < NaN)
true
Note however that java object comparisons for NaNs behave differently: NaN is the largest Double, and NaNs equal each other (see the javadoc). If we make object NaN comparisons always return false, we would need to add the rest of the comparison methods to Numbers.Ops. Yet doing so could also make collection sorting algorithms behave oddly, deviating from sorting written in java. Besides, (= NaN NaN) => false is annoying. Clojure already throws out the notion of error-free dividing by zero (which for doubles would otherwise result in NaN or Infinity, depending on the dividend). Perhaps we could similarly error on NaNs passed to clojure numeric ops. They seem to be more trouble than they're worth. That said, people smarter than me thought they were useful. Then there's that -0.0 nonsense...
Hide
Jouni K. Seppänen added a comment -

On current master, (<= x y) seems to be special-cased by the compiler, but when <= is called dynamically, the bug is still there:

user=> (<= 1 Float/NaN)
false
user=> (let [op <=] (op 1 Float/NaN))
true

Since CLJ-354 got marked "Completed", perhaps there was an attempt to fix this.

Show
Jouni K. Seppänen added a comment - On current master, (<= x y) seems to be special-cased by the compiler, but when <= is called dynamically, the bug is still there:
user=> (<= 1 Float/NaN)
false
user=> (let [op <=] (op 1 Float/NaN))
true
Since CLJ-354 got marked "Completed", perhaps there was an attempt to fix this.
Hide
Alexander Taggart added a comment -

Using let forces calling <= as a function rather than inlining Numbers/lte, which means the args are treated as objects not primitives, thus the different behaviour as I discussed earlier.

Show
Alexander Taggart added a comment - Using let forces calling <= as a function rather than inlining Numbers/lte, which means the args are treated as objects not primitives, thus the different behaviour as I discussed earlier.
Alexander Taggart made changes -
Field Original Value New Value
Waiting On richhickey
Hide
Aaron Bedra added a comment -

Rich, what should the behavior be?

Show
Aaron Bedra added a comment - Rich, what should the behavior be?
Aaron Bedra made changes -
Fix Version/s Release.Next [ 10038 ]
Affects Version/s Release 1.2 [ 10037 ]
Priority Critical [ 2 ] Minor [ 4 ]
Hide
Jouni K. Seppänen added a comment -

My suggestion for the behavior is to follow Java (Java Language Specification §15.20.1) and IEEE 754. The java.sun.com site seems to be down right now, but here's a Google cache link:

http://webcache.googleusercontent.com/search?q=cache:http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#15.20.1

Show
Jouni K. Seppänen added a comment - My suggestion for the behavior is to follow Java (Java Language Specification §15.20.1) and IEEE 754. The java.sun.com site seems to be down right now, but here's a Google cache link: http://webcache.googleusercontent.com/search?q=cache:http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#15.20.1
Hide
Rich Hickey added a comment -

It should work the same as primitive double in all cases

Show
Rich Hickey added a comment - It should work the same as primitive double in all cases
Rich Hickey made changes -
Waiting On richhickey
Luke VanderHart made changes -
Assignee Luke VanderHart [ lvanderhart ]
Luke VanderHart made changes -
Status Open [ 1 ] In Progress [ 3 ]
Hide
Luke VanderHart added a comment - - edited

Added patches. The problem was that our logic for lte/gte depended on the fact that lte is equivalent to !gt.

However, in Java, this assumption is invalid - any comparison involving NaN always yields false.

The fix was to adding lte and gte methods to Numbers.Ops directly, rather than implementing everything in terms of lt. This was the only fix I could see that didn't incur the cost of runtime checks for NaN.

Show
Luke VanderHart added a comment - - edited Added patches. The problem was that our logic for lte/gte depended on the fact that lte is equivalent to !gt. However, in Java, this assumption is invalid - any comparison involving NaN always yields false. The fix was to adding lte and gte methods to Numbers.Ops directly, rather than implementing everything in terms of lt. This was the only fix I could see that didn't incur the cost of runtime checks for NaN.
Luke VanderHart made changes -
Attachment 738.diff [ 10321 ]
Attachment 738-tests.diff [ 10322 ]
Luke VanderHart made changes -
Status In Progress [ 3 ] Resolved [ 5 ]
Resolution Completed [ 1 ]
Patch Code and Test
Stuart Halloway made changes -
Status Resolved [ 5 ] Closed [ 6 ]

People

Vote (1)
Watch (4)

Dates

  • Created:
    Updated:
    Resolved: