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
+```
+