From 50816dc466f894df101b8f25858ad7c2523752b8 Mon Sep 17 00:00:00 2001 From: treeform Date: Fri, 7 Aug 2026 08:56:21 -0700 Subject: [PATCH 1/4] Add explicit float classification --- README.md | 8 ++++++++ src/vmath.nim | 12 ++++++++++-- tests/bench_isNan.nim | 10 ++++++---- tests/tests.nim | 25 +++++++++++++++++++++---- vmath.nimble | 2 +- 5 files changed, 46 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d4ba883..9643bbc 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,14 @@ We run identical math operations across vmath, GLSL, [nim-glm](https://github.co ❌ **Orthogonal vector note**: Jolt Physics provides `GetNormalizedPerpendicular()`. vmath's `orthogonal()` returns a perpendicular vector without normalizing it. GLSL and nim-GLM have no direct equivalent, while gl-matrix only computes one internally as part of `quat.rotationTo()`. +# 3.0.0 to 3.1.0 vmath changes: + +Version `3.1.0` adds explicit floating-point classification functions: `isNan`, `isInf`, and `isFinite`. + +* **Breaking change:** `isNan(x)` now returns true only for NaN. It previously also returned true for positive and negative infinity. Replace old checks with `not isFinite(x)` when both NaN and infinity should be rejected. +* **`isInf(x)` added:** Returns true for positive or negative infinity. +* **`isFinite(x)` added:** Returns true for usable finite values, including zero and subnormal values. + # 2.x.x to 3.0.0 vmath breaking changes: Version `3.0.0` changed rotation to be CCW (counter-clockwise) and updated the quaternion conventions to match GLSL, GLM, gl-matrix. Added a multi-library conformance suite. diff --git a/src/vmath.nim b/src/vmath.nim index 62d3bfe..b17e68b 100644 --- a/src/vmath.nim +++ b/src/vmath.nim @@ -635,8 +635,16 @@ proc toDegrees*(deg: SomeInteger): float32 = deg.float32.toDegrees proc isNan*(x: SomeFloat): bool = - ## Returns true if number is a NaN. - x != 0.0 and (x != x or x * 0.5 == x) + ## Returns true if x is NaN. + x != x + +proc isInf*(x: SomeFloat): bool = + ## Returns true if x is positive or negative infinity. + x == Inf or x == -Inf + +proc isFinite*(x: SomeFloat): bool = + ## Returns true if x is finite, including zero and subnormal values. + x == x and x != Inf and x != -Inf proc `zmod`*(a, b: float32): float32 = ## Float point mod. diff --git a/tests/bench_isNan.nim b/tests/bench_isNan.nim index 01ed304..bc638a5 100644 --- a/tests/bench_isNan.nim +++ b/tests/bench_isNan.nim @@ -29,14 +29,16 @@ assert isNaNRyan(5.0e-324) == false assert isNan(float32(0.3)) == false assert isNan(float32(0.0)) == false -assert isNan(float32(0.3/0.0)) == true -assert isNan(float32(-0.3/0.0)) == true +assert isNan(float32(0.3/0.0)) == false +assert isNan(float32(-0.3/0.0)) == false +assert isNan(float32(0.0/0.0)) == true assert isNan(float32(5.0e-324)) == false assert isNan(float64(0.3)) == false assert isNan(float64(0.0)) == false -assert isNan(float64(0.3/0.0)) == true -assert isNan(float64(-0.3/0.0)) == true +assert isNan(float64(0.3/0.0)) == false +assert isNan(float64(-0.3/0.0)) == false +assert isNan(float64(0.0/0.0)) == true assert isNan(float64(5.0e-324)) == false assert isNan3(float32(0.3)) == false diff --git a/tests/tests.nim b/tests/tests.nim index 610700b..3ffe6cd 100644 --- a/tests/tests.nim +++ b/tests/tests.nim @@ -98,10 +98,27 @@ suite "scalar utilities": check angleBetween(0.2, 0.1) ~= -0.1 test "isNan": - check vmath.isNan(float32(0.3)) == false - check vmath.isNan(float32(0.0)) == false - check vmath.isNan(float32(0.3/0.0)) == true - check vmath.isNan(float64(0.3/0.0)) == true + check not vmath.isNan(0.3'f32) + check not vmath.isNan(0.0'f32) + check vmath.isNan(NaN.float32) + check vmath.isNan(NaN.float64) + check not vmath.isNan(Inf.float32) + check not vmath.isNan(NegInf.float64) + + test "isInf": + check vmath.isInf(Inf.float32) + check vmath.isInf(NegInf.float64) + check not vmath.isInf(NaN.float32) + check not vmath.isInf(0.0'f64) + + test "isFinite": + check vmath.isFinite(0.0'f32) + check vmath.isFinite(0.3'f64) + check vmath.isFinite(1.0e-45'f32) # Subnormal float32. + check vmath.isFinite(5.0e-324'f64) # Subnormal float64. + check not vmath.isFinite(NaN.float32) + check not vmath.isFinite(Inf.float64) + check not vmath.isFinite(NegInf.float32) suite "vector memory layout": test "vec2 cast to array": diff --git a/vmath.nimble b/vmath.nimble index 396d743..3c3da4b 100644 --- a/vmath.nimble +++ b/vmath.nimble @@ -1,4 +1,4 @@ -version = "3.0.0" +version = "3.1.0" author = "Andre von Houck" description = "Your single stop for vector math routines for 2d and 3d graphics." license = "MIT" From 44410e317b934b86132370875b548663d0880865 Mon Sep 17 00:00:00 2001 From: treeform Date: Fri, 7 Aug 2026 08:57:45 -0700 Subject: [PATCH 2/4] Test float classification against std math --- tests/tests.nim | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/tests.nim b/tests/tests.nim index 3ffe6cd..8d76a21 100644 --- a/tests/tests.nim +++ b/tests/tests.nim @@ -120,6 +120,23 @@ suite "scalar utilities": check not vmath.isFinite(Inf.float64) check not vmath.isFinite(NegInf.float32) + test "float classification matches std/math": + for x in [NaN.float32, Inf.float32, NegInf.float32, -0.0'f32, + 0.0'f32, 1.0e-45'f32, -1.0e-45'f32, 1.0'f32, -1.0'f32]: + let standardClass = math.classify(x.float64) + check vmath.isNan(x) == math.isNaN(x) + check vmath.isInf(x) == (standardClass in {fcInf, fcNegInf}) + check vmath.isFinite(x) == + (standardClass notin {fcNan, fcInf, fcNegInf}) + + for x in [NaN.float64, Inf.float64, NegInf.float64, -0.0'f64, + 0.0'f64, 5.0e-324'f64, -5.0e-324'f64, 1.0'f64, -1.0'f64]: + let standardClass = math.classify(x) + check vmath.isNan(x) == math.isNaN(x) + check vmath.isInf(x) == (standardClass in {fcInf, fcNegInf}) + check vmath.isFinite(x) == + (standardClass notin {fcNan, fcInf, fcNegInf}) + suite "vector memory layout": test "vec2 cast to array": when not defined(js): From 9d8bf34bfbc5a4422eab9f880b6e04deb3c3f27e Mon Sep 17 00:00:00 2001 From: treeform Date: Fri, 7 Aug 2026 09:13:45 -0700 Subject: [PATCH 3/4] Benchmark direct float classification --- README.md | 2 +- src/vmath.nim | 6 +--- tests/bench_isNan.nim | 78 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9643bbc..edcaee2 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ We run identical math operations across vmath, GLSL, [nim-glm](https://github.co Version `3.1.0` adds explicit floating-point classification functions: `isNan`, `isInf`, and `isFinite`. -* **Breaking change:** `isNan(x)` now returns true only for NaN. It previously also returned true for positive and negative infinity. Replace old checks with `not isFinite(x)` when both NaN and infinity should be rejected. +* **Breaking change:** `isNan(x)` now uses the standard `std/math.isNaN` behavior and returns true only for NaN. It previously also returned true for positive and negative infinity. Replace old checks with `not isFinite(x)` when both NaN and infinity should be rejected. * **`isInf(x)` added:** Returns true for positive or negative infinity. * **`isFinite(x)` added:** Returns true for usable finite values, including zero and subnormal values. diff --git a/src/vmath.nim b/src/vmath.nim index b17e68b..7ecaffc 100644 --- a/src/vmath.nim +++ b/src/vmath.nim @@ -32,7 +32,7 @@ float64 double DVec2 DVec3 DVec4 DMat3 DMat4 DQuat import std/[macros, math, strutils] -export math except isNan +export math {.push inline.} when defined(release): @@ -634,10 +634,6 @@ proc toDegrees*(deg: SomeInteger): float32 = ## Convert degrees to radians. deg.float32.toDegrees -proc isNan*(x: SomeFloat): bool = - ## Returns true if x is NaN. - x != x - proc isInf*(x: SomeFloat): bool = ## Returns true if x is positive or negative infinity. x == Inf or x == -Inf diff --git a/tests/bench_isNan.nim b/tests/bench_isNan.nim index bc638a5..cb60c14 100644 --- a/tests/bench_isNan.nim +++ b/tests/bench_isNan.nim @@ -15,6 +15,12 @@ proc isNaN3*(x: SomeFloat): bool = ## Returns true if number is a NaN. x != 0.0 and (x != x or x * 0.5 == x) +proc isInfClassify(x: SomeFloat): bool = + classify(x.float64) in {fcInf, fcNegInf} + +proc isFiniteClassify(x: SomeFloat): bool = + classify(x.float64) notin {fcNan, fcInf, fcNegInf} + assert isNaNSlow(0.3) == false assert isNaNSlow(0.0) == false assert isNaNSlow(0.3/0.0) == true @@ -53,8 +59,24 @@ assert isNan3(float64(0.3/0.0)) == true assert isNan3(float64(-0.3/0.0)) == true assert isNan3(float64(5.0e-324)) == false +for x in [NaN.float32, Inf.float32, NegInf.float32, 0.0'f32, + 1.0e-45'f32, 1.0'f32]: + assert isInf(x) == isInfClassify(x) + assert isFinite(x) == isFiniteClassify(x) + +for x in [NaN.float64, Inf.float64, NegInf.float64, 0.0'f64, + 5.0e-324'f64, 1.0'f64]: + assert isInf(x) == isInfClassify(x) + assert isFinite(x) == isFiniteClassify(x) + const num = 1_00_000 +let + values32 = [0.0'f32, -0.0'f32, 1.0'f32, -1.0'f32, Inf.float32, + NegInf.float32, NaN.float32, 1.0e-45'f32] + values64 = [0.0'f64, -0.0'f64, 1.0'f64, -1.0'f64, Inf.float64, + NegInf.float64, NaN.float64, 5.0e-324'f64] + timeIt "isNaNSlow float32": var x: float32 var n = 0 @@ -126,3 +148,59 @@ timeIt "isNan3 float64": n += 1 x = float64(i) / float64(i) keep n + +timeIt "isInf direct float32": + var n = 0 + for i in 0 .. num: + if values32[i and 7].isInf: + n += 1 + keep n + +timeIt "isInf classify float32": + var n = 0 + for i in 0 .. num: + if values32[i and 7].isInfClassify: + n += 1 + keep n + +timeIt "isInf direct float64": + var n = 0 + for i in 0 .. num: + if values64[i and 7].isInf: + n += 1 + keep n + +timeIt "isInf classify float64": + var n = 0 + for i in 0 .. num: + if values64[i and 7].isInfClassify: + n += 1 + keep n + +timeIt "isFinite direct float32": + var n = 0 + for i in 0 .. num: + if values32[i and 7].isFinite: + n += 1 + keep n + +timeIt "isFinite classify float32": + var n = 0 + for i in 0 .. num: + if values32[i and 7].isFiniteClassify: + n += 1 + keep n + +timeIt "isFinite direct float64": + var n = 0 + for i in 0 .. num: + if values64[i and 7].isFinite: + n += 1 + keep n + +timeIt "isFinite classify float64": + var n = 0 + for i in 0 .. num: + if values64[i and 7].isFiniteClassify: + n += 1 + keep n From 5c2268d9ce28256ed48410032c206ed737dd7b28 Mon Sep 17 00:00:00 2001 From: treeform Date: Fri, 7 Aug 2026 09:14:34 -0700 Subject: [PATCH 4/4] Keep direct isNan implementation --- README.md | 2 +- src/vmath.nim | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index edcaee2..f53a610 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ We run identical math operations across vmath, GLSL, [nim-glm](https://github.co Version `3.1.0` adds explicit floating-point classification functions: `isNan`, `isInf`, and `isFinite`. -* **Breaking change:** `isNan(x)` now uses the standard `std/math.isNaN` behavior and returns true only for NaN. It previously also returned true for positive and negative infinity. Replace old checks with `not isFinite(x)` when both NaN and infinity should be rejected. +* **Breaking change:** `isNan(x)` now returns true only for NaN, matching standard `std/math.isNaN` behavior. It previously also returned true for positive and negative infinity. Replace old checks with `not isFinite(x)` when both NaN and infinity should be rejected. * **`isInf(x)` added:** Returns true for positive or negative infinity. * **`isFinite(x)` added:** Returns true for usable finite values, including zero and subnormal values. diff --git a/src/vmath.nim b/src/vmath.nim index 7ecaffc..b17e68b 100644 --- a/src/vmath.nim +++ b/src/vmath.nim @@ -32,7 +32,7 @@ float64 double DVec2 DVec3 DVec4 DMat3 DMat4 DQuat import std/[macros, math, strutils] -export math +export math except isNan {.push inline.} when defined(release): @@ -634,6 +634,10 @@ proc toDegrees*(deg: SomeInteger): float32 = ## Convert degrees to radians. deg.float32.toDegrees +proc isNan*(x: SomeFloat): bool = + ## Returns true if x is NaN. + x != x + proc isInf*(x: SomeFloat): bool = ## Returns true if x is positive or negative infinity. x == Inf or x == -Inf