Skip to content

Commit f7ceae7

Browse files
authored
fix(stdlib): Fix NaN comparisons (#1543)
feat(runtime): Optimize simple number comparison
1 parent eeb2eaa commit f7ceae7

File tree

6 files changed

+88
-55
lines changed

6 files changed

+88
-55
lines changed

compiler/test/stdlib/pervasives.test.gr

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ assert compare(0.0 / 0.0, -1 / 0.0) < 0
1111
assert compare(0.0 / 0.0, 987654321.) < 0
1212
assert compare(0.0 / 0.0, 987654321) < 0
1313
assert compare(0.0 / 0.0, 0) < 0
14+
assert !(0.0 / 0.0 < 0.0 / 0.0)
15+
assert !(0.0 / 0.0 < 10)
16+
assert !(0.0 / 0.0 < 10.)
17+
assert !(0.0 / 0.0 <= 0.0 / 0.0)
18+
assert !(0.0 / 0.0 <= 10)
19+
assert !(0.0 / 0.0 <= 10.)
20+
assert !(0.0 / 0.0 > 0.0 / 0.0)
21+
assert !(0.0 / 0.0 > 10)
22+
assert !(0.0 / 0.0 > 10.)
23+
assert !(0.0 / 0.0 >= 0.0 / 0.0)
24+
assert !(0.0 / 0.0 >= 10)
25+
assert !(0.0 / 0.0 >= 10.)
1426
// Booleans
1527
assert compare(false, true) < 0
1628
assert compare(true, false) > 0

stdlib/number.gr

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
isInteger,
1717
isRational,
1818
isBoxedNumber,
19+
isNaN,
1920
scalbn,
2021
} from "runtime/numbers"
2122
import Atoi from "runtime/atoi/parse"
@@ -814,8 +815,7 @@ export let isFinite = (x: Number) => {
814815
}
815816

816817
/**
817-
* Checks if a number contains the NaN value (Not A Number).
818-
* Only boxed floating point numbers can contain NaN.
818+
* Checks if a number is the float NaN value (Not A Number).
819819
*
820820
* @param x: The number to check
821821
* @returns `true` if the value is NaN, otherwise `false`
@@ -825,25 +825,7 @@ export let isFinite = (x: Number) => {
825825
@unsafe
826826
export let isNaN = (x: Number) => {
827827
let asPtr = WasmI32.fromGrain(x)
828-
if (isBoxedNumber(asPtr)) {
829-
// Boxed numbers can have multiple subtypes, of which float32 and float64 can be NaN.
830-
let tag = WasmI32.load(asPtr, 4n)
831-
if (WasmI32.eq(tag, Tags._GRAIN_FLOAT64_BOXED_NUM_TAG)) {
832-
// uses the fact that NaN is the only number not equal to itself
833-
let wf64 = WasmF64.load(asPtr, 8n)
834-
WasmF64.ne(wf64, wf64)
835-
} else if (WasmI32.eq(tag, Tags._GRAIN_FLOAT32_BOXED_NUM_TAG)) {
836-
let wf32 = WasmF32.load(asPtr, 8n)
837-
WasmF32.ne(wf32, wf32)
838-
} else {
839-
// Neither rational numbers nor boxed integers can be infinite or NaN.
840-
// Grain doesn't allow creating a rational with denominator of zero either.
841-
false
842-
}
843-
} else {
844-
// Simple numbers are integers and cannot be NaN.
845-
false
846-
}
828+
isNaN(asPtr)
847829
}
848830

849831
/**

stdlib/number.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -656,8 +656,7 @@ No other changes yet.
656656
isNaN : Number -> Bool
657657
```
658658

659-
Checks if a number contains the NaN value (Not A Number).
660-
Only boxed floating point numbers can contain NaN.
659+
Checks if a number is the float NaN value (Not A Number).
661660

662661
Parameters:
663662

stdlib/runtime/compare.gr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ compareHelp = (x, y) => {
161161
}
162162
} else if (isNumber(x)) {
163163
// Numbers have special comparison rules, e.g. NaN == NaN
164-
tagSimpleNumber(numberCompare(x, y, true))
164+
tagSimpleNumber(numberCompare(x, y))
165165
} else {
166166
// Handle all other heap allocated things
167167
// Can short circuit if pointers are the same

stdlib/runtime/numbers.gr

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,29 @@ export let isRational = x => {
107107
}
108108
}
109109

110+
@unsafe
111+
export let isNaN = x => {
112+
if (isBoxedNumber(x)) {
113+
// Boxed numbers can have multiple subtypes, of which float32 and float64 can be NaN.
114+
let tag = WasmI32.load(x, 4n)
115+
if (WasmI32.eq(tag, Tags._GRAIN_FLOAT64_BOXED_NUM_TAG)) {
116+
// uses the fact that NaN is the only number not equal to itself
117+
let wf64 = WasmF64.load(x, 8n)
118+
WasmF64.ne(wf64, wf64)
119+
} else if (WasmI32.eq(tag, Tags._GRAIN_FLOAT32_BOXED_NUM_TAG)) {
120+
let wf32 = WasmF32.load(x, 8n)
121+
WasmF32.ne(wf32, wf32)
122+
} else {
123+
// Neither rational numbers nor boxed integers can be infinite or NaN.
124+
// Grain doesn't allow creating a rational with denominator of zero either.
125+
false
126+
}
127+
} else {
128+
// Simple numbers are integers and cannot be NaN.
129+
false
130+
}
131+
}
132+
110133
@unsafe
111134
let isBigInt = x => {
112135
if (isBoxedNumber(x)) {
@@ -1777,8 +1800,11 @@ let cmpBigInt = (x: WasmI32, y: WasmI32) => {
17771800
}
17781801
}
17791802

1803+
// cmpFloat applies a total ordering relation:
1804+
// unlike regular float logic, NaN is considered equal to itself and
1805+
// smaller than any other number
17801806
@unsafe
1781-
let cmpFloat = (x: WasmI32, y: WasmI32, is64: Bool, totalOrdering: Bool) => {
1807+
let cmpFloat = (x: WasmI32, y: WasmI32, is64: Bool) => {
17821808
let xf = if (is64) {
17831809
boxedFloat64Number(x)
17841810
} else {
@@ -1787,13 +1813,13 @@ let cmpFloat = (x: WasmI32, y: WasmI32, is64: Bool, totalOrdering: Bool) => {
17871813
if (isSimpleNumber(y)) {
17881814
let yf = WasmF64.convertI32S(untagSimple(y))
17891815
// special NaN cases
1790-
if (totalOrdering && WasmF64.ne(xf, xf)) {
1816+
if (WasmF64.ne(xf, xf)) {
17911817
if (WasmF64.ne(yf, yf)) {
17921818
0n
17931819
} else {
17941820
-1n
17951821
}
1796-
} else if (totalOrdering && WasmF64.ne(yf, yf)) {
1822+
} else if (WasmF64.ne(yf, yf)) {
17971823
if (WasmF64.ne(xf, xf)) {
17981824
0n
17991825
} else {
@@ -1834,13 +1860,13 @@ let cmpFloat = (x: WasmI32, y: WasmI32, is64: Bool, totalOrdering: Bool) => {
18341860
},
18351861
}
18361862
// special NaN cases
1837-
if (totalOrdering && WasmF64.ne(xf, xf)) {
1863+
if (WasmF64.ne(xf, xf)) {
18381864
if (WasmF64.ne(yf, yf)) {
18391865
0n
18401866
} else {
18411867
-1n
18421868
}
1843-
} else if (totalOrdering && WasmF64.ne(yf, yf)) {
1869+
} else if (WasmF64.ne(yf, yf)) {
18441870
if (WasmF64.ne(xf, xf)) {
18451871
0n
18461872
} else {
@@ -1854,7 +1880,7 @@ let cmpFloat = (x: WasmI32, y: WasmI32, is64: Bool, totalOrdering: Bool) => {
18541880
}
18551881

18561882
@unsafe
1857-
let cmpSmallInt = (x: WasmI32, y: WasmI32, is64: Bool, totalOrdering: Bool) => {
1883+
let cmpSmallInt = (x: WasmI32, y: WasmI32, is64: Bool) => {
18581884
let xi = if (is64) {
18591885
boxedInt64Number(x)
18601886
} else {
@@ -1890,10 +1916,10 @@ let cmpSmallInt = (x: WasmI32, y: WasmI32, is64: Bool, totalOrdering: Bool) => {
18901916
) -1n else 1n
18911917
},
18921918
t when WasmI32.eq(t, Tags._GRAIN_FLOAT32_BOXED_NUM_TAG) => {
1893-
WasmI32.sub(0n, cmpFloat(y, x, false, totalOrdering))
1919+
WasmI32.sub(0n, cmpFloat(y, x, false))
18941920
},
18951921
t when WasmI32.eq(t, Tags._GRAIN_FLOAT64_BOXED_NUM_TAG) => {
1896-
WasmI32.sub(0n, cmpFloat(y, x, true, totalOrdering))
1922+
WasmI32.sub(0n, cmpFloat(y, x, true))
18971923
},
18981924
_ => {
18991925
throw UnknownNumberTag
@@ -1903,7 +1929,7 @@ let cmpSmallInt = (x: WasmI32, y: WasmI32, is64: Bool, totalOrdering: Bool) => {
19031929
}
19041930

19051931
@unsafe
1906-
let cmpRational = (x: WasmI32, y: WasmI32, totalOrdering: Bool) => {
1932+
let cmpRational = (x: WasmI32, y: WasmI32) => {
19071933
if (isSimpleNumber(y)) {
19081934
let xf = WasmF64.div(
19091935
BI.toFloat64(boxedRationalNumerator(x)),
@@ -1915,10 +1941,10 @@ let cmpRational = (x: WasmI32, y: WasmI32, totalOrdering: Bool) => {
19151941
let yBoxedNumberTag = boxedNumberTag(y)
19161942
match (yBoxedNumberTag) {
19171943
t when WasmI32.eq(t, Tags._GRAIN_INT32_BOXED_NUM_TAG) => {
1918-
WasmI32.sub(0n, cmpSmallInt(y, x, false, totalOrdering))
1944+
WasmI32.sub(0n, cmpSmallInt(y, x, false))
19191945
},
19201946
t when WasmI32.eq(t, Tags._GRAIN_INT64_BOXED_NUM_TAG) => {
1921-
WasmI32.sub(0n, cmpSmallInt(y, x, true, totalOrdering))
1947+
WasmI32.sub(0n, cmpSmallInt(y, x, true))
19221948
},
19231949
t when WasmI32.eq(t, Tags._GRAIN_BIGINT_BOXED_NUM_TAG) => {
19241950
WasmI32.sub(0n, cmpBigInt(y, x))
@@ -1951,10 +1977,10 @@ let cmpRational = (x: WasmI32, y: WasmI32, totalOrdering: Bool) => {
19511977
}
19521978
},
19531979
t when WasmI32.eq(t, Tags._GRAIN_FLOAT32_BOXED_NUM_TAG) => {
1954-
WasmI32.sub(0n, cmpFloat(y, x, false, totalOrdering))
1980+
WasmI32.sub(0n, cmpFloat(y, x, false))
19551981
},
19561982
t when WasmI32.eq(t, Tags._GRAIN_FLOAT64_BOXED_NUM_TAG) => {
1957-
WasmI32.sub(0n, cmpFloat(y, x, true, totalOrdering))
1983+
WasmI32.sub(0n, cmpFloat(y, x, true))
19581984
},
19591985
_ => {
19601986
throw UnknownNumberTag
@@ -1964,30 +1990,31 @@ let cmpRational = (x: WasmI32, y: WasmI32, totalOrdering: Bool) => {
19641990
}
19651991

19661992
@unsafe
1967-
export let cmp = (x: WasmI32, y: WasmI32, totalOrdering: Bool) => {
1993+
export let cmp = (x: WasmI32, y: WasmI32) => {
19681994
if (isSimpleNumber(x)) {
19691995
if (isSimpleNumber(y)) {
1970-
if (WasmI32.ltS(x, y)) -1n else if (WasmI32.gtS(x, y)) 1n else 0n
1996+
// fast comparison path for simple numbers
1997+
WasmI32.sub(x, y)
19711998
} else {
19721999
let yBoxedNumberTag = boxedNumberTag(y)
19732000
match (yBoxedNumberTag) {
19742001
t when WasmI32.eq(t, Tags._GRAIN_INT32_BOXED_NUM_TAG) => {
1975-
WasmI32.sub(0n, cmpSmallInt(y, x, false, totalOrdering))
2002+
WasmI32.sub(0n, cmpSmallInt(y, x, false))
19762003
},
19772004
t when WasmI32.eq(t, Tags._GRAIN_INT64_BOXED_NUM_TAG) => {
1978-
WasmI32.sub(0n, cmpSmallInt(y, x, true, totalOrdering))
2005+
WasmI32.sub(0n, cmpSmallInt(y, x, true))
19792006
},
19802007
t when WasmI32.eq(t, Tags._GRAIN_BIGINT_BOXED_NUM_TAG) => {
19812008
WasmI32.sub(0n, cmpBigInt(y, x))
19822009
},
19832010
t when WasmI32.eq(t, Tags._GRAIN_RATIONAL_BOXED_NUM_TAG) => {
1984-
WasmI32.sub(0n, cmpRational(y, x, totalOrdering))
2011+
WasmI32.sub(0n, cmpRational(y, x))
19852012
},
19862013
t when WasmI32.eq(t, Tags._GRAIN_FLOAT32_BOXED_NUM_TAG) => {
1987-
WasmI32.sub(0n, cmpFloat(y, x, false, totalOrdering))
2014+
WasmI32.sub(0n, cmpFloat(y, x, false))
19882015
},
19892016
t when WasmI32.eq(t, Tags._GRAIN_FLOAT64_BOXED_NUM_TAG) => {
1990-
WasmI32.sub(0n, cmpFloat(y, x, true, totalOrdering))
2017+
WasmI32.sub(0n, cmpFloat(y, x, true))
19912018
},
19922019
_ => {
19932020
throw UnknownNumberTag
@@ -1998,22 +2025,22 @@ export let cmp = (x: WasmI32, y: WasmI32, totalOrdering: Bool) => {
19982025
let xBoxedNumberTag = boxedNumberTag(x)
19992026
match (xBoxedNumberTag) {
20002027
t when WasmI32.eq(t, Tags._GRAIN_INT32_BOXED_NUM_TAG) => {
2001-
cmpSmallInt(x, y, false, totalOrdering)
2028+
cmpSmallInt(x, y, false)
20022029
},
20032030
t when WasmI32.eq(t, Tags._GRAIN_INT64_BOXED_NUM_TAG) => {
2004-
cmpSmallInt(x, y, true, totalOrdering)
2031+
cmpSmallInt(x, y, true)
20052032
},
20062033
t when WasmI32.eq(t, Tags._GRAIN_BIGINT_BOXED_NUM_TAG) => {
20072034
cmpBigInt(x, y)
20082035
},
20092036
t when WasmI32.eq(t, Tags._GRAIN_RATIONAL_BOXED_NUM_TAG) => {
2010-
cmpRational(x, y, totalOrdering)
2037+
cmpRational(x, y)
20112038
},
20122039
t when WasmI32.eq(t, Tags._GRAIN_FLOAT32_BOXED_NUM_TAG) => {
2013-
cmpFloat(x, y, false, totalOrdering)
2040+
cmpFloat(x, y, false)
20142041
},
20152042
t when WasmI32.eq(t, Tags._GRAIN_FLOAT64_BOXED_NUM_TAG) => {
2016-
cmpFloat(x, y, true, totalOrdering)
2043+
cmpFloat(x, y, true)
20172044
},
20182045
_ => {
20192046
throw UnknownNumberTag
@@ -2022,39 +2049,46 @@ export let cmp = (x: WasmI32, y: WasmI32, totalOrdering: Bool) => {
20222049
}
20232050
}
20242051

2052+
// In the comparison functions below, NaN is neither greater than, less than,
2053+
// or equal to any other number (including NaN), so any comparison involving
2054+
// NaN is always false. The only exception to this rule is `compare`, which
2055+
// applies a total ordering relation to allow numbers to be sortable (with
2056+
// NaN being considered equal to itself and less than all other numbers in
2057+
// this case).
2058+
20252059
@unsafe
20262060
export let (<) = (x: Number, y: Number) => {
20272061
let x = WasmI32.fromGrain(x)
20282062
let y = WasmI32.fromGrain(y)
2029-
WasmI32.ltS(cmp(x, y, false), 0n)
2063+
!isNaN(x) && !isNaN(y) && WasmI32.ltS(cmp(x, y), 0n)
20302064
}
20312065

20322066
@unsafe
20332067
export let (>) = (x: Number, y: Number) => {
20342068
let x = WasmI32.fromGrain(x)
20352069
let y = WasmI32.fromGrain(y)
2036-
WasmI32.gtS(cmp(x, y, false), 0n)
2070+
!isNaN(x) && !isNaN(y) && WasmI32.gtS(cmp(x, y), 0n)
20372071
}
20382072

20392073
@unsafe
20402074
export let (<=) = (x: Number, y: Number) => {
20412075
let x = WasmI32.fromGrain(x)
20422076
let y = WasmI32.fromGrain(y)
2043-
WasmI32.leS(cmp(x, y, false), 0n)
2077+
!isNaN(x) && !isNaN(y) && WasmI32.leS(cmp(x, y), 0n)
20442078
}
20452079

20462080
@unsafe
20472081
export let (>=) = (x: Number, y: Number) => {
20482082
let x = WasmI32.fromGrain(x)
20492083
let y = WasmI32.fromGrain(y)
2050-
WasmI32.geS(cmp(x, y, false), 0n)
2084+
!isNaN(x) && !isNaN(y) && WasmI32.geS(cmp(x, y), 0n)
20512085
}
20522086

20532087
@unsafe
20542088
export let compare = (x: Number, y: Number) => {
20552089
let x = WasmI32.fromGrain(x)
20562090
let y = WasmI32.fromGrain(y)
2057-
WasmI32.toGrain(tagSimple(cmp(x, y, true))): Number
2091+
WasmI32.toGrain(tagSimple(cmp(x, y))): Number
20582092
}
20592093

20602094
/*

stdlib/runtime/numbers.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ isInteger : WasmI32 -> Bool
2222
isRational : WasmI32 -> Bool
2323
```
2424

25+
### Numbers.**isNaN**
26+
27+
```grain
28+
isNaN : WasmI32 -> Bool
29+
```
30+
2531
### Numbers.**isNumber**
2632

2733
```grain
@@ -109,7 +115,7 @@ numberEqual : (WasmI32, WasmI32) -> Bool
109115
### Numbers.**cmp**
110116

111117
```grain
112-
cmp : (WasmI32, WasmI32, Bool) -> WasmI32
118+
cmp : (WasmI32, WasmI32) -> WasmI32
113119
```
114120

115121
### Numbers.**(<)**

0 commit comments

Comments
 (0)