From fa728d24437593f1eb60d65131d990ece27a5f30 Mon Sep 17 00:00:00 2001 From: Spotandjake <40705786+spotandjake@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:09:51 -0400 Subject: [PATCH] feat(stdlib): Add `isFinite`, `isClose`, `sin`, `cos`, `tan` to Float64 (#2166) --- compiler/test/stdlib/float64.test.gr | 208 +++++++++++++++++++++++++++ stdlib/float64.gr | 108 ++++++++++++++ stdlib/float64.md | 205 ++++++++++++++++++++++++++ 3 files changed, 521 insertions(+) diff --git a/compiler/test/stdlib/float64.test.gr b/compiler/test/stdlib/float64.test.gr index 361cfcad9..aae4e5e56 100644 --- a/compiler/test/stdlib/float64.test.gr +++ b/compiler/test/stdlib/float64.test.gr @@ -116,6 +116,16 @@ assert !(5.0d >= 22.0d) assert !(5.0d < -17.0d) assert !(5.0d <= 4.0d) +// isFinite +assert Float64.isFinite(NaNd) == false +assert Float64.isFinite(Infinityd) == false +assert Float64.isFinite(-Infinityd) == false +assert Float64.isFinite(1.0d) +assert Float64.isFinite(0.0d) +assert Float64.isFinite(-1.0d) +assert Float64.isFinite(25.76d) +assert Float64.isFinite(-25.00d) + // isNaN assert Float64.isNaN(NaNd) assert Float64.isNaN(1.0d) == false @@ -211,3 +221,201 @@ assert Float64.isNaN(Float64.copySign(NaNd, 1.0d)) assert Float64.isNaN(Float64.copySign(NaNd, -1.0d)) assert Float64.copySign(1.0d, NaNd) == 1.0d assert Float64.copySign(1.0d, -NaNd) == -1.0d + +// Float64.isClose +assert Float64.isClose(1.0d, 1.0d) +assert Float64.isClose( + 1.0d, + 1.0d, + relativeTolerance=0.5d, + absoluteTolerance=0.5d +) +assert Float64.isClose( + 1.0d, + 1.0d, + relativeTolerance=0.0d, + absoluteTolerance=0.0d +) +assert Float64.isClose(0.0d, 0.0d) +assert Float64.isClose( + 0.0d, + 0.0d, + relativeTolerance=0.5d, + absoluteTolerance=0.5d +) +assert Float64.isClose( + 0.0d, + 0.0d, + relativeTolerance=0.0d, + absoluteTolerance=0.0d +) +assert Float64.isClose(0.0d, 0.1d) == false +assert Float64.isClose(0.0d, 0.000000001d) == false +assert Float64.isClose(0.0d, 0.00000001d, absoluteTolerance=1e-9d) == false +assert Float64.isClose(0.0d, 0.000000001d, absoluteTolerance=1e-9d) +assert Float64.isClose(-0.0d, 0.000000001d) == false +assert Float64.isClose(-0.0d, 0.00000001d, absoluteTolerance=1e-9d) == false +assert Float64.isClose(-0.0d, 0.000000001d, absoluteTolerance=1e-9d) +assert Float64.isClose(1.1d, 1.10000001d, absoluteTolerance=1e-9d) == false +assert Float64.isClose(1.1d, 1.100000001d, absoluteTolerance=1e-9d) +assert Float64.isClose(Infinityd, Infinityd) +assert Float64.isClose(-Infinityd, -Infinityd) +assert Float64.isClose(Infinityd, -Infinityd) == false +assert Float64.isClose(NaNd, NaNd) == false + +// Float64.sin - 0 to pi/2 +assert Float64.sin(0.0d) == 0.0d +assert Float64.isClose(Float64.sin(Float64.pi / 6.0d), 0.5d) +assert Float64.isClose( + Float64.sin(Float64.pi / 4.0d), + Float64.sqrt(2.0d) / 2.0d +) +assert Float64.isClose( + Float64.sin(Float64.pi / 3.0d), + Float64.sqrt(3.0d) / 2.0d +) +assert Float64.isClose(Float64.sin(Float64.pi / 2.0d), 1.0d) +// Float64.sin - pi/2 to 2pi +assert Float64.isClose( + Float64.sin(2.0d * Float64.pi / 3.0d), + Float64.sqrt(3.0d) / 2.0d +) +assert Float64.isClose( + Float64.sin(3.0d * Float64.pi / 4.0d), + Float64.sqrt(2.0d) / 2.0d +) +assert Float64.isClose(Float64.sin(5.0d * Float64.pi / 6.0d), 0.5d) +// Note: This has an absolute error of 1e-15 because `Float64.pi` is not exact +assert Float64.isClose(Float64.sin(Float64.pi), 0.0d, absoluteTolerance=1e-15d) +// Float64.sin - 2pi to 3pi/2 +assert Float64.isClose(Float64.sin(7.0d * Float64.pi / 6.0d), -0.5d) +assert Float64.isClose( + Float64.sin(5.0d * Float64.pi / 4.0d), + Float64.sqrt(2.0d) / -2.0d +) +assert Float64.isClose( + Float64.sin(4.0d * Float64.pi / 3.0d), + Float64.sqrt(3.0d) / -2.0d +) +assert Float64.isClose(Float64.sin(3.0d * Float64.pi / 2.0d), -1.0d) +// Float64.sin - 3pi/2 to 0 +assert Float64.isClose( + Float64.sin(5.0d * Float64.pi / 3.0d), + Float64.sqrt(3.0d) / -2.0d +) +assert Float64.isClose( + Float64.sin(7.0d * Float64.pi / 4.0d), + Float64.sqrt(2.0d) / -2.0d +) +assert Float64.isClose(Float64.sin(11.0d * Float64.pi / 6.0d), -0.5d) +// Note: This has an absolute error of 1e-15 because `Float64.pi` is not exact +assert Float64.isClose( + Float64.sin(2.0d * Float64.pi), + 0.0d, + absoluteTolerance=1e-15d +) +// Float64.sin - special cases +assert Float64.sin(0.5d) == Float64.sin(0.5d) +assert Float64.sin(0.25d) == Float64.sin(0.25d) +assert Float64.isClose( // Note: We lose a lot of precision here do to ieee754 representation + Float64.sin(1.7976931348623157e+308d), + 0.0049619d, + absoluteTolerance=1e7d +) // Max F64 +assert Float64.isClose( + Float64.sin(-1.7976931348623157e+308d), + 0.00496d, + absoluteTolerance=1e7d +) // Min F64 +assert Float64.isNaN(Float64.sin(Infinityd)) +assert Float64.isNaN(Float64.sin(-Infinityd)) +assert Float64.isNaN(Float64.sin(NaNd)) + +// Float64.cos - 0 to pi/2 +assert Float64.cos(0.0d) == 1.0d +assert Float64.isClose( + Float64.cos(Float64.pi / 6.0d), + Float64.sqrt(3.0d) / 2.0d +) +assert Float64.isClose( + Float64.cos(Float64.pi / 4.0d), + Float64.sqrt(2.0d) / 2.0d +) +assert Float64.isClose(Float64.cos(Float64.pi / 3.0d), 0.5d) +// Note: This has an absolute error of 1e-15 because `Float64.pi` is not exact +assert Float64.isClose( + Float64.cos(Float64.pi / 2.0d), + 0.0d, + absoluteTolerance=1e-15d +) +// Float64.cos - pi/2 to 2pi +assert Float64.isClose(Float64.cos(2.0d * Float64.pi / 3.0d), -0.5d) +assert Float64.isClose( + Float64.cos(3.0d * Float64.pi / 4.0d), + Float64.sqrt(2.0d) / -2.0d +) +assert Float64.isClose( + Float64.cos(5.0d * Float64.pi / 6.0d), + Float64.sqrt(3.0d) / -2.0d +) +assert Float64.isClose(Float64.cos(Float64.pi), -1.0d) +// Float64.cos - 2pi to 3pi/2 +assert Float64.isClose( + Float64.cos(7.0d * Float64.pi / 6.0d), + Float64.sqrt(3.0d) / -2.0d +) +assert Float64.isClose( + Float64.cos(5.0d * Float64.pi / 4.0d), + Float64.sqrt(2.0d) / -2.0d +) +assert Float64.isClose(Float64.cos(4.0d * Float64.pi / 3.0d), -0.5d) +// Note: This has an absolute error of 1e-15 because `Float64.pi` is not exact +assert Float64.isClose( + Float64.cos(3.0d * Float64.pi / 2.0d), + 0.0d, + absoluteTolerance=1e-15d +) +// Float64.cos - 3pi/2 to 0 +assert Float64.isClose(Float64.cos(5.0d * Float64.pi / 3.0d), 0.5d) +assert Float64.isClose( + Float64.cos(7.0d * Float64.pi / 4.0d), + Float64.sqrt(2.0d) / 2.0d +) +assert Float64.isClose( + Float64.cos(11.0d * Float64.pi / 6.0d), + Float64.sqrt(3.0d) / 2.0d +) +assert Float64.isClose(Float64.cos(2.0d * Float64.pi), 1.0d) +// Float64.cos - special cases +assert Float64.cos(0.5d) == Float64.cos(0.5d) +assert Float64.cos(0.25d) == Float64.cos(0.25d) +assert Float64.isNaN(Float64.cos(Infinityd)) +assert Float64.isNaN(Float64.cos(-Infinityd)) +assert Float64.isNaN(Float64.cos(NaNd)) + +// Float64.tan - base cases +assert Float64.tan(0.0d) == 0.0d +assert Float64.isClose( + Float64.tan(Float64.pi / 6.0d), + 1.0d / Float64.sqrt(3.0d) +) +assert Float64.isClose(Float64.tan(Float64.pi / 4.0d), 1.0d) +assert Float64.isClose(Float64.tan(Float64.pi / 3.0d), Float64.sqrt(3.0d)) +// Note: one might expect this to produce infinity but instead we produce 16331239353195370 because pi can not be represented accurately in iee754, This logic follows c +assert Float64.isClose(Float64.tan(Float64.pi / 2.0d), 16331239353195370.0d) +// Float64.tan - special cases +assert Float64.tan(0.5d) == Float64.tan(0.5d) +assert Float64.tan(0.25d) == Float64.tan(0.25d) +assert Float64.isClose( // Note: We lose a lot of precision here do to ieee754 representation + Float64.tan(1.7976931348623157e+308d), + -0.00496201587d, + absoluteTolerance=1e7d +) // Max F64 +assert Float64.isClose( + Float64.tan(-1.7976931348623157e+308d), + -0.00496201587d, + absoluteTolerance=1e7d +) // Max F64 +assert Float64.isNaN(Float64.tan(Infinityd)) +assert Float64.isNaN(Float64.tan(-Infinityd)) +assert Float64.isNaN(Float64.tan(NaNd)) diff --git a/stdlib/float64.gr b/stdlib/float64.gr index d33b42857..0a0cbd40c 100644 --- a/stdlib/float64.gr +++ b/stdlib/float64.gr @@ -23,6 +23,8 @@ use Numbers.{ coerceNumberToFloat64 as fromNumber, coerceFloat64ToNumber as toNumber, } +from "runtime/math/trig" include Trig +use Trig.{ sin, cos, tan } @unsafe let _VALUE_OFFSET = 8n @@ -269,6 +271,29 @@ provide let (>=) = (x: Float64, y: Float64) => { xv >= yv } +/** + * Checks if a float is finite. + * All values are finite exept for NaN, infinity or negative infinity. + * + * @param x: The number to check + * @returns `true` if the value is finite or `false` otherwise + * + * @example Float64.isFinite(0.5d) + * @example Float64.isFinite(1.0d) + * @example Float64.isFinite(Infinityd) == false + * @example Float64.isFinite(-Infinityd) == false + * @example Float64.isFinite(NaNd) == false + * + * @since v0.7.0 + */ +@unsafe +provide let isFinite = (x: Float64) => { + // uses the fact that all finite floats minus themselves are zero + // (NaN - NaN == NaN, inf - inf == NaN, + // -inf - -inf == NaN, inf - -inf == inf, -inf - inf == -inf) + x - x == 0.0d +} + /** * Checks if the value is a float NaN value (Not A Number). * @@ -485,3 +510,86 @@ provide let copySign = (x: Float64, y: Float64) => { let ptr = newFloat64(WasmF64.copySign(xv, yv)) WasmI32.toGrain(ptr): Float64 } + +/** + * Determines whether two values are considered close to each other using a relative and absolute tolerance. + * + * @param a: The first value + * @param b: The second value + * @param relativeTolerance: The maximum tolerance to use relative to the larger absolute value `a` or `b` + * @param absoluteTolerance: The absolute tolerance to use, regardless of the values of `a` or `b` + * @returns `true` if the values are considered close to each other or `false` otherwise + * + * @example Float64.isClose(1.233d, 1.233d) + * @example Float64.isClose(1.233d, 1.233000001d) + * @example Float64.isClose(8.005d, 8.450d, absoluteTolerance=0.5d) + * @example Float64.isClose(4.0d, 4.1d, relativeTolerance=0.025d) + * @example Float64.isClose(1.233d, 1.24d) == false + * @example Float64.isClose(1.233d, 1.4566d) == false + * @example Float64.isClose(8.005d, 8.450d, absoluteTolerance=0.4d) == false + * @example Float64.isClose(4.0d, 4.1d, relativeTolerance=0.024d) == false + * + * @since v0.7.0 + */ +provide let isClose = (a, b, relativeTolerance=1e-9d, absoluteTolerance=0.0d) => { + if (a == b) { + true + } else if (isFinite(a) && isFinite(b)) { + abs(a - b) <= + max(relativeTolerance * max(abs(a), abs(b)), absoluteTolerance) + } else { + // NaN and infinities which were not equal + false + } +} + +/** + * Computes the sine of a float (in radians). + * + * @param radians: The input in radians + * @returns The computed sine + * + * @example Float64.sin(0.0d) == 0.0d + * + * @since v0.7.0 + */ +@unsafe +provide let sin = (radians: Float64) => { + let xval = WasmF64.load(WasmI32.fromGrain(radians), _VALUE_OFFSET) + let value = sin(xval) + WasmI32.toGrain(newFloat64(value)): Float64 +} + +/** + * Computes the cosine of a float (in radians). + * + * @param radians: The input in radians + * @returns The computed cosine + * + * @example Float64.cos(0.0d) == 1.0d + * + * @since v0.7.0 + */ +@unsafe +provide let cos = (radians: Float64) => { + let xval = WasmF64.load(WasmI32.fromGrain(radians), _VALUE_OFFSET) + let value = cos(xval) + WasmI32.toGrain(newFloat64(value)): Float64 +} + +/** + * Computes the tangent of a number (in radians). + * + * @param radians: The input in radians + * @returns The computed tangent + * + * @example Float64.tan(0.0d) == 0.0d + * + * @since v0.7.0 + */ +@unsafe +provide let tan = (radians: Float64) => { + let xval = WasmF64.load(WasmI32.fromGrain(radians), _VALUE_OFFSET) + let value = tan(xval) + WasmI32.toGrain(newFloat64(value)): Float64 +} diff --git a/stdlib/float64.md b/stdlib/float64.md index d33bce1e9..3274d524b 100644 --- a/stdlib/float64.md +++ b/stdlib/float64.md @@ -513,6 +513,54 @@ use Float64.{ (>=) } assert -1.0d >= -1.0d ``` +### Float64.**isFinite** + +
+Added in next +No other changes yet. +
+ +```grain +isFinite : (x: Float64) => Bool +``` + +Checks if a float is finite. +All values are finite exept for NaN, infinity or negative infinity. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`x`|`Float64`|The number to check| + +Returns: + +|type|description| +|----|-----------| +|`Bool`|`true` if the value is finite or `false` otherwise| + +Examples: + +```grain +Float64.isFinite(0.5d) +``` + +```grain +Float64.isFinite(1.0d) +``` + +```grain +Float64.isFinite(Infinityd) == false +``` + +```grain +Float64.isFinite(-Infinityd) == false +``` + +```grain +Float64.isFinite(NaNd) == false +``` + ### Float64.**isNaN**
@@ -956,3 +1004,160 @@ Float64.copySign(3.0d, -1.0d) == -3.0d Float64.copySign(-5.0d, 1.0d) == 5.0d ``` +### Float64.**isClose** + +
+Added in next +No other changes yet. +
+ +```grain +isClose : + (a: Float64, b: Float64, ?relativeTolerance: Float64, + ?absoluteTolerance: Float64) => Bool +``` + +Determines whether two values are considered close to each other using a relative and absolute tolerance. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`a`|`Float64`|The first value| +|`b`|`Float64`|The second value| +|`?relativeTolerance`|`Float64`|The maximum tolerance to use relative to the larger absolute value `a` or `b`| +|`?absoluteTolerance`|`Float64`|The absolute tolerance to use, regardless of the values of `a` or `b`| + +Returns: + +|type|description| +|----|-----------| +|`Bool`|`true` if the values are considered close to each other or `false` otherwise| + +Examples: + +```grain +Float64.isClose(1.233d, 1.233d) +``` + +```grain +Float64.isClose(1.233d, 1.233000001d) +``` + +```grain +Float64.isClose(8.005d, 8.450d, absoluteTolerance=0.5d) +``` + +```grain +Float64.isClose(4.0d, 4.1d, relativeTolerance=0.025d) +``` + +```grain +Float64.isClose(1.233d, 1.24d) == false +``` + +```grain +Float64.isClose(1.233d, 1.4566d) == false +``` + +```grain +Float64.isClose(8.005d, 8.450d, absoluteTolerance=0.4d) == false +``` + +```grain +Float64.isClose(4.0d, 4.1d, relativeTolerance=0.024d) == false +``` + +### Float64.**sin** + +
+Added in next +No other changes yet. +
+ +```grain +sin : (radians: Float64) => Float64 +``` + +Computes the sine of a float (in radians). + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`radians`|`Float64`|The input in radians| + +Returns: + +|type|description| +|----|-----------| +|`Float64`|The computed sine| + +Examples: + +```grain +Float64.sin(0.0d) == 0.0d +``` + +### Float64.**cos** + +
+Added in next +No other changes yet. +
+ +```grain +cos : (radians: Float64) => Float64 +``` + +Computes the cosine of a float (in radians). + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`radians`|`Float64`|The input in radians| + +Returns: + +|type|description| +|----|-----------| +|`Float64`|The computed cosine| + +Examples: + +```grain +Float64.cos(0.0d) == 1.0d +``` + +### Float64.**tan** + +
+Added in next +No other changes yet. +
+ +```grain +tan : (radians: Float64) => Float64 +``` + +Computes the tangent of a number (in radians). + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`radians`|`Float64`|The input in radians| + +Returns: + +|type|description| +|----|-----------| +|`Float64`|The computed tangent| + +Examples: + +```grain +Float64.tan(0.0d) == 0.0d +``` +