summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/unittests
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/unittests')
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/README.txt1
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/async_expectations.spec.ts168
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/basic.spec.ts35
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/check_contents.spec.ts71
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/conversion.spec.ts640
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/floating_point.spec.ts8238
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/getStackTrace.spec.ts138
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/listing.ts5
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/loaders_and_trees.spec.ts978
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/logger.spec.ts173
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/maths.spec.ts1924
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/params_builder_and_utils.spec.ts549
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/params_builder_toplevel.spec.ts112
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/preprocessor.spec.ts207
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/prng.spec.ts74
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/query_compare.spec.ts144
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/query_string.spec.ts268
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/serialization.spec.ts413
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/test_group.spec.ts437
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/test_group_test.ts34
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/test_query.spec.ts143
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/texture_ok.spec.ts161
-rw-r--r--dom/webgpu/tests/cts/checkout/src/unittests/unit_test.ts3
23 files changed, 14916 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/README.txt b/dom/webgpu/tests/cts/checkout/src/unittests/README.txt
new file mode 100644
index 0000000000..17272c3919
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/README.txt
@@ -0,0 +1 @@
+Unit tests for CTS framework.
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/async_expectations.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/async_expectations.spec.ts
new file mode 100644
index 0000000000..2d62978b8f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/async_expectations.spec.ts
@@ -0,0 +1,168 @@
+/* eslint-disable @typescript-eslint/require-await */
+export const description = `
+Tests for eventualAsyncExpectation and immediateAsyncExpectation.
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { makeTestGroupForUnitTesting } from '../common/internal/test_group.js';
+import { assert, objectEquals, rejectOnTimeout, resolveOnTimeout } from '../common/util/util.js';
+
+import { TestGroupTest } from './test_group_test.js';
+import { UnitTest } from './unit_test.js';
+
+class FixtureToTest extends UnitTest {
+ public override immediateAsyncExpectation<T>(fn: () => Promise<T>): Promise<T> {
+ return super.immediateAsyncExpectation(fn);
+ }
+ public override eventualAsyncExpectation<T>(fn: (niceStack: Error) => Promise<T>): void {
+ super.eventualAsyncExpectation(fn);
+ }
+}
+
+export const g = makeTestGroup(TestGroupTest);
+
+g.test('eventual').fn(async t0 => {
+ const g = makeTestGroupForUnitTesting(FixtureToTest);
+
+ const runState = [0, 0, 0, 0];
+ let runStateIndex = 0;
+
+ // Should pass in state 3
+ g.test('noawait,resolve').fn(t => {
+ const idx = runStateIndex++;
+
+ runState[idx] = 1;
+ t.eventualAsyncExpectation(async () => {
+ runState[idx] = 2;
+ await resolveOnTimeout(50);
+ runState[idx] = 3;
+ });
+ runState[idx] = 4;
+ });
+
+ // Should fail in state 4
+ g.test('noawait,reject').fn(t => {
+ const idx = runStateIndex++;
+
+ runState[idx] = 1;
+ t.eventualAsyncExpectation(async () => {
+ runState[idx] = 2;
+ await rejectOnTimeout(50, 'rejected 1');
+ runState[idx] = 3;
+ });
+ runState[idx] = 4;
+ });
+
+ // Should fail in state 3
+ g.test('nested,2').fn(t => {
+ const idx = runStateIndex++;
+
+ runState[idx] = 1;
+ t.eventualAsyncExpectation(async () => {
+ runState[idx] = 2;
+ await resolveOnTimeout(50); // Wait a bit before adding a new eventualAsyncExpectation
+ t.eventualAsyncExpectation(() => rejectOnTimeout(100, 'inner rejected 1'));
+ runState[idx] = 3;
+ });
+ runState[idx] = 4;
+ });
+
+ // Should fail in state 3
+ g.test('nested,4').fn(t => {
+ const idx = runStateIndex++;
+
+ runState[idx] = 1;
+ t.eventualAsyncExpectation(async () => {
+ t.eventualAsyncExpectation(async () => {
+ t.eventualAsyncExpectation(async () => {
+ runState[idx] = 2;
+ await resolveOnTimeout(50); // Wait a bit before adding a new eventualAsyncExpectation
+ t.eventualAsyncExpectation(() => rejectOnTimeout(100, 'inner rejected 2'));
+ runState[idx] = 3;
+ });
+ });
+ });
+ runState[idx] = 4;
+ });
+
+ const resultsPromise = t0.run(g);
+ assert(objectEquals(runState, [0, 0, 0, 0]));
+
+ const statuses = Array.from(await resultsPromise).map(([, v]) => v.status);
+ assert(objectEquals(runState, [3, 4, 3, 3]), () => runState.toString());
+ assert(objectEquals(statuses, ['pass', 'fail', 'fail', 'fail']), () => statuses.toString());
+});
+
+g.test('immediate').fn(async t0 => {
+ const g = makeTestGroupForUnitTesting(FixtureToTest);
+
+ const runState = [0, 0, 0, 0, 0];
+
+ g.test('noawait,resolve').fn(t => {
+ runState[0] = 1;
+ void t.immediateAsyncExpectation(async () => {
+ runState[0] = 2;
+ await resolveOnTimeout(50);
+ runState[0] = 3;
+ });
+ runState[0] = 4;
+ });
+
+ // (Can't g.test('noawait,reject') because it causes a top-level Promise
+ // rejection which crashes Node.)
+
+ g.test('await,resolve').fn(async t => {
+ runState[1] = 1;
+ await t.immediateAsyncExpectation(async () => {
+ runState[1] = 2;
+ await resolveOnTimeout(50);
+ runState[1] = 3;
+ });
+ });
+
+ g.test('await,reject').fn(async t => {
+ runState[2] = 1;
+ await t.immediateAsyncExpectation(async () => {
+ runState[2] = 2;
+ await rejectOnTimeout(50, 'rejected 3');
+ runState[2] = 3;
+ });
+ });
+
+ // (Similarly can't test 'nested,noawait'.)
+
+ g.test('nested,await,2').fn(t => {
+ runState[3] = 1;
+ t.eventualAsyncExpectation(async () => {
+ runState[3] = 2;
+ await resolveOnTimeout(50); // Wait a bit before adding a new immediateAsyncExpectation
+ runState[3] = 3;
+ await t.immediateAsyncExpectation(() => rejectOnTimeout(100, 'inner rejected 3'));
+ runState[3] = 5;
+ });
+ runState[3] = 4;
+ });
+
+ g.test('nested,await,4').fn(t => {
+ runState[4] = 1;
+ t.eventualAsyncExpectation(async () => {
+ t.eventualAsyncExpectation(async () => {
+ t.eventualAsyncExpectation(async () => {
+ runState[4] = 2;
+ await resolveOnTimeout(50); // Wait a bit before adding a new immediateAsyncExpectation
+ runState[4] = 3;
+ await t.immediateAsyncExpectation(() => rejectOnTimeout(100, 'inner rejected 3'));
+ runState[4] = 5;
+ });
+ });
+ });
+ runState[4] = 4;
+ });
+
+ const resultsPromise = t0.run(g);
+ assert(objectEquals(runState, [0, 0, 0, 0, 0]));
+
+ const statuses = Array.from(await resultsPromise).map(([, v]) => v.status);
+ assert(objectEquals(runState, [3, 3, 2, 3, 3]));
+ assert(objectEquals(statuses, ['fail', 'pass', 'fail', 'fail', 'fail']));
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/basic.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/basic.spec.ts
new file mode 100644
index 0000000000..5c04067396
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/basic.spec.ts
@@ -0,0 +1,35 @@
+export const description = `
+Basic unit tests for test framework.
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+g.test('test,sync').fn(_t => {});
+
+g.test('test,async').fn(async _t => {});
+
+g.test('test_with_params,sync')
+ .paramsSimple([{}])
+ .fn(t => {
+ t.debug(JSON.stringify(t.params));
+ });
+
+g.test('test_with_params,async')
+ .paramsSimple([{}])
+ .fn(t => {
+ t.debug(JSON.stringify(t.params));
+ });
+
+g.test('test_with_params,private_params')
+ .paramsSimple([
+ { a: 1, b: 2, _result: 3 }, //
+ { a: 4, b: -3, _result: 1 },
+ ])
+ .fn(t => {
+ const { a, b, _result } = t.params;
+ t.expect(a + b === _result);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/check_contents.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/check_contents.spec.ts
new file mode 100644
index 0000000000..1a722a1b86
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/check_contents.spec.ts
@@ -0,0 +1,71 @@
+export const description = `Unit tests for check_contents`;
+
+import { Fixture } from '../common/framework/fixture.js';
+import { makeTestGroup } from '../common/internal/test_group.js';
+import { ErrorWithExtra } from '../common/util/util.js';
+import { checkElementsEqual } from '../webgpu/util/check_contents.js';
+
+class F extends Fixture {
+ test(substr: undefined | string, result: undefined | ErrorWithExtra) {
+ if (substr === undefined) {
+ this.expect(result === undefined, result?.message);
+ } else {
+ this.expect(result !== undefined && result.message.indexOf(substr) !== -1, result?.message);
+ }
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('checkElementsEqual').fn(t => {
+ t.shouldThrow('Error', () => checkElementsEqual(new Uint8Array(), new Uint16Array()));
+ t.shouldThrow('Error', () => checkElementsEqual(new Uint32Array(), new Float32Array()));
+ t.shouldThrow('Error', () => checkElementsEqual(new Uint8Array([]), new Uint8Array([0])));
+ t.shouldThrow('Error', () => checkElementsEqual(new Uint8Array([0]), new Uint8Array([])));
+ {
+ t.test(undefined, checkElementsEqual(new Uint8Array([]), new Uint8Array([])));
+ t.test(undefined, checkElementsEqual(new Uint8Array([0]), new Uint8Array([0])));
+ t.test(undefined, checkElementsEqual(new Uint8Array([1]), new Uint8Array([1])));
+ t.test(
+ `
+ Starting at index 0:
+ actual == 0x: 00
+ failed -> xx
+ expected == 01`,
+ checkElementsEqual(new Uint8Array([0]), new Uint8Array([1]))
+ );
+ t.test(
+ 'expected == 01 02 01',
+ checkElementsEqual(new Uint8Array([1, 1, 1]), new Uint8Array([1, 2, 1]))
+ );
+ }
+ {
+ const actual = new Uint8Array(280);
+ const exp = new Uint8Array(280);
+ for (let i = 2; i < 20; ++i) actual[i] = i - 4;
+ t.test(
+ '00 fe ff 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00',
+ checkElementsEqual(actual, exp)
+ );
+ for (let i = 2; i < 280; ++i) actual[i] = i - 4;
+ t.test('Starting at index 1:', checkElementsEqual(actual, exp));
+ for (let i = 0; i < 2; ++i) actual[i] = i - 4;
+ t.test('Starting at index 0:', checkElementsEqual(actual, exp));
+ }
+ {
+ const actual = new Int32Array(30);
+ const exp = new Int32Array(30);
+ for (let i = 2; i < 7; ++i) actual[i] = i - 3;
+ t.test('00000002 00000003 00000000\n', checkElementsEqual(actual, exp));
+ for (let i = 2; i < 30; ++i) actual[i] = i - 3;
+ t.test('00000000 00000000 ...', checkElementsEqual(actual, exp));
+ }
+ {
+ const actual = new Float64Array(30);
+ const exp = new Float64Array(30);
+ for (let i = 2; i < 7; ++i) actual[i] = (i - 4) * 1e100;
+ t.test('2.000e+100 0.000\n', checkElementsEqual(actual, exp));
+ for (let i = 2; i < 280; ++i) actual[i] = (i - 4) * 1e100;
+ t.test('6.000e+100 7.000e+100 ...', checkElementsEqual(actual, exp));
+ }
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/conversion.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/conversion.spec.ts
new file mode 100644
index 0000000000..8606aa8717
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/conversion.spec.ts
@@ -0,0 +1,640 @@
+export const description = `Unit tests for conversion`;
+
+import { mergeParams } from '../common/internal/params_utils.js';
+import { makeTestGroup } from '../common/internal/test_group.js';
+import { keysOf } from '../common/util/data_tables.js';
+import { assert, objectEquals } from '../common/util/util.js';
+import { kValue } from '../webgpu/util/constants.js';
+import {
+ bool,
+ f16Bits,
+ f32,
+ f32Bits,
+ float16BitsToFloat32,
+ float32ToFloat16Bits,
+ float32ToFloatBits,
+ floatBitsToNormalULPFromZero,
+ floatBitsToNumber,
+ i32,
+ kFloat16Format,
+ kFloat32Format,
+ Matrix,
+ numbersApproximatelyEqual,
+ pack2x16float,
+ pack2x16snorm,
+ pack2x16unorm,
+ pack4x8snorm,
+ pack4x8unorm,
+ packRGB9E5UFloat,
+ Scalar,
+ toMatrix,
+ u32,
+ unpackRGB9E5UFloat,
+ vec2,
+ vec3,
+ vec4,
+ Vector,
+} from '../webgpu/util/conversion.js';
+
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+const kFloat16BitsToNumberCases = [
+ [0b0_01111_0000000000, 1],
+ [0b0_00001_0000000000, 0.00006103515625],
+ [0b0_01101_0101010101, 0.33325195],
+ [0b0_11110_1111111111, 65504],
+ [0b0_00000_0000000000, 0],
+ [0b1_00000_0000000000, -0.0], // -0.0 compares as equal to 0.0
+ [0b0_01110_0000000000, 0.5],
+ [0b0_01100_1001100110, 0.1999512],
+ [0b0_01111_0000000001, 1.00097656],
+ [0b0_10101_1001000000, 100],
+ [0b1_01100_1001100110, -0.1999512],
+ [0b1_10101_1001000000, -100],
+ [0b0_11111_1111111111, Number.NaN],
+ [0b0_11111_0000000000, Number.POSITIVE_INFINITY],
+ [0b1_11111_0000000000, Number.NEGATIVE_INFINITY],
+];
+
+g.test('float16BitsToFloat32').fn(t => {
+ for (const [bits, number] of [
+ ...kFloat16BitsToNumberCases,
+ [0b0_00000_1111111111, 0.00006104], // subnormal f16 input
+ [0b1_00000_1111111111, -0.00006104],
+ ]) {
+ const actual = float16BitsToFloat32(bits);
+ t.expect(
+ // some loose check
+ numbersApproximatelyEqual(actual, number, 0.00001),
+ `for ${bits.toString(2)}, expected ${number}, got ${actual}`
+ );
+ }
+});
+
+g.test('float32ToFloat16Bits').fn(t => {
+ for (const [bits, number] of [
+ ...kFloat16BitsToNumberCases,
+ [0b0_00000_0000000000, 0.00001], // input that becomes subnormal in f16 is rounded to 0
+ [0b1_00000_0000000000, -0.00001], // and sign is preserved
+ ]) {
+ // some loose check
+ const actual = float32ToFloat16Bits(number);
+ t.expect(
+ Math.abs(actual - bits) <= 1,
+ `for ${number}, expected ${bits.toString(2)}, got ${actual.toString(2)}`
+ );
+ }
+});
+
+g.test('float32ToFloatBits_floatBitsToNumber')
+ .paramsSubcasesOnly(u =>
+ u
+ .combine('signed', [0, 1] as const)
+ .combine('exponentBits', [5, 8])
+ .combine('mantissaBits', [10, 23])
+ )
+ .fn(t => {
+ const { signed, exponentBits, mantissaBits } = t.params;
+ const bias = (1 << (exponentBits - 1)) - 1;
+
+ for (const [, value] of kFloat16BitsToNumberCases) {
+ if (value < 0 && signed === 0) continue;
+ const bits = float32ToFloatBits(value, signed, exponentBits, mantissaBits, bias);
+ const reconstituted = floatBitsToNumber(bits, { signed, exponentBits, mantissaBits, bias });
+ t.expect(
+ numbersApproximatelyEqual(reconstituted, value, 0.0000001),
+ `${reconstituted} vs ${value}`
+ );
+ }
+ });
+
+g.test('floatBitsToULPFromZero,16').fn(t => {
+ const test = (bits: number, ulpFromZero: number) =>
+ t.expect(floatBitsToNormalULPFromZero(bits, kFloat16Format) === ulpFromZero, bits.toString(2));
+ // Zero
+ test(0b0_00000_0000000000, 0);
+ test(0b1_00000_0000000000, 0);
+ // Subnormal
+ test(0b0_00000_0000000001, 0);
+ test(0b1_00000_0000000001, 0);
+ test(0b0_00000_1111111111, 0);
+ test(0b1_00000_1111111111, 0);
+ // Normal
+ test(0b0_00001_0000000000, 1); // 0 + 1ULP
+ test(0b1_00001_0000000000, -1); // 0 - 1ULP
+ test(0b0_00001_0000000001, 2); // 0 + 2ULP
+ test(0b1_00001_0000000001, -2); // 0 - 2ULP
+ test(0b0_01110_0000000000, 0b01101_0000000001); // 0.5
+ test(0b1_01110_0000000000, -0b01101_0000000001); // -0.5
+ test(0b0_01110_1111111110, 0b01101_1111111111); // 1.0 - 2ULP
+ test(0b1_01110_1111111110, -0b01101_1111111111); // -(1.0 - 2ULP)
+ test(0b0_01110_1111111111, 0b01110_0000000000); // 1.0 - 1ULP
+ test(0b1_01110_1111111111, -0b01110_0000000000); // -(1.0 - 1ULP)
+ test(0b0_01111_0000000000, 0b01110_0000000001); // 1.0
+ test(0b1_01111_0000000000, -0b01110_0000000001); // -1.0
+ test(0b0_01111_0000000001, 0b01110_0000000010); // 1.0 + 1ULP
+ test(0b1_01111_0000000001, -0b01110_0000000010); // -(1.0 + 1ULP)
+ test(0b0_10000_0000000000, 0b01111_0000000001); // 2.0
+ test(0b1_10000_0000000000, -0b01111_0000000001); // -2.0
+
+ const testThrows = (b: number) =>
+ t.shouldThrow('Error', () => floatBitsToNormalULPFromZero(b, kFloat16Format));
+ // Infinity
+ testThrows(0b0_11111_0000000000);
+ testThrows(0b1_11111_0000000000);
+ // NaN
+ testThrows(0b0_11111_1111111111);
+ testThrows(0b1_11111_1111111111);
+});
+
+g.test('floatBitsToULPFromZero,32').fn(t => {
+ const test = (bits: number, ulpFromZero: number) =>
+ t.expect(floatBitsToNormalULPFromZero(bits, kFloat32Format) === ulpFromZero, bits.toString(2));
+ // Zero
+ test(0b0_00000000_00000000000000000000000, 0);
+ test(0b1_00000000_00000000000000000000000, 0);
+ // Subnormal
+ test(0b0_00000000_00000000000000000000001, 0);
+ test(0b1_00000000_00000000000000000000001, 0);
+ test(0b0_00000000_11111111111111111111111, 0);
+ test(0b1_00000000_11111111111111111111111, 0);
+ // Normal
+ test(0b0_00000001_00000000000000000000000, 1); // 0 + 1ULP
+ test(0b1_00000001_00000000000000000000000, -1); // 0 - 1ULP
+ test(0b0_00000001_00000000000000000000001, 2); // 0 + 2ULP
+ test(0b1_00000001_00000000000000000000001, -2); // 0 - 2ULP
+ test(0b0_01111110_00000000000000000000000, 0b01111101_00000000000000000000001); // 0.5
+ test(0b1_01111110_00000000000000000000000, -0b01111101_00000000000000000000001); // -0.5
+ test(0b0_01111110_11111111111111111111110, 0b01111101_11111111111111111111111); // 1.0 - 2ULP
+ test(0b1_01111110_11111111111111111111110, -0b01111101_11111111111111111111111); // -(1.0 - 2ULP)
+ test(0b0_01111110_11111111111111111111111, 0b01111110_00000000000000000000000); // 1.0 - 1ULP
+ test(0b1_01111110_11111111111111111111111, -0b01111110_00000000000000000000000); // -(1.0 - 1ULP)
+ test(0b0_01111111_00000000000000000000000, 0b01111110_00000000000000000000001); // 1.0
+ test(0b1_01111111_00000000000000000000000, -0b01111110_00000000000000000000001); // -1.0
+ test(0b0_01111111_00000000000000000000001, 0b01111110_00000000000000000000010); // 1.0 + 1ULP
+ test(0b1_01111111_00000000000000000000001, -0b01111110_00000000000000000000010); // -(1.0 + 1ULP)
+ test(0b0_11110000_00000000000000000000000, 0b11101111_00000000000000000000001); // 2.0
+ test(0b1_11110000_00000000000000000000000, -0b11101111_00000000000000000000001); // -2.0
+
+ const testThrows = (b: number) =>
+ t.shouldThrow('Error', () => floatBitsToNormalULPFromZero(b, kFloat32Format));
+ // Infinity
+ testThrows(0b0_11111111_00000000000000000000000);
+ testThrows(0b1_11111111_00000000000000000000000);
+ // NaN
+ testThrows(0b0_11111111_11111111111111111111111);
+ testThrows(0b0_11111111_00000000000000000000001);
+ testThrows(0b1_11111111_11111111111111111111111);
+ testThrows(0b1_11111111_00000000000000000000001);
+});
+
+g.test('scalarWGSL').fn(t => {
+ const cases: Array<[Scalar, string]> = [
+ [f32(0.0), '0.0f'],
+ // The number -0.0 can be remapped to 0.0 when stored in a Scalar
+ // object. It is not possible to guarantee that '-0.0f' will
+ // be emitted. So the WGSL scalar value printing does not try
+ // to handle this case.
+ [f32(-0.0), '0.0f'], // -0.0 can be remapped to 0.0
+ [f32(1.0), '1.0f'],
+ [f32(-1.0), '-1.0f'],
+ [f32Bits(0x70000000), '1.5845632502852868e+29f'],
+ [f32Bits(0xf0000000), '-1.5845632502852868e+29f'],
+ [f16Bits(0), '0.0h'],
+ [f16Bits(0x3c00), '1.0h'],
+ [f16Bits(0xbc00), '-1.0h'],
+ [u32(0), '0u'],
+ [u32(1), '1u'],
+ [u32(2000000000), '2000000000u'],
+ [u32(-1), '4294967295u'],
+ [i32(0), 'i32(0)'],
+ [i32(1), 'i32(1)'],
+ [i32(-1), 'i32(-1)'],
+ [bool(true), 'true'],
+ [bool(false), 'false'],
+ ];
+ for (const [value, expect] of cases) {
+ const got = value.wgsl();
+ t.expect(
+ got === expect,
+ `[value: ${value.value}, type: ${value.type}]
+got: ${got}
+expect: ${expect}`
+ );
+ }
+});
+
+g.test('vectorWGSL').fn(t => {
+ const cases: Array<[Vector, string]> = [
+ [vec2(f32(42.0), f32(24.0)), 'vec2(42.0f, 24.0f)'],
+ [vec2(f16Bits(0x5140), f16Bits(0x4e00)), 'vec2(42.0h, 24.0h)'],
+ [vec2(u32(42), u32(24)), 'vec2(42u, 24u)'],
+ [vec2(i32(42), i32(24)), 'vec2(i32(42), i32(24))'],
+ [vec2(bool(false), bool(true)), 'vec2(false, true)'],
+
+ [vec3(f32(0.0), f32(1.0), f32(-1.0)), 'vec3(0.0f, 1.0f, -1.0f)'],
+ [vec3(f16Bits(0), f16Bits(0x3c00), f16Bits(0xbc00)), 'vec3(0.0h, 1.0h, -1.0h)'],
+ [vec3(u32(0), u32(1), u32(-1)), 'vec3(0u, 1u, 4294967295u)'],
+ [vec3(i32(0), i32(1), i32(-1)), 'vec3(i32(0), i32(1), i32(-1))'],
+ [vec3(bool(true), bool(false), bool(true)), 'vec3(true, false, true)'],
+
+ [vec4(f32(1.0), f32(-2.0), f32(4.0), f32(-8.0)), 'vec4(1.0f, -2.0f, 4.0f, -8.0f)'],
+ [
+ vec4(f16Bits(0xbc00), f16Bits(0x4000), f16Bits(0xc400), f16Bits(0x4800)),
+ 'vec4(-1.0h, 2.0h, -4.0h, 8.0h)',
+ ],
+ [vec4(u32(1), u32(-2), u32(4), u32(-8)), 'vec4(1u, 4294967294u, 4u, 4294967288u)'],
+ [vec4(i32(1), i32(-2), i32(4), i32(-8)), 'vec4(i32(1), i32(-2), i32(4), i32(-8))'],
+ [vec4(bool(false), bool(true), bool(true), bool(false)), 'vec4(false, true, true, false)'],
+ ];
+ for (const [value, expect] of cases) {
+ const got = value.wgsl();
+ t.expect(
+ got === expect,
+ `[values: ${value.elements}, type: ${value.type}]
+got: ${got}
+expect: ${expect}`
+ );
+ }
+});
+
+g.test('matrixWGSL').fn(t => {
+ const cases: Array<[Matrix, string]> = [
+ [
+ toMatrix(
+ [
+ [0.0, 1.0],
+ [2.0, 3.0],
+ ],
+ f32
+ ),
+ 'mat2x2(0.0f, 1.0f, 2.0f, 3.0f)',
+ ],
+ [
+ toMatrix(
+ [
+ [0.0, 1.0, 2.0],
+ [3.0, 4.0, 5.0],
+ ],
+ f32
+ ),
+ 'mat2x3(0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f)',
+ ],
+ [
+ toMatrix(
+ [
+ [0.0, 1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0, 7.0],
+ ],
+ f32
+ ),
+ 'mat2x4(0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f)',
+ ],
+ [
+ toMatrix(
+ [
+ [0.0, 1.0],
+ [2.0, 3.0],
+ [4.0, 5.0],
+ ],
+ f32
+ ),
+ 'mat3x2(0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f)',
+ ],
+ [
+ toMatrix(
+ [
+ [0.0, 1.0, 2.0],
+ [3.0, 4.0, 5.0],
+ [6.0, 7.0, 8.0],
+ ],
+ f32
+ ),
+ 'mat3x3(0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f)',
+ ],
+ [
+ toMatrix(
+ [
+ [0.0, 1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0, 7.0],
+ [8.0, 9.0, 10.0, 11.0],
+ ],
+ f32
+ ),
+ 'mat3x4(0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f)',
+ ],
+ [
+ toMatrix(
+ [
+ [0.0, 1.0],
+ [2.0, 3.0],
+ [4.0, 5.0],
+ [6.0, 7.0],
+ ],
+ f32
+ ),
+ 'mat4x2(0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f)',
+ ],
+ [
+ toMatrix(
+ [
+ [0.0, 1.0, 2.0],
+ [3.0, 4.0, 5.0],
+ [6.0, 7.0, 8.0],
+ [9.0, 10.0, 11.0],
+ ],
+ f32
+ ),
+ 'mat4x3(0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f)',
+ ],
+ [
+ toMatrix(
+ [
+ [0.0, 1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0, 7.0],
+ [8.0, 9.0, 10.0, 11.0],
+ [12.0, 13.0, 14.0, 15.0],
+ ],
+ f32
+ ),
+ 'mat4x4(0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f)',
+ ],
+ ];
+ for (const [value, expect] of cases) {
+ const got = value.wgsl();
+ t.expect(
+ got === expect,
+ `[values: ${value.elements}, type: ${value.type}]
+got: ${got}
+expect: ${expect}`
+ );
+ }
+});
+
+g.test('constructorMatrix')
+ .params(u =>
+ u
+ .combine('cols', [2, 3, 4] as const)
+ .combine('rows', [2, 3, 4] as const)
+ .combine('type', ['f32'] as const)
+ )
+ .fn(t => {
+ const cols = t.params.cols;
+ const rows = t.params.rows;
+ const type = t.params.type;
+ const scalar_builder = type === 'f32' ? f32 : undefined;
+ assert(scalar_builder !== undefined, `Unexpected type param '${type}' provided`);
+
+ const elements = [...Array(cols).keys()].map(c => {
+ return [...Array(rows).keys()].map(r => scalar_builder(c * cols + r));
+ });
+
+ const got = new Matrix(elements);
+ const got_type = got.type;
+ t.expect(
+ got_type.cols === cols,
+ `expected Matrix to have ${cols} columns, received ${got_type.cols} instead`
+ );
+ t.expect(
+ got_type.rows === rows,
+ `expected Matrix to have ${rows} columns, received ${got_type.rows} instead`
+ );
+ t.expect(
+ got_type.elementType.kind === type,
+ `expected Matrix to have ${type} elements, received ${got_type.elementType.kind} instead`
+ );
+ t.expect(
+ objectEquals(got.elements, elements),
+ `Matrix did not have expected elements (${JSON.stringify(elements)}), instead had (${
+ got.elements
+ })`
+ );
+ });
+
+g.test('pack2x16float')
+ .paramsSimple([
+ // f16 normals
+ { inputs: [0, 0], result: [0x00000000, 0x80000000, 0x00008000, 0x80008000] },
+ { inputs: [1, 0], result: [0x00003c00, 0x80003c00] },
+ { inputs: [1, 1], result: [0x3c003c00] },
+ { inputs: [-1, -1], result: [0xbc00bc00] },
+ { inputs: [10, 1], result: [0x3c004900] },
+ { inputs: [-10, 1], result: [0x3c00c900] },
+
+ // f32 normal, but not f16 precise
+ { inputs: [1.00000011920928955078125, 1], result: [0x3c003c00, 0x3c003c01] },
+
+ // f32 subnormals
+ // prettier-ignore
+ { inputs: [kValue.f32.positive.subnormal.max, 1], result: [0x3c000000, 0x3c008000, 0x3c000001] },
+ // prettier-ignore
+ { inputs: [kValue.f32.negative.subnormal.min, 1], result: [0x3c008001, 0x3c000000, 0x3c008000] },
+
+ // f16 subnormals
+ // prettier-ignore
+ { inputs: [kValue.f16.positive.subnormal.max, 1], result: [0x3c0003ff, 0x3c000000, 0x3c008000] },
+ // prettier-ignore
+ { inputs: [kValue.f16.negative.subnormal.min, 1], result: [0x03c0083ff, 0x3c000000, 0x3c008000] },
+
+ // f16 out of bounds
+ { inputs: [kValue.f16.positive.max + 1, 1], result: [undefined] },
+ { inputs: [kValue.f16.negative.min - 1, 1], result: [undefined] },
+ { inputs: [1, kValue.f16.positive.max + 1], result: [undefined] },
+ { inputs: [1, kValue.f16.negative.min - 1], result: [undefined] },
+ ] as const)
+ .fn(test => {
+ const toString = (data: readonly (undefined | number)[]): String[] => {
+ return data.map(d => (d !== undefined ? u32(d).toString() : 'undefined'));
+ };
+
+ const inputs = test.params.inputs;
+ const got = pack2x16float(inputs[0], inputs[1]);
+ const expect = test.params.result;
+
+ const got_str = toString(got);
+ const expect_str = toString(expect);
+
+ // Using strings of the outputs, so they can be easily sorted, since order of the results doesn't matter.
+ test.expect(
+ objectEquals(got_str.sort(), expect_str.sort()),
+ `pack2x16float(${inputs}) returned [${got_str}]. Expected [${expect_str}]`
+ );
+ });
+
+g.test('pack2x16snorm')
+ .paramsSimple([
+ // Normals
+ { inputs: [0, 0], result: 0x00000000 },
+ { inputs: [1, 0], result: 0x00007fff },
+ { inputs: [0, 1], result: 0x7fff0000 },
+ { inputs: [1, 1], result: 0x7fff7fff },
+ { inputs: [-1, -1], result: 0x80018001 },
+ { inputs: [10, 10], result: 0x7fff7fff },
+ { inputs: [-10, -10], result: 0x80018001 },
+ { inputs: [0.1, 0.1], result: 0x0ccd0ccd },
+ { inputs: [-0.1, -0.1], result: 0xf333f333 },
+ { inputs: [0.5, 0.5], result: 0x40004000 },
+ { inputs: [-0.5, -0.5], result: 0xc001c001 },
+ { inputs: [0.1, 0.5], result: 0x40000ccd },
+ { inputs: [-0.1, -0.5], result: 0xc001f333 },
+
+ // Subnormals
+ { inputs: [kValue.f32.positive.subnormal.max, 1], result: 0x7fff0000 },
+ { inputs: [kValue.f32.negative.subnormal.min, 1], result: 0x7fff0000 },
+ ] as const)
+ .fn(test => {
+ const inputs = test.params.inputs;
+ const got = pack2x16snorm(inputs[0], inputs[1]);
+ const expect = test.params.result;
+
+ test.expect(got === expect, `pack2x16snorm(${inputs}) returned ${got}. Expected ${expect}`);
+ });
+
+g.test('pack2x16unorm')
+ .paramsSimple([
+ // Normals
+ { inputs: [0, 0], result: 0x00000000 },
+ { inputs: [1, 0], result: 0x0000ffff },
+ { inputs: [0, 1], result: 0xffff0000 },
+ { inputs: [1, 1], result: 0xffffffff },
+ { inputs: [-1, -1], result: 0x00000000 },
+ { inputs: [0.1, 0.1], result: 0x199a199a },
+ { inputs: [0.5, 0.5], result: 0x80008000 },
+ { inputs: [0.1, 0.5], result: 0x8000199a },
+ { inputs: [10, 10], result: 0xffffffff },
+
+ // Subnormals
+ { inputs: [kValue.f32.positive.subnormal.max, 1], result: 0xffff0000 },
+ ] as const)
+ .fn(test => {
+ const inputs = test.params.inputs;
+ const got = pack2x16unorm(inputs[0], inputs[1]);
+ const expect = test.params.result;
+
+ test.expect(got === expect, `pack2x16unorm(${inputs}) returned ${got}. Expected ${expect}`);
+ });
+
+g.test('pack4x8snorm')
+ .paramsSimple([
+ // Normals
+ { inputs: [0, 0, 0, 0], result: 0x00000000 },
+ { inputs: [1, 0, 0, 0], result: 0x0000007f },
+ { inputs: [0, 1, 0, 0], result: 0x00007f00 },
+ { inputs: [0, 0, 1, 0], result: 0x007f0000 },
+ { inputs: [0, 0, 0, 1], result: 0x7f000000 },
+ { inputs: [1, 1, 1, 1], result: 0x7f7f7f7f },
+ { inputs: [10, 10, 10, 10], result: 0x7f7f7f7f },
+ { inputs: [-1, 0, 0, 0], result: 0x00000081 },
+ { inputs: [0, -1, 0, 0], result: 0x00008100 },
+ { inputs: [0, 0, -1, 0], result: 0x00810000 },
+ { inputs: [0, 0, 0, -1], result: 0x81000000 },
+ { inputs: [-1, -1, -1, -1], result: 0x81818181 },
+ { inputs: [-10, -10, -10, -10], result: 0x81818181 },
+ { inputs: [0.1, 0.1, 0.1, 0.1], result: 0x0d0d0d0d },
+ { inputs: [-0.1, -0.1, -0.1, -0.1], result: 0xf3f3f3f3 },
+ { inputs: [0.1, -0.1, 0.1, -0.1], result: 0xf30df30d },
+ { inputs: [0.5, 0.5, 0.5, 0.5], result: 0x40404040 },
+ { inputs: [-0.5, -0.5, -0.5, -0.5], result: 0xc1c1c1c1 },
+ { inputs: [-0.5, 0.5, -0.5, 0.5], result: 0x40c140c1 },
+ { inputs: [0.1, 0.5, 0.1, 0.5], result: 0x400d400d },
+ { inputs: [-0.1, -0.5, -0.1, -0.5], result: 0xc1f3c1f3 },
+
+ // Subnormals
+ { inputs: [kValue.f32.positive.subnormal.max, 1, 1, 1], result: 0x7f7f7f00 },
+ { inputs: [kValue.f32.negative.subnormal.min, 1, 1, 1], result: 0x7f7f7f00 },
+ ] as const)
+ .fn(test => {
+ const inputs = test.params.inputs;
+ const got = pack4x8snorm(inputs[0], inputs[1], inputs[2], inputs[3]);
+ const expect = test.params.result;
+
+ test.expect(got === expect, `pack4x8snorm(${inputs}) returned ${u32(got)}. Expected ${expect}`);
+ });
+
+g.test('pack4x8unorm')
+ .paramsSimple([
+ // Normals
+ { inputs: [0, 0, 0, 0], result: 0x00000000 },
+ { inputs: [1, 0, 0, 0], result: 0x000000ff },
+ { inputs: [0, 1, 0, 0], result: 0x0000ff00 },
+ { inputs: [0, 0, 1, 0], result: 0x00ff0000 },
+ { inputs: [0, 0, 0, 1], result: 0xff000000 },
+ { inputs: [1, 1, 1, 1], result: 0xffffffff },
+ { inputs: [10, 10, 10, 10], result: 0xffffffff },
+ { inputs: [-1, -1, -1, -1], result: 0x00000000 },
+ { inputs: [-10, -10, -10, -10], result: 0x00000000 },
+ { inputs: [0.1, 0.1, 0.1, 0.1], result: 0x1a1a1a1a },
+ { inputs: [0.5, 0.5, 0.5, 0.5], result: 0x80808080 },
+ { inputs: [0.1, 0.5, 0.1, 0.5], result: 0x801a801a },
+
+ // Subnormals
+ { inputs: [kValue.f32.positive.subnormal.max, 1, 1, 1], result: 0xffffff00 },
+ ] as const)
+ .fn(test => {
+ const inputs = test.params.inputs;
+ const got = pack4x8unorm(inputs[0], inputs[1], inputs[2], inputs[3]);
+ const expect = test.params.result;
+
+ test.expect(got === expect, `pack4x8unorm(${inputs}) returned ${got}. Expected ${expect}`);
+ });
+
+const kRGB9E5UFloatCommonData = {
+ zero: /* */ { encoded: 0b00000_000000000_000000000_000000000, rgb: [0, 0, 0] },
+ max: /* */ { encoded: 0b11111_111111111_111111111_111111111, rgb: [65408, 65408, 65408] },
+ r1: /* */ { encoded: 0b10000_000000000_000000000_100000000, rgb: [1, 0, 0] },
+ r2: /* */ { encoded: 0b10001_000000000_000000000_100000000, rgb: [2, 0, 0] },
+ g1: /* */ { encoded: 0b10000_000000000_100000000_000000000, rgb: [0, 1, 0] },
+ g2: /* */ { encoded: 0b10001_000000000_100000000_000000000, rgb: [0, 2, 0] },
+ b1: /* */ { encoded: 0b10000_100000000_000000000_000000000, rgb: [0, 0, 1] },
+ b2: /* */ { encoded: 0b10001_100000000_000000000_000000000, rgb: [0, 0, 2] },
+ r1_g1_b1: /* */ { encoded: 0b10000_100000000_100000000_100000000, rgb: [1, 1, 1] },
+ r1_g2_b1: /* */ { encoded: 0b10001_010000000_100000000_010000000, rgb: [1, 2, 1] },
+ r4_g8_b2: /* */ { encoded: 0b10011_001000000_100000000_010000000, rgb: [4, 8, 2] },
+ r1_g2_b3: /* */ { encoded: 0b10001_110000000_100000000_010000000, rgb: [1, 2, 3] },
+ r128_g3968_b65408: { encoded: 0b11111_111111111_000011111_000000001, rgb: [128, 3968, 65408] },
+ r128_g1984_b30016: { encoded: 0b11110_111010101_000011111_000000010, rgb: [128, 1984, 30016] },
+ r_5_g_25_b_8: /**/ { encoded: 0b10011_100000000_000001000_000010000, rgb: [0.5, 0.25, 8] },
+};
+
+const kPackRGB9E5UFloatData = mergeParams(kRGB9E5UFloatCommonData, {
+ clamp_max: /* */ { encoded: 0b11111_111111111_111111111_111111111, rgb: [1e7, 1e10, 1e50] },
+ subnormals: /* */ { encoded: 0b00000_000000000_000000000_000000000, rgb: [1e-10, 1e-20, 1e-30] },
+ r57423_g54_b3478: { encoded: 0b11111_000011011_000000000_111000001, rgb: [57423, 54, 3478] },
+ r6852_g3571_b2356: { encoded: 0b11100_010010011_011011111_110101100, rgb: [6852, 3571, 2356] },
+ r68312_g12_b8123: { encoded: 0b11111_000111111_000000000_111111111, rgb: [68312, 12, 8123] },
+ r7321_g846_b32: { encoded: 0b11100_000000010_000110101_111001010, rgb: [7321, 846, 32] },
+});
+
+function bits5_9_9_9(x: number) {
+ const s = (x >>> 0).toString(2).padStart(32, '0');
+ return `${s.slice(0, 5)}_${s.slice(5, 14)}_${s.slice(14, 23)}_${s.slice(23, 32)}`;
+}
+
+g.test('packRGB9E5UFloat')
+ .params(u => u.combine('case', keysOf(kPackRGB9E5UFloatData)))
+ .fn(test => {
+ const c = kPackRGB9E5UFloatData[test.params.case];
+ const got = packRGB9E5UFloat(c.rgb[0], c.rgb[1], c.rgb[2]);
+ const expect = c.encoded;
+
+ test.expect(
+ got === expect,
+ `packRGB9E5UFloat(${c.rgb}) returned ${bits5_9_9_9(got)}. Expected ${bits5_9_9_9(expect)}`
+ );
+ });
+
+g.test('unpackRGB9E5UFloat')
+ .params(u => u.combine('case', keysOf(kRGB9E5UFloatCommonData)))
+ .fn(test => {
+ const c = kRGB9E5UFloatCommonData[test.params.case];
+ const got = unpackRGB9E5UFloat(c.encoded);
+ const expect = c.rgb;
+
+ test.expect(
+ got.R === expect[0] && got.G === expect[1] && got.B === expect[2],
+ `unpackRGB9E5UFloat(${bits5_9_9_9(c.encoded)} ` +
+ `returned ${got.R},${got.G},${got.B}. Expected ${expect}`
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/floating_point.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/floating_point.spec.ts
new file mode 100644
index 0000000000..e8f8525d7f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/floating_point.spec.ts
@@ -0,0 +1,8238 @@
+export const description = `
+Floating Point unit tests.
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { objectEquals, unreachable } from '../common/util/util.js';
+import { kValue } from '../webgpu/util/constants.js';
+import { FP, FPInterval, FPIntervalParam, IntervalBounds } from '../webgpu/util/floating_point.js';
+import { map2DArray, oneULPF32, oneULPF16, oneULPF64 } from '../webgpu/util/math.js';
+import {
+ reinterpretU16AsF16,
+ reinterpretU32AsF32,
+ reinterpretU64AsF64,
+} from '../webgpu/util/reinterpret.js';
+
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+/**
+ * For ULP purposes, abstract float behaves like f32, so need to swizzle it in
+ * for expectations.
+ */
+const kFPTraitForULP = {
+ abstract: 'f32',
+ f32: 'f32',
+ f16: 'f16',
+} as const;
+
+/** Bounds indicating an expectation of unbounded error */
+const kUnboundedBounds: IntervalBounds = [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];
+
+/** Interval from kUnboundedBounds */
+const kUnboundedInterval = {
+ f32: FP.f32.toParam(kUnboundedBounds),
+ f16: FP.f16.toParam(kUnboundedBounds),
+ abstract: FP.abstract.toParam(kUnboundedBounds),
+};
+
+/** @returns a number N * ULP greater than the provided number */
+const kPlusNULPFunctions = {
+ f32: (x: number, n: number) => {
+ return x + n * oneULPF32(x);
+ },
+ f16: (x: number, n: number) => {
+ return x + n * oneULPF16(x);
+ },
+ abstract: (x: number, n: number) => {
+ return x + n * oneULPF64(x);
+ },
+};
+
+/** @returns a number one ULP greater than the provided number */
+const kPlusOneULPFunctions = {
+ f32: (x: number): number => {
+ return kPlusNULPFunctions['f32'](x, 1);
+ },
+ f16: (x: number): number => {
+ return kPlusNULPFunctions['f16'](x, 1);
+ },
+ abstract: (x: number): number => {
+ return kPlusNULPFunctions['abstract'](x, 1);
+ },
+};
+
+/** @returns a number N * ULP less than the provided number */
+const kMinusNULPFunctions = {
+ f32: (x: number, n: number) => {
+ return x - n * oneULPF32(x);
+ },
+ f16: (x: number, n: number) => {
+ return x - n * oneULPF16(x);
+ },
+ abstract: (x: number, n: number) => {
+ return x - n * oneULPF64(x);
+ },
+};
+
+/** @returns a number one ULP less than the provided number */
+const kMinusOneULPFunctions = {
+ f32: (x: number): number => {
+ return kMinusNULPFunctions['f32'](x, 1);
+ },
+ f16: (x: number): number => {
+ return kMinusNULPFunctions['f16'](x, 1);
+ },
+ abstract: (x: number): number => {
+ return kMinusNULPFunctions['abstract'](x, 1);
+ },
+};
+
+/** @returns the expected IntervalBounds adjusted by the given error function
+ *
+ * @param expected the bounds to be adjusted
+ * @param error error function to adjust the bounds via
+ */
+function applyError(
+ expected: number | IntervalBounds,
+ error: (n: number) => number
+): IntervalBounds {
+ // Avoiding going through FPInterval to avoid tying this to a specific kind
+ const unpack = (n: number | IntervalBounds): [number, number] => {
+ if (expected instanceof Array) {
+ switch (expected.length) {
+ case 1:
+ return [expected[0], expected[0]];
+ case 2:
+ return [expected[0], expected[1]];
+ }
+ unreachable(`Tried to unpack an IntervalBounds with length other than 1 or 2`);
+ } else {
+ // TS doesn't narrow this to number automatically
+ return [n as number, n as number];
+ }
+ };
+
+ let [begin, end] = unpack(expected);
+
+ begin -= error(begin);
+ end += error(end);
+
+ if (begin === end) {
+ return [begin];
+ }
+ return [begin, end];
+}
+
+// FPInterval
+
+interface ConstructorCase {
+ input: IntervalBounds;
+ expected: IntervalBounds;
+}
+
+g.test('constructor')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ConstructorCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ const cases: ConstructorCase[] = [
+ // Common cases
+ { input: [0, 10], expected: [0, 10] },
+ { input: [-5, 0], expected: [-5, 0] },
+ { input: [-5, 10], expected: [-5, 10] },
+ { input: [0], expected: [0] },
+ { input: [10], expected: [10] },
+ { input: [-5], expected: [-5] },
+ { input: [2.5], expected: [2.5] },
+ { input: [-1.375], expected: [-1.375] },
+ { input: [-1.375, 2.5], expected: [-1.375, 2.5] },
+
+ // Edges
+ { input: [0, constants.positive.max], expected: [0, constants.positive.max] },
+ { input: [constants.negative.min, 0], expected: [constants.negative.min, 0] },
+ { input: [constants.negative.min, constants.positive.max], expected: [constants.negative.min, constants.positive.max] },
+
+ // Infinities
+ { input: [0, constants.positive.infinity], expected: [0, Number.POSITIVE_INFINITY] },
+ { input: [constants.negative.infinity, 0], expected: [Number.NEGATIVE_INFINITY, 0] },
+ { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ ];
+
+ // Note: Out of range values are limited to infinities for abstract float, due to abstract
+ // float and 'number' both being f64. So there are no separate OOR tests for abstract float,
+ // otherwise the testing framework will consider them duplicated.
+ if (p.trait !== 'abstract') {
+ // prettier-ignore
+ cases.push(...[
+ // Out of range
+ { input: [0, 2 * constants.positive.max], expected: [0, 2 * constants.positive.max] },
+ { input: [2 * constants.negative.min, 0], expected: [2 * constants.negative.min, 0] },
+ { input: [2 * constants.negative.min, 2 * constants.positive.max], expected: [2 * constants.negative.min, 2 * constants.positive.max] },
+ ] as ConstructorCase[]);
+ }
+
+ return cases;
+ })
+ )
+ .fn(t => {
+ const i = new FPInterval(t.params.trait, ...t.params.input);
+ t.expect(
+ objectEquals(i.bounds(), t.params.expected),
+ `new FPInterval('${t.params.trait}', [${t.params.input}]) returned ${i}. Expected [${t.params.expected}]`
+ );
+ });
+
+interface ContainsNumberCase {
+ bounds: number | IntervalBounds;
+ value: number;
+ expected: boolean;
+}
+
+g.test('contains_number')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ContainsNumberCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ const cases: ContainsNumberCase[] = [
+ // Common usage
+ { bounds: [0, 10], value: 0, expected: true },
+ { bounds: [0, 10], value: 10, expected: true },
+ { bounds: [0, 10], value: 5, expected: true },
+ { bounds: [0, 10], value: -5, expected: false },
+ { bounds: [0, 10], value: 50, expected: false },
+ { bounds: [0, 10], value: Number.NaN, expected: false },
+ { bounds: [-5, 10], value: 0, expected: true },
+ { bounds: [-5, 10], value: 10, expected: true },
+ { bounds: [-5, 10], value: 5, expected: true },
+ { bounds: [-5, 10], value: -5, expected: true },
+ { bounds: [-5, 10], value: -6, expected: false },
+ { bounds: [-5, 10], value: 50, expected: false },
+ { bounds: [-5, 10], value: -10, expected: false },
+ { bounds: [-1.375, 2.5], value: -10, expected: false },
+ { bounds: [-1.375, 2.5], value: 0.5, expected: true },
+ { bounds: [-1.375, 2.5], value: 10, expected: false },
+
+ // Point
+ { bounds: 0, value: 0, expected: true },
+ { bounds: 0, value: 10, expected: false },
+ { bounds: 0, value: -1000, expected: false },
+ { bounds: 10, value: 10, expected: true },
+ { bounds: 10, value: 0, expected: false },
+ { bounds: 10, value: -10, expected: false },
+ { bounds: 10, value: 11, expected: false },
+
+ // Upper infinity
+ { bounds: [0, constants.positive.infinity], value: constants.positive.min, expected: true },
+ { bounds: [0, constants.positive.infinity], value: constants.positive.max, expected: true },
+ { bounds: [0, constants.positive.infinity], value: constants.positive.infinity, expected: true },
+ { bounds: [0, constants.positive.infinity], value: constants.negative.min, expected: false },
+ { bounds: [0, constants.positive.infinity], value: constants.negative.max, expected: false },
+ { bounds: [0, constants.positive.infinity], value: constants.negative.infinity, expected: false },
+
+ // Lower infinity
+ { bounds: [constants.negative.infinity, 0], value: constants.positive.min, expected: false },
+ { bounds: [constants.negative.infinity, 0], value: constants.positive.max, expected: false },
+ { bounds: [constants.negative.infinity, 0], value: constants.positive.infinity, expected: false },
+ { bounds: [constants.negative.infinity, 0], value: constants.negative.min, expected: true },
+ { bounds: [constants.negative.infinity, 0], value: constants.negative.max, expected: true },
+ { bounds: [constants.negative.infinity, 0], value: constants.negative.infinity, expected: true },
+
+ // Full infinity
+ { bounds: [constants.negative.infinity, constants.positive.infinity], value: constants.positive.min, expected: true },
+ { bounds: [constants.negative.infinity, constants.positive.infinity], value: constants.positive.max, expected: true },
+ { bounds: [constants.negative.infinity, constants.positive.infinity], value: constants.positive.infinity, expected: true },
+ { bounds: [constants.negative.infinity, constants.positive.infinity], value: constants.negative.min, expected: true },
+ { bounds: [constants.negative.infinity, constants.positive.infinity], value: constants.negative.max, expected: true },
+ { bounds: [constants.negative.infinity, constants.positive.infinity], value: constants.negative.infinity, expected: true },
+ { bounds: [constants.negative.infinity, constants.positive.infinity], value: Number.NaN, expected: true },
+
+ // Maximum f32 boundary
+ { bounds: [0, constants.positive.max], value: constants.positive.min, expected: true },
+ { bounds: [0, constants.positive.max], value: constants.positive.max, expected: true },
+ { bounds: [0, constants.positive.max], value: constants.positive.infinity, expected: false },
+ { bounds: [0, constants.positive.max], value: constants.negative.min, expected: false },
+ { bounds: [0, constants.positive.max], value: constants.negative.max, expected: false },
+ { bounds: [0, constants.positive.max], value: constants.negative.infinity, expected: false },
+
+ // Minimum f32 boundary
+ { bounds: [constants.negative.min, 0], value: constants.positive.min, expected: false },
+ { bounds: [constants.negative.min, 0], value: constants.positive.max, expected: false },
+ { bounds: [constants.negative.min, 0], value: constants.positive.infinity, expected: false },
+ { bounds: [constants.negative.min, 0], value: constants.negative.min, expected: true },
+ { bounds: [constants.negative.min, 0], value: constants.negative.max, expected: true },
+ { bounds: [constants.negative.min, 0], value: constants.negative.infinity, expected: false },
+
+ // Subnormals
+ { bounds: [0, constants.positive.min], value: constants.positive.subnormal.min, expected: true },
+ { bounds: [0, constants.positive.min], value: constants.positive.subnormal.max, expected: true },
+ { bounds: [0, constants.positive.min], value: constants.negative.subnormal.min, expected: false },
+ { bounds: [0, constants.positive.min], value: constants.negative.subnormal.max, expected: false },
+ { bounds: [constants.negative.max, 0], value: constants.positive.subnormal.min, expected: false },
+ { bounds: [constants.negative.max, 0], value: constants.positive.subnormal.max, expected: false },
+ { bounds: [constants.negative.max, 0], value: constants.negative.subnormal.min, expected: true },
+ { bounds: [constants.negative.max, 0], value: constants.negative.subnormal.max, expected: true },
+ { bounds: [0, constants.positive.subnormal.min], value: constants.positive.subnormal.min, expected: true },
+ { bounds: [0, constants.positive.subnormal.min], value: constants.positive.subnormal.max, expected: false },
+ { bounds: [0, constants.positive.subnormal.min], value: constants.negative.subnormal.min, expected: false },
+ { bounds: [0, constants.positive.subnormal.min], value: constants.negative.subnormal.max, expected: false },
+ { bounds: [constants.negative.subnormal.max, 0], value: constants.positive.subnormal.min, expected: false },
+ { bounds: [constants.negative.subnormal.max, 0], value: constants.positive.subnormal.max, expected: false },
+ { bounds: [constants.negative.subnormal.max, 0], value: constants.negative.subnormal.min, expected: false },
+ { bounds: [constants.negative.subnormal.max, 0], value: constants.negative.subnormal.max, expected: true },
+ ];
+
+ // Note: Out of range values are limited to infinities for abstract float, due to abstract
+ // float and 'number' both being f64. So there are no separate OOR tests for abstract float,
+ // otherwise the testing framework will consider them duplicated.
+ if (p.trait !== 'abstract') {
+ // prettier-ignore
+ cases.push(...[
+ // Out of range high
+ { bounds: [0, 2 * constants.positive.max], value: constants.positive.min, expected: true },
+ { bounds: [0, 2 * constants.positive.max], value: constants.positive.max, expected: true },
+ { bounds: [0, 2 * constants.positive.max], value: constants.positive.infinity, expected: false },
+ { bounds: [0, 2 * constants.positive.max], value: constants.negative.min, expected: false },
+ { bounds: [0, 2 * constants.positive.max], value: constants.negative.max, expected: false },
+ { bounds: [0, 2 * constants.positive.max], value: constants.negative.infinity, expected: false },
+
+ // Out of range low
+ { bounds: [2 * constants.negative.min, 0], value: constants.positive.min, expected: false },
+ { bounds: [2 * constants.negative.min, 0], value: constants.positive.max, expected: false },
+ { bounds: [2 * constants.negative.min, 0], value: constants.positive.infinity, expected: false },
+ { bounds: [2 * constants.negative.min, 0], value: constants.negative.min, expected: true },
+ { bounds: [2 * constants.negative.min, 0], value: constants.negative.max, expected: true },
+ { bounds: [2 * constants.negative.min, 0], value: constants.negative.infinity, expected: false },
+ ] as ContainsNumberCase[]);
+ }
+
+ return cases;
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const i = trait.toInterval(t.params.bounds);
+ const value = t.params.value;
+ const expected = t.params.expected;
+
+ const got = i.contains(value);
+ t.expect(expected === got, `${i}.contains(${value}) returned ${got}. Expected ${expected}`);
+ });
+
+interface ContainsIntervalCase {
+ lhs: number | IntervalBounds;
+ rhs: number | IntervalBounds;
+ expected: boolean;
+}
+
+g.test('contains_interval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ContainsIntervalCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ const cases: ContainsIntervalCase[] = [
+ // Common usage
+ { lhs: [-10, 10], rhs: 0, expected: true },
+ { lhs: [-10, 10], rhs: [-1, 0], expected: true },
+ { lhs: [-10, 10], rhs: [0, 2], expected: true },
+ { lhs: [-10, 10], rhs: [-1, 2], expected: true },
+ { lhs: [-10, 10], rhs: [0, 10], expected: true },
+ { lhs: [-10, 10], rhs: [-10, 2], expected: true },
+ { lhs: [-10, 10], rhs: [-10, 10], expected: true },
+ { lhs: [-10, 10], rhs: [-100, 10], expected: false },
+
+ // Upper infinity
+ { lhs: [0, constants.positive.infinity], rhs: 0, expected: true },
+ { lhs: [0, constants.positive.infinity], rhs: [-1, 0], expected: false },
+ { lhs: [0, constants.positive.infinity], rhs: [0, 1], expected: true },
+ { lhs: [0, constants.positive.infinity], rhs: [0, constants.positive.max], expected: true },
+ { lhs: [0, constants.positive.infinity], rhs: [0, constants.positive.infinity], expected: true },
+ { lhs: [0, constants.positive.infinity], rhs: [100, constants.positive.infinity], expected: true },
+ { lhs: [0, constants.positive.infinity], rhs: [Number.NEGATIVE_INFINITY, constants.positive.infinity], expected: false },
+
+ // Lower infinity
+ { lhs: [constants.negative.infinity, 0], rhs: 0, expected: true },
+ { lhs: [constants.negative.infinity, 0], rhs: [-1, 0], expected: true },
+ { lhs: [constants.negative.infinity, 0], rhs: [constants.negative.min, 0], expected: true },
+ { lhs: [constants.negative.infinity, 0], rhs: [0, 1], expected: false },
+ { lhs: [constants.negative.infinity, 0], rhs: [constants.negative.infinity, 0], expected: true },
+ { lhs: [constants.negative.infinity, 0], rhs: [constants.negative.infinity, -100 ], expected: true },
+ { lhs: [constants.negative.infinity, 0], rhs: [constants.negative.infinity, constants.positive.infinity], expected: false },
+
+ // Full infinity
+ { lhs: [constants.negative.infinity, constants.positive.infinity], rhs: 0, expected: true },
+ { lhs: [constants.negative.infinity, constants.positive.infinity], rhs: [-1, 0], expected: true },
+ { lhs: [constants.negative.infinity, constants.positive.infinity], rhs: [0, 1], expected: true },
+ { lhs: [constants.negative.infinity, constants.positive.infinity], rhs: [0, constants.positive.infinity], expected: true },
+ { lhs: [constants.negative.infinity, constants.positive.infinity], rhs: [100, constants.positive.infinity], expected: true },
+ { lhs: [constants.negative.infinity, constants.positive.infinity], rhs: [constants.negative.infinity, 0], expected: true },
+ { lhs: [constants.negative.infinity, constants.positive.infinity], rhs: [constants.negative.infinity, -100 ], expected: true },
+ { lhs: [constants.negative.infinity, constants.positive.infinity], rhs: [constants.negative.infinity, constants.positive.infinity], expected: true },
+
+ // Maximum boundary
+ { lhs: [0, constants.positive.max], rhs: 0, expected: true },
+ { lhs: [0, constants.positive.max], rhs: [-1, 0], expected: false },
+ { lhs: [0, constants.positive.max], rhs: [0, 1], expected: true },
+ { lhs: [0, constants.positive.max], rhs: [0, constants.positive.max], expected: true },
+ { lhs: [0, constants.positive.max], rhs: [0, constants.positive.infinity], expected: false },
+ { lhs: [0, constants.positive.max], rhs: [100, constants.positive.infinity], expected: false },
+ { lhs: [0, constants.positive.max], rhs: [constants.negative.infinity, constants.positive.infinity], expected: false },
+
+ // Minimum boundary
+ { lhs: [constants.negative.min, 0], rhs: [0, 0], expected: true },
+ { lhs: [constants.negative.min, 0], rhs: [-1, 0], expected: true },
+ { lhs: [constants.negative.min, 0], rhs: [constants.negative.min, 0], expected: true },
+ { lhs: [constants.negative.min, 0], rhs: [0, 1], expected: false },
+ { lhs: [constants.negative.min, 0], rhs: [constants.negative.infinity, 0], expected: false },
+ { lhs: [constants.negative.min, 0], rhs: [constants.negative.infinity, -100 ], expected: false },
+ { lhs: [constants.negative.min, 0], rhs: [constants.negative.infinity, constants.positive.infinity], expected: false },
+ ];
+
+ // Note: Out of range values are limited to infinities for abstract float, due to abstract
+ // float and 'number' both being f64. So there are no separate OOR tests for abstract float,
+ // otherwise the testing framework will consider them duplicated.
+ if (p.trait !== 'abstract') {
+ // prettier-ignore
+ cases.push(...[
+ // Out of range high
+ { lhs: [0, 2 * constants.positive.max], rhs: 0, expected: true },
+ { lhs: [0, 2 * constants.positive.max], rhs: [-1, 0], expected: false },
+ { lhs: [0, 2 * constants.positive.max], rhs: [0, 1], expected: true },
+ { lhs: [0, 2 * constants.positive.max], rhs: [0, constants.positive.max], expected: true },
+ { lhs: [0, 2 * constants.positive.max], rhs: [0, constants.positive.infinity], expected: false },
+ { lhs: [0, 2 * constants.positive.max], rhs: [100, constants.positive.infinity], expected: false },
+ { lhs: [0, 2 * constants.positive.max], rhs: [constants.negative.infinity, constants.positive.infinity], expected: false },
+
+ // Out of range low
+ { lhs: [2 * constants.negative.min, 0], rhs: 0, expected: true },
+ { lhs: [2 * constants.negative.min, 0], rhs: [-1, 0], expected: true },
+ { lhs: [2 * constants.negative.min, 0], rhs: [constants.negative.min, 0], expected: true },
+ { lhs: [2 * constants.negative.min, 0], rhs: [0, 1], expected: false },
+ { lhs: [2 * constants.negative.min, 0], rhs: [constants.negative.infinity, 0], expected: false },
+ { lhs: [2 * constants.negative.min, 0], rhs: [constants.negative.infinity, -100 ], expected: false },
+ { lhs: [2 * constants.negative.min, 0], rhs: [constants.negative.infinity, constants.positive.infinity], expected: false },
+ ] as ContainsIntervalCase[]);
+ }
+
+ return cases;
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const lhs = trait.toInterval(t.params.lhs);
+ const rhs = trait.toInterval(t.params.rhs);
+ const expected = t.params.expected;
+
+ const got = lhs.contains(rhs);
+ t.expect(expected === got, `${lhs}.contains(${rhs}) returned ${got}. Expected ${expected}`);
+ });
+
+// Utilities
+
+interface SpanIntervalsCase {
+ intervals: (number | IntervalBounds)[];
+ expected: number | IntervalBounds;
+}
+
+g.test('spanIntervals')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<SpanIntervalsCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ return [
+ // Single Intervals
+ { intervals: [[0, 10]], expected: [0, 10] },
+ { intervals: [[0, constants.positive.max]], expected: [0, constants.positive.max] },
+ { intervals: [[0, constants.positive.nearest_max]], expected: [0, constants.positive.nearest_max] },
+ { intervals: [[0, constants.positive.infinity]], expected: [0, Number.POSITIVE_INFINITY] },
+ { intervals: [[constants.negative.min, 0]], expected: [constants.negative.min, 0] },
+ { intervals: [[constants.negative.nearest_min, 0]], expected: [constants.negative.nearest_min, 0] },
+ { intervals: [[constants.negative.infinity, 0]], expected: [Number.NEGATIVE_INFINITY, 0] },
+
+ // Double Intervals
+ { intervals: [[0, 1], [2, 5]], expected: [0, 5] },
+ { intervals: [[2, 5], [0, 1]], expected: [0, 5] },
+ { intervals: [[0, 2], [1, 5]], expected: [0, 5] },
+ { intervals: [[0, 5], [1, 2]], expected: [0, 5] },
+ { intervals: [[constants.negative.infinity, 0], [0, constants.positive.infinity]], expected: kUnboundedBounds },
+
+ // Multiple Intervals
+ { intervals: [[0, 1], [2, 3], [4, 5]], expected: [0, 5] },
+ { intervals: [[0, 1], [4, 5], [2, 3]], expected: [0, 5] },
+ { intervals: [[0, 1], [0, 1], [0, 1]], expected: [0, 1] },
+
+ // Point Intervals
+ { intervals: [1], expected: 1 },
+ { intervals: [1, 2], expected: [1, 2] },
+ { intervals: [-10, 2], expected: [-10, 2] },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const intervals = t.params.intervals.map(i => trait.toInterval(i));
+ const expected = trait.toInterval(t.params.expected);
+
+ const got = trait.spanIntervals(...intervals);
+ t.expect(
+ objectEquals(got, expected),
+ `${t.params.trait}.span({${intervals}}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface isVectorCase {
+ input: (number | IntervalBounds | FPIntervalParam)[];
+ expected: boolean;
+}
+
+g.test('isVector')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<isVectorCase>(p => {
+ const trait = FP[p.trait];
+ return [
+ // numbers
+ { input: [1, 2], expected: false },
+ { input: [1, 2, 3], expected: false },
+ { input: [1, 2, 3, 4], expected: false },
+
+ // IntervalBounds
+ { input: [[1], [2]], expected: false },
+ { input: [[1], [2], [3]], expected: false },
+ { input: [[1], [2], [3], [4]], expected: false },
+ {
+ input: [
+ [1, 2],
+ [2, 3],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ [4, 5],
+ ],
+ expected: false,
+ },
+
+ // FPInterval, valid dimensions
+ { input: [trait.toParam([1]), trait.toParam([2])], expected: true },
+ { input: [trait.toParam([1, 2]), trait.toParam([2, 3])], expected: true },
+ {
+ input: [trait.toParam([1]), trait.toParam([2]), trait.toParam([3])],
+ expected: true,
+ },
+ {
+ input: [trait.toParam([1, 2]), trait.toParam([2, 3]), trait.toParam([3, 4])],
+ expected: true,
+ },
+ {
+ input: [trait.toParam([1]), trait.toParam([2]), trait.toParam([3]), trait.toParam([4])],
+ expected: true,
+ },
+ {
+ input: [
+ trait.toParam([1, 2]),
+ trait.toParam([2, 3]),
+ trait.toParam([3, 4]),
+ trait.toParam([4, 5]),
+ ],
+ expected: true,
+ },
+
+ // FPInterval, invalid dimensions
+ { input: [trait.toParam([1])], expected: false },
+ {
+ input: [
+ trait.toParam([1]),
+ trait.toParam([2]),
+ trait.toParam([3]),
+ trait.toParam([4]),
+ trait.toParam([5]),
+ ],
+ expected: false,
+ },
+
+ // Mixed
+ { input: [1, [2]], expected: false },
+ { input: [1, [2], trait.toParam([3])], expected: false },
+ { input: [1, trait.toParam([2]), [3], 4], expected: false },
+ { input: [trait.toParam(1), 2], expected: false },
+ { input: [trait.toParam(1), [2]], expected: false },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const input = t.params.input.map(e => trait.fromParam(e));
+ const expected = t.params.expected;
+
+ const got = trait.isVector(input);
+ t.expect(
+ got === expected,
+ `${t.params.trait}.isVector([${input}]) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface toVectorCase {
+ input: (number | IntervalBounds | FPIntervalParam)[];
+ expected: (number | IntervalBounds)[];
+}
+
+g.test('toVector')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<toVectorCase>(p => {
+ const trait = FP[p.trait];
+ return [
+ // numbers
+ { input: [1, 2], expected: [1, 2] },
+ { input: [1, 2, 3], expected: [1, 2, 3] },
+ { input: [1, 2, 3, 4], expected: [1, 2, 3, 4] },
+
+ // IntervalBounds
+ { input: [[1], [2]], expected: [1, 2] },
+ { input: [[1], [2], [3]], expected: [1, 2, 3] },
+ { input: [[1], [2], [3], [4]], expected: [1, 2, 3, 4] },
+ {
+ input: [
+ [1, 2],
+ [2, 3],
+ ],
+ expected: [
+ [1, 2],
+ [2, 3],
+ ],
+ },
+ {
+ input: [
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ ],
+ expected: [
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ ],
+ },
+ {
+ input: [
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ [4, 5],
+ ],
+ expected: [
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ [4, 5],
+ ],
+ },
+
+ // FPInterval
+ { input: [trait.toParam([1]), trait.toParam([2])], expected: [1, 2] },
+ {
+ input: [trait.toParam([1, 2]), trait.toParam([2, 3])],
+ expected: [
+ [1, 2],
+ [2, 3],
+ ],
+ },
+ {
+ input: [trait.toParam([1]), trait.toParam([2]), trait.toParam([3])],
+ expected: [1, 2, 3],
+ },
+ {
+ input: [trait.toParam([1, 2]), trait.toParam([2, 3]), trait.toParam([3, 4])],
+ expected: [
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ ],
+ },
+ {
+ input: [trait.toParam([1]), trait.toParam([2]), trait.toParam([3]), trait.toParam([4])],
+ expected: [1, 2, 3, 4],
+ },
+ {
+ input: [
+ trait.toParam([1, 2]),
+ trait.toParam([2, 3]),
+ trait.toParam([3, 4]),
+ trait.toParam([4, 5]),
+ ],
+ expected: [
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ [4, 5],
+ ],
+ },
+
+ // Mixed
+ { input: [1, [2]], expected: [1, 2] },
+ { input: [1, [2], trait.toParam([3])], expected: [1, 2, 3] },
+ { input: [1, trait.toParam([2]), [3], 4], expected: [1, 2, 3, 4] },
+ {
+ input: [1, [2], [2, 3], kUnboundedInterval[p.trait]],
+ expected: [1, 2, [2, 3], kUnboundedBounds],
+ },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const input = t.params.input.map(e => trait.fromParam(e));
+ const expected = t.params.expected.map(e => trait.toInterval(e));
+
+ const got = trait.toVector(input);
+ t.expect(
+ objectEquals(got, expected),
+ `${t.params.trait}.toVector([${input}]) returned [${got}]. Expected [${expected}]`
+ );
+ });
+
+interface isMatrixCase {
+ input: (number | IntervalBounds | FPIntervalParam)[][];
+ expected: boolean;
+}
+
+g.test('isMatrix')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<isMatrixCase>(p => {
+ const trait = FP[p.trait];
+ return [
+ // numbers
+ {
+ input: [
+ [1, 2],
+ [3, 4],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ [7, 8],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [1, 2, 3],
+ [4, 5, 6],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ [10, 11, 12],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ [13, 14, 15, 16],
+ ],
+ expected: false,
+ },
+
+ // IntervalBounds
+ {
+ input: [
+ [[1], [2]],
+ [[3], [4]],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [[1], [2]],
+ [[3], [4]],
+ [[5], [6]],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [[1], [2]],
+ [[3], [4]],
+ [[5], [6]],
+ [[7], [8]],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [[1], [2], [3]],
+ [[4], [5], [6]],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [[1], [2], [3]],
+ [[4], [5], [6]],
+ [[7], [8], [9]],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [[1], [2], [3]],
+ [[4], [5], [6]],
+ [[7], [8], [9]],
+ [[10], [11], [12]],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [[1], [2], [3], [4]],
+ [[5], [6], [7], [8]],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [[1], [2], [3], [4]],
+ [[5], [6], [7], [8]],
+ [[9], [10], [11], [12]],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [[1], [2], [3], [4]],
+ [[5], [6], [7], [8]],
+ [[9], [10], [11], [12]],
+ [[13], [14], [15], [16]],
+ ],
+ expected: false,
+ },
+
+ // FPInterval, valid dimensions
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2)],
+ [trait.toParam(3), trait.toParam(4)],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2)],
+ [trait.toParam(3), trait.toParam(4)],
+ [trait.toParam(5), trait.toParam(6)],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2)],
+ [trait.toParam(3), trait.toParam(4)],
+ [trait.toParam(5), trait.toParam(6)],
+ [trait.toParam(7), trait.toParam(8)],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2), trait.toParam(3)],
+ [trait.toParam(4), trait.toParam(5), trait.toParam(6)],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2), trait.toParam(3)],
+ [trait.toParam(4), trait.toParam(5), trait.toParam(6)],
+ [trait.toParam(7), trait.toParam(8), trait.toParam(9)],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2), trait.toParam(3)],
+ [trait.toParam(4), trait.toParam(5), trait.toParam(6)],
+ [trait.toParam(7), trait.toParam(8), trait.toParam(9)],
+ [trait.toParam(10), trait.toParam(11), trait.toParam(12)],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2), trait.toParam(3), trait.toParam(4)],
+ [trait.toParam(5), trait.toParam(6), trait.toParam(7), trait.toParam(8)],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2), trait.toParam(3), trait.toParam(4)],
+ [trait.toParam(5), trait.toParam(6), trait.toParam(7), trait.toParam(8)],
+ [trait.toParam(9), trait.toParam(10), trait.toParam(11), trait.toParam(12)],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2), trait.toParam(3), trait.toParam(4)],
+ [trait.toParam(5), trait.toParam(6), trait.toParam(7), trait.toParam(8)],
+ [trait.toParam(9), trait.toParam(10), trait.toParam(11), trait.toParam(12)],
+ [trait.toParam(13), trait.toParam(14), trait.toParam(15), trait.toParam(16)],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [trait.toParam([1, 2]), trait.toParam([2, 3])],
+ [trait.toParam([3, 4]), trait.toParam([4, 5])],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [trait.toParam([1, 2]), trait.toParam([2, 3])],
+ [trait.toParam([3, 4]), trait.toParam([4, 5])],
+ [trait.toParam([5, 6]), trait.toParam([6, 7])],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [trait.toParam([1, 2]), trait.toParam([2, 3])],
+ [trait.toParam([3, 4]), trait.toParam([4, 5])],
+ [trait.toParam([5, 6]), trait.toParam([6, 7])],
+ [trait.toParam([7, 8]), trait.toParam([8, 9])],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [trait.toParam([1, 2]), trait.toParam([2, 3]), trait.toParam([3, 4])],
+ [trait.toParam([4, 5]), trait.toParam([5, 6]), trait.toParam([6, 7])],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [trait.toParam([1, 2]), trait.toParam([2, 3]), trait.toParam([3, 4])],
+ [trait.toParam([4, 5]), trait.toParam([5, 6]), trait.toParam([6, 7])],
+ [trait.toParam([7, 8]), trait.toParam([8, 9]), trait.toParam([9, 10])],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [trait.toParam([1, 2]), trait.toParam([2, 3]), trait.toParam([3, 4])],
+ [trait.toParam([4, 5]), trait.toParam([5, 6]), trait.toParam([6, 7])],
+ [trait.toParam([7, 8]), trait.toParam([8, 9]), trait.toParam([9, 10])],
+ [trait.toParam([10, 11]), trait.toParam([11, 12]), trait.toParam([12, 13])],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [
+ trait.toParam([1, 2]),
+ trait.toParam([2, 3]),
+ trait.toParam([3, 4]),
+ trait.toParam([4, 5]),
+ ],
+ [
+ trait.toParam([5, 6]),
+ trait.toParam([6, 7]),
+ trait.toParam([7, 8]),
+ trait.toParam([8, 9]),
+ ],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [
+ trait.toParam([1, 2]),
+ trait.toParam([2, 3]),
+ trait.toParam([3, 4]),
+ trait.toParam([4, 5]),
+ ],
+ [
+ trait.toParam([5, 6]),
+ trait.toParam([6, 7]),
+ trait.toParam([7, 8]),
+ trait.toParam([8, 9]),
+ ],
+ [
+ trait.toParam([9, 10]),
+ trait.toParam([10, 11]),
+ trait.toParam([11, 12]),
+ trait.toParam([12, 13]),
+ ],
+ ],
+ expected: true,
+ },
+ {
+ input: [
+ [
+ trait.toParam([1, 2]),
+ trait.toParam([2, 3]),
+ trait.toParam([3, 4]),
+ trait.toParam([4, 5]),
+ ],
+ [
+ trait.toParam([5, 6]),
+ trait.toParam([6, 7]),
+ trait.toParam([7, 8]),
+ trait.toParam([8, 9]),
+ ],
+ [
+ trait.toParam([9, 10]),
+ trait.toParam([10, 11]),
+ trait.toParam([11, 12]),
+ trait.toParam([12, 13]),
+ ],
+ [
+ trait.toParam([13, 14]),
+ trait.toParam([14, 15]),
+ trait.toParam([15, 16]),
+ trait.toParam([16, 17]),
+ ],
+ ],
+ expected: true,
+ },
+
+ // FPInterval, invalid dimensions
+ { input: [[trait.toParam(1)]], expected: false },
+ {
+ input: [[trait.toParam(1)], [trait.toParam(3), trait.toParam(4)]],
+ expected: false,
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2)],
+ [trait.toParam(3), trait.toParam(4), trait.toParam(5)],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2)],
+ [trait.toParam(3), trait.toParam(4)],
+ [trait.toParam(5)],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2)],
+ [trait.toParam(3), trait.toParam(4)],
+ [trait.toParam(5), trait.toParam(6)],
+ [trait.toParam(7), trait.toParam(8)],
+ [trait.toParam(9), trait.toParam(10)],
+ ],
+ expected: false,
+ },
+
+ // Mixed
+ {
+ input: [
+ [1, [2]],
+ [3, 4],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [[1], [2]],
+ [[3], 4],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [1, 2],
+ [trait.toParam([3]), 4],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [[1], trait.toParam([2])],
+ [trait.toParam([3]), trait.toParam([4])],
+ ],
+ expected: false,
+ },
+ {
+ input: [
+ [trait.toParam(1), [2]],
+ [3, 4],
+ ],
+ expected: false,
+ },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const input = t.params.input.map(a => a.map(e => trait.fromParam(e)));
+ const expected = t.params.expected;
+
+ const got = trait.isMatrix(input);
+ t.expect(
+ got === expected,
+ `${t.params.trait}.isMatrix([${input}]) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface toMatrixCase {
+ input: (number | IntervalBounds | FPIntervalParam)[][];
+ expected: (number | IntervalBounds)[][];
+}
+
+g.test('toMatrix')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<toMatrixCase>(p => {
+ const trait = FP[p.trait];
+ return [
+ // numbers
+ {
+ input: [
+ [1, 2],
+ [3, 4],
+ ],
+ expected: [
+ [1, 2],
+ [3, 4],
+ ],
+ },
+ {
+ input: [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ ],
+ expected: [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ ],
+ },
+ {
+ input: [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ [7, 8],
+ ],
+ expected: [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ [7, 8],
+ ],
+ },
+ {
+ input: [
+ [1, 2, 3],
+ [4, 5, 6],
+ ],
+ expected: [
+ [1, 2, 3],
+ [4, 5, 6],
+ ],
+ },
+ {
+ input: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ ],
+ expected: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ ],
+ },
+ {
+ input: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ [10, 11, 12],
+ ],
+ expected: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ [10, 11, 12],
+ ],
+ },
+ {
+ input: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ ],
+ expected: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ ],
+ },
+ {
+ input: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ ],
+ expected: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ ],
+ },
+ {
+ input: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ [13, 14, 15, 16],
+ ],
+ expected: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ [13, 14, 15, 16],
+ ],
+ },
+
+ // IntervalBounds
+ {
+ input: [
+ [[1], [2]],
+ [[3], [4]],
+ ],
+ expected: [
+ [1, 2],
+ [3, 4],
+ ],
+ },
+ {
+ input: [
+ [[1], [2]],
+ [[3], [4]],
+ [[5], [6]],
+ ],
+ expected: [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ ],
+ },
+ {
+ input: [
+ [[1], [2]],
+ [[3], [4]],
+ [[5], [6]],
+ [[7], [8]],
+ ],
+ expected: [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ [7, 8],
+ ],
+ },
+ {
+ input: [
+ [[1], [2], [3]],
+ [[4], [5], [6]],
+ ],
+ expected: [
+ [1, 2, 3],
+ [4, 5, 6],
+ ],
+ },
+ {
+ input: [
+ [[1], [2], [3]],
+ [[4], [5], [6]],
+ [[7], [8], [9]],
+ ],
+ expected: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ ],
+ },
+ {
+ input: [
+ [[1], [2], [3]],
+ [[4], [5], [6]],
+ [[7], [8], [9]],
+ [[10], [11], [12]],
+ ],
+ expected: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ [10, 11, 12],
+ ],
+ },
+ {
+ input: [
+ [[1], [2], [3], [4]],
+ [[5], [6], [7], [8]],
+ ],
+ expected: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ ],
+ },
+ {
+ input: [
+ [[1], [2], [3], [4]],
+ [[5], [6], [7], [8]],
+ [[9], [10], [11], [12]],
+ ],
+ expected: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ ],
+ },
+ {
+ input: [
+ [[1], [2], [3], [4]],
+ [[5], [6], [7], [8]],
+ [[9], [10], [11], [12]],
+ [[13], [14], [15], [16]],
+ ],
+ expected: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ [13, 14, 15, 16],
+ ],
+ },
+
+ // FPInterval
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2)],
+ [trait.toParam(3), trait.toParam(4)],
+ ],
+ expected: [
+ [1, 2],
+ [3, 4],
+ ],
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2)],
+ [trait.toParam(3), trait.toParam(4)],
+ [trait.toParam(5), trait.toParam(6)],
+ ],
+ expected: [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ ],
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2)],
+ [trait.toParam(3), trait.toParam(4)],
+ [trait.toParam(5), trait.toParam(6)],
+ [trait.toParam(7), trait.toParam(8)],
+ ],
+ expected: [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ [7, 8],
+ ],
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2), trait.toParam(3)],
+ [trait.toParam(4), trait.toParam(5), trait.toParam(6)],
+ ],
+ expected: [
+ [1, 2, 3],
+ [4, 5, 6],
+ ],
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2), trait.toParam(3)],
+ [trait.toParam(4), trait.toParam(5), trait.toParam(6)],
+ [trait.toParam(7), trait.toParam(8), trait.toParam(9)],
+ ],
+ expected: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ ],
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2), trait.toParam(3)],
+ [trait.toParam(4), trait.toParam(5), trait.toParam(6)],
+ [trait.toParam(7), trait.toParam(8), trait.toParam(9)],
+ [trait.toParam(10), trait.toParam(11), trait.toParam(12)],
+ ],
+ expected: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ [10, 11, 12],
+ ],
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2), trait.toParam(3), trait.toParam(4)],
+ [trait.toParam(5), trait.toParam(6), trait.toParam(7), trait.toParam(8)],
+ ],
+ expected: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ ],
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2), trait.toParam(3), trait.toParam(4)],
+ [trait.toParam(5), trait.toParam(6), trait.toParam(7), trait.toParam(8)],
+ [trait.toParam(9), trait.toParam(10), trait.toParam(11), trait.toParam(12)],
+ ],
+ expected: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ ],
+ },
+ {
+ input: [
+ [trait.toParam(1), trait.toParam(2), trait.toParam(3), trait.toParam(4)],
+ [trait.toParam(5), trait.toParam(6), trait.toParam(7), trait.toParam(8)],
+ [trait.toParam(9), trait.toParam(10), trait.toParam(11), trait.toParam(12)],
+ [trait.toParam(13), trait.toParam(14), trait.toParam(15), trait.toParam(16)],
+ ],
+ expected: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ [13, 14, 15, 16],
+ ],
+ },
+
+ {
+ input: [
+ [trait.toParam([1, 2]), trait.toParam([2, 3])],
+ [trait.toParam([3, 4]), trait.toParam([4, 5])],
+ ],
+ expected: [
+ [
+ [1, 2],
+ [2, 3],
+ ],
+ [
+ [3, 4],
+ [4, 5],
+ ],
+ ],
+ },
+ {
+ input: [
+ [trait.toParam([1, 2]), trait.toParam([2, 3])],
+ [trait.toParam([3, 4]), trait.toParam([4, 5])],
+ [trait.toParam([5, 6]), trait.toParam([6, 7])],
+ ],
+ expected: [
+ [
+ [1, 2],
+ [2, 3],
+ ],
+ [
+ [3, 4],
+ [4, 5],
+ ],
+ [
+ [5, 6],
+ [6, 7],
+ ],
+ ],
+ },
+ {
+ input: [
+ [trait.toParam([1, 2]), trait.toParam([2, 3])],
+ [trait.toParam([3, 4]), trait.toParam([4, 5])],
+ [trait.toParam([5, 6]), trait.toParam([6, 7])],
+ [trait.toParam([7, 8]), trait.toParam([8, 9])],
+ ],
+ expected: [
+ [
+ [1, 2],
+ [2, 3],
+ ],
+ [
+ [3, 4],
+ [4, 5],
+ ],
+ [
+ [5, 6],
+ [6, 7],
+ ],
+ [
+ [7, 8],
+ [8, 9],
+ ],
+ ],
+ },
+ {
+ input: [
+ [trait.toParam([1, 2]), trait.toParam([2, 3]), trait.toParam([3, 4])],
+ [trait.toParam([4, 5]), trait.toParam([5, 6]), trait.toParam([6, 7])],
+ ],
+ expected: [
+ [
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ ],
+ [
+ [4, 5],
+ [5, 6],
+ [6, 7],
+ ],
+ ],
+ },
+ {
+ input: [
+ [trait.toParam([1, 2]), trait.toParam([2, 3]), trait.toParam([3, 4])],
+ [trait.toParam([4, 5]), trait.toParam([5, 6]), trait.toParam([6, 7])],
+ [trait.toParam([7, 8]), trait.toParam([8, 9]), trait.toParam([9, 10])],
+ ],
+ expected: [
+ [
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ ],
+ [
+ [4, 5],
+ [5, 6],
+ [6, 7],
+ ],
+ [
+ [7, 8],
+ [8, 9],
+ [9, 10],
+ ],
+ ],
+ },
+ {
+ input: [
+ [trait.toParam([1, 2]), trait.toParam([2, 3]), trait.toParam([3, 4])],
+ [trait.toParam([4, 5]), trait.toParam([5, 6]), trait.toParam([6, 7])],
+ [trait.toParam([7, 8]), trait.toParam([8, 9]), trait.toParam([9, 10])],
+ [trait.toParam([10, 11]), trait.toParam([11, 12]), trait.toParam([12, 13])],
+ ],
+ expected: [
+ [
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ ],
+ [
+ [4, 5],
+ [5, 6],
+ [6, 7],
+ ],
+ [
+ [7, 8],
+ [8, 9],
+ [9, 10],
+ ],
+ [
+ [10, 11],
+ [11, 12],
+ [12, 13],
+ ],
+ ],
+ },
+ {
+ input: [
+ [
+ trait.toParam([1, 2]),
+ trait.toParam([2, 3]),
+ trait.toParam([3, 4]),
+ trait.toParam([4, 5]),
+ ],
+ [
+ trait.toParam([5, 6]),
+ trait.toParam([6, 7]),
+ trait.toParam([7, 8]),
+ trait.toParam([8, 9]),
+ ],
+ ],
+ expected: [
+ [
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ [4, 5],
+ ],
+ [
+ [5, 6],
+ [6, 7],
+ [7, 8],
+ [8, 9],
+ ],
+ ],
+ },
+ {
+ input: [
+ [
+ trait.toParam([1, 2]),
+ trait.toParam([2, 3]),
+ trait.toParam([3, 4]),
+ trait.toParam([4, 5]),
+ ],
+ [
+ trait.toParam([5, 6]),
+ trait.toParam([6, 7]),
+ trait.toParam([7, 8]),
+ trait.toParam([8, 9]),
+ ],
+ [
+ trait.toParam([9, 10]),
+ trait.toParam([10, 11]),
+ trait.toParam([11, 12]),
+ trait.toParam([12, 13]),
+ ],
+ ],
+ expected: [
+ [
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ [4, 5],
+ ],
+ [
+ [5, 6],
+ [6, 7],
+ [7, 8],
+ [8, 9],
+ ],
+ [
+ [9, 10],
+ [10, 11],
+ [11, 12],
+ [12, 13],
+ ],
+ ],
+ },
+ {
+ input: [
+ [
+ trait.toParam([1, 2]),
+ trait.toParam([2, 3]),
+ trait.toParam([3, 4]),
+ trait.toParam([4, 5]),
+ ],
+ [
+ trait.toParam([5, 6]),
+ trait.toParam([6, 7]),
+ trait.toParam([7, 8]),
+ trait.toParam([8, 9]),
+ ],
+ [
+ trait.toParam([9, 10]),
+ trait.toParam([10, 11]),
+ trait.toParam([11, 12]),
+ trait.toParam([12, 13]),
+ ],
+ [
+ trait.toParam([13, 14]),
+ trait.toParam([14, 15]),
+ trait.toParam([15, 16]),
+ trait.toParam([16, 17]),
+ ],
+ ],
+ expected: [
+ [
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ [4, 5],
+ ],
+ [
+ [5, 6],
+ [6, 7],
+ [7, 8],
+ [8, 9],
+ ],
+ [
+ [9, 10],
+ [10, 11],
+ [11, 12],
+ [12, 13],
+ ],
+ [
+ [13, 14],
+ [14, 15],
+ [15, 16],
+ [16, 17],
+ ],
+ ],
+ },
+
+ // Mixed
+ {
+ input: [
+ [1, [2]],
+ [3, 4],
+ ],
+ expected: [
+ [1, 2],
+ [3, 4],
+ ],
+ },
+ {
+ input: [
+ [[1], [2]],
+ [[3], 4],
+ ],
+ expected: [
+ [1, 2],
+ [3, 4],
+ ],
+ },
+ {
+ input: [
+ [1, 2],
+ [trait.toParam([3]), 4],
+ ],
+ expected: [
+ [1, 2],
+ [3, 4],
+ ],
+ },
+ {
+ input: [
+ [[1], trait.toParam([2])],
+ [trait.toParam([3]), trait.toParam([4])],
+ ],
+ expected: [
+ [1, 2],
+ [3, 4],
+ ],
+ },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const input = map2DArray(t.params.input, e => trait.fromParam(e));
+ const expected = map2DArray(t.params.expected, e => trait.toInterval(e));
+
+ const got = trait.toMatrix(input);
+ t.expect(
+ objectEquals(got, expected),
+ `${t.params.trait}.toMatrix([${input}]) returned [${got}]. Expected [${expected}]`
+ );
+ });
+
+// API - Fundamental Error Intervals
+
+interface AbsoluteErrorCase {
+ value: number;
+ error: number;
+ expected: number | IntervalBounds;
+}
+
+// Special values used for testing absolute error interval
+// A small absolute error value is a representable value x that much smaller than 1.0,
+// but 1.0 +/- x is still exactly representable.
+const kSmallAbsoluteErrorValue = {
+ f32: 2 ** -11, // Builtin cos and sin has a absolute error 2**-11 for f32
+ f16: 2 ** -7, // Builtin cos and sin has a absolute error 2**-7 for f16
+} as const;
+// A large absolute error value is a representable value x that much smaller than maximum
+// positive, but positive.max - x is still exactly representable.
+const kLargeAbsoluteErrorValue = {
+ f32: 2 ** 110, // f32.positive.max - 2**110 = 3.4028104e+38 = 0x7f7fffbf in f32
+ f16: 2 ** 10, // f16.positive.max - 2**10 = 64480 = 0x7bdf in f16
+} as const;
+// A subnormal absolute error value is a subnormal representable value x of kind, which ensures
+// that positive.subnormal.min +/- x is still exactly representable.
+const kSubnormalAbsoluteErrorValue = {
+ f32: 2 ** -140, // f32 0x00000200
+ f16: 2 ** -20, // f16 0x0010
+} as const;
+
+g.test('absoluteErrorInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<AbsoluteErrorCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ const smallErr = kSmallAbsoluteErrorValue[p.trait];
+ const largeErr = kLargeAbsoluteErrorValue[p.trait];
+ const subnormalErr = kSubnormalAbsoluteErrorValue[p.trait];
+ // prettier-ignore
+ return [
+ // Edge Cases
+ // 1. Interval around infinity would be kUnboundedBounds
+ { value: constants.positive.infinity, error: 0, expected: kUnboundedBounds },
+ { value: constants.positive.infinity, error: largeErr, expected: kUnboundedBounds },
+ { value: constants.positive.infinity, error: 1, expected: kUnboundedBounds },
+ { value: constants.negative.infinity, error: 0, expected: kUnboundedBounds },
+ { value: constants.negative.infinity, error: largeErr, expected: kUnboundedBounds },
+ { value: constants.negative.infinity, error: 1, expected: kUnboundedBounds },
+ // 2. Interval around largest finite positive/negative
+ { value: constants.positive.max, error: 0, expected: constants.positive.max },
+ { value: constants.positive.max, error: largeErr, expected: kUnboundedBounds},
+ { value: constants.positive.max, error: constants.positive.max, expected: kUnboundedBounds},
+ { value: constants.negative.min, error: 0, expected: constants.negative.min },
+ { value: constants.negative.min, error: largeErr, expected: kUnboundedBounds},
+ { value: constants.negative.min, error: constants.positive.max, expected: kUnboundedBounds},
+ // 3. Interval around small but normal center, center should not get flushed.
+ { value: constants.positive.min, error: 0, expected: constants.positive.min },
+ { value: constants.positive.min, error: smallErr, expected: [constants.positive.min - smallErr, constants.positive.min + smallErr]},
+ { value: constants.positive.min, error: 1, expected: [constants.positive.min - 1, constants.positive.min + 1]},
+ { value: constants.negative.max, error: 0, expected: constants.negative.max },
+ { value: constants.negative.max, error: smallErr, expected: [constants.negative.max - smallErr, constants.negative.max + smallErr]},
+ { value: constants.negative.max, error: 1, expected: [constants.negative.max - 1, constants.negative.max + 1] },
+ // 4. Subnormals, center can be flushed to 0.0
+ { value: constants.positive.subnormal.max, error: 0, expected: [0, constants.positive.subnormal.max] },
+ { value: constants.positive.subnormal.max, error: subnormalErr, expected: [-subnormalErr, constants.positive.subnormal.max + subnormalErr]},
+ { value: constants.positive.subnormal.max, error: smallErr, expected: [-smallErr, constants.positive.subnormal.max + smallErr]},
+ { value: constants.positive.subnormal.max, error: 1, expected: [-1, constants.positive.subnormal.max + 1]},
+ { value: constants.positive.subnormal.min, error: 0, expected: [0, constants.positive.subnormal.min] },
+ { value: constants.positive.subnormal.min, error: subnormalErr, expected: [-subnormalErr, constants.positive.subnormal.min + subnormalErr]},
+ { value: constants.positive.subnormal.min, error: smallErr, expected: [-smallErr, constants.positive.subnormal.min + smallErr]},
+ { value: constants.positive.subnormal.min, error: 1, expected: [-1, constants.positive.subnormal.min + 1] },
+ { value: constants.negative.subnormal.min, error: 0, expected: [constants.negative.subnormal.min, 0] },
+ { value: constants.negative.subnormal.min, error: subnormalErr, expected: [constants.negative.subnormal.min - subnormalErr, subnormalErr] },
+ { value: constants.negative.subnormal.min, error: smallErr, expected: [constants.negative.subnormal.min - smallErr, smallErr] },
+ { value: constants.negative.subnormal.min, error: 1, expected: [constants.negative.subnormal.min - 1, 1] },
+ { value: constants.negative.subnormal.max, error: 0, expected: [constants.negative.subnormal.max, 0] },
+ { value: constants.negative.subnormal.max, error: subnormalErr, expected: [constants.negative.subnormal.max - subnormalErr, subnormalErr] },
+ { value: constants.negative.subnormal.max, error: smallErr, expected: [constants.negative.subnormal.max - smallErr, smallErr] },
+ { value: constants.negative.subnormal.max, error: 1, expected: [constants.negative.subnormal.max - 1, 1] },
+
+ // 64-bit subnormals, expected to be treated as 0.0 or smallest subnormal of kind.
+ { value: reinterpretU64AsF64(0x0000_0000_0000_0001n), error: 0, expected: [0, constants.positive.subnormal.min] },
+ { value: reinterpretU64AsF64(0x0000_0000_0000_0001n), error: subnormalErr, expected: [-subnormalErr, constants.positive.subnormal.min + subnormalErr] },
+ // Note that f32 minimum subnormal is so smaller than 1.0, adding them together may result in the f64 results 1.0.
+ { value: reinterpretU64AsF64(0x0000_0000_0000_0001n), error: 1, expected: [-1, constants.positive.subnormal.min + 1] },
+ { value: reinterpretU64AsF64(0x0000_0000_0000_0002n), error: 0, expected: [0, constants.positive.subnormal.min] },
+ { value: reinterpretU64AsF64(0x0000_0000_0000_0002n), error: subnormalErr, expected: [-subnormalErr, constants.positive.subnormal.min + subnormalErr] },
+ { value: reinterpretU64AsF64(0x0000_0000_0000_0002n), error: 1, expected: [-1, constants.positive.subnormal.min + 1] },
+ { value: reinterpretU64AsF64(0x800f_ffff_ffff_ffffn), error: 0, expected: [constants.negative.subnormal.max, 0] },
+ { value: reinterpretU64AsF64(0x800f_ffff_ffff_ffffn), error: subnormalErr, expected: [constants.negative.subnormal.max - subnormalErr, subnormalErr] },
+ { value: reinterpretU64AsF64(0x800f_ffff_ffff_ffffn), error: 1, expected: [constants.negative.subnormal.max - 1, 1] },
+ { value: reinterpretU64AsF64(0x800f_ffff_ffff_fffen), error: 0, expected: [constants.negative.subnormal.max, 0] },
+ { value: reinterpretU64AsF64(0x800f_ffff_ffff_fffen), error: subnormalErr, expected: [constants.negative.subnormal.max - subnormalErr, subnormalErr] },
+ { value: reinterpretU64AsF64(0x800f_ffff_ffff_fffen), error: 1, expected: [constants.negative.subnormal.max - 1, 1] },
+
+ // Zero
+ { value: 0, error: 0, expected: 0 },
+ { value: 0, error: smallErr, expected: [-smallErr, smallErr] },
+ { value: 0, error: 1, expected: [-1, 1] },
+
+ // Two
+ { value: 2, error: 0, expected: 2 },
+ { value: 2, error: smallErr, expected: [2 - smallErr, 2 + smallErr] },
+ { value: 2, error: 1, expected: [1, 3] },
+ { value: -2, error: 0, expected: -2 },
+ { value: -2, error: smallErr, expected: [-2 - smallErr, -2 + smallErr] },
+ { value: -2, error: 1, expected: [-3, -1] },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.absoluteErrorInterval(t.params.value, t.params.error);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.absoluteErrorInterval(${t.params.value}, ${
+ t.params.error
+ }) returned ${got} (${got.begin.toExponential()}, ${got.end.toExponential()}). Expected ${expected}`
+ );
+ });
+
+interface CorrectlyRoundedCase {
+ value: number;
+ expected: number | IntervalBounds;
+}
+
+// Correctly rounded cases that input values are exactly representable normal values of target type
+// prettier-ignore
+const kCorrectlyRoundedNormalCases = {
+ f32: [
+ { value: 0, expected: [0, 0] },
+ { value: reinterpretU32AsF32(0x03800000), expected: reinterpretU32AsF32(0x03800000) },
+ { value: reinterpretU32AsF32(0x03800001), expected: reinterpretU32AsF32(0x03800001) },
+ { value: reinterpretU32AsF32(0x83800000), expected: reinterpretU32AsF32(0x83800000) },
+ { value: reinterpretU32AsF32(0x83800001), expected: reinterpretU32AsF32(0x83800001) },
+ ] as CorrectlyRoundedCase[],
+ f16: [
+ { value: 0, expected: [0, 0] },
+ { value: reinterpretU16AsF16(0x0c00), expected: reinterpretU16AsF16(0x0c00) },
+ { value: reinterpretU16AsF16(0x0c01), expected: reinterpretU16AsF16(0x0c01) },
+ { value: reinterpretU16AsF16(0x8c00), expected: reinterpretU16AsF16(0x8c00) },
+ { value: reinterpretU16AsF16(0x8c01), expected: reinterpretU16AsF16(0x8c01) },
+ ] as CorrectlyRoundedCase[],
+} as const;
+
+// 64-bit normals that fall between two conjunction normal values in target type
+const kCorrectlyRoundedF64NormalCases = [
+ {
+ value: reinterpretU64AsF64(0x3ff0_0000_0000_0001n),
+ expected: {
+ f32: [reinterpretU32AsF32(0x3f800000), reinterpretU32AsF32(0x3f800001)],
+ f16: [reinterpretU16AsF16(0x3c00), reinterpretU16AsF16(0x3c01)],
+ },
+ },
+ {
+ value: reinterpretU64AsF64(0x3ff0_0000_0000_0002n),
+ expected: {
+ f32: [reinterpretU32AsF32(0x3f800000), reinterpretU32AsF32(0x3f800001)],
+ f16: [reinterpretU16AsF16(0x3c00), reinterpretU16AsF16(0x3c01)],
+ },
+ },
+ {
+ value: reinterpretU64AsF64(0x3ff0_0800_0000_0010n),
+ expected: {
+ f32: [reinterpretU32AsF32(0x3f804000), reinterpretU32AsF32(0x3f804001)],
+ f16: [reinterpretU16AsF16(0x3c02), reinterpretU16AsF16(0x3c03)],
+ },
+ },
+ {
+ value: reinterpretU64AsF64(0x3ff0_1000_0000_0020n),
+ expected: {
+ f32: [reinterpretU32AsF32(0x3f808000), reinterpretU32AsF32(0x3f808001)],
+ f16: [reinterpretU16AsF16(0x3c04), reinterpretU16AsF16(0x3c05)],
+ },
+ },
+ {
+ value: reinterpretU64AsF64(0xbff0_0000_0000_0001n),
+ expected: {
+ f32: [reinterpretU32AsF32(0xbf800001), reinterpretU32AsF32(0xbf800000)],
+ f16: [reinterpretU16AsF16(0xbc01), reinterpretU16AsF16(0xbc00)],
+ },
+ },
+ {
+ value: reinterpretU64AsF64(0xbff0_0000_0000_0002n),
+ expected: {
+ f32: [reinterpretU32AsF32(0xbf800001), reinterpretU32AsF32(0xbf800000)],
+ f16: [reinterpretU16AsF16(0xbc01), reinterpretU16AsF16(0xbc00)],
+ },
+ },
+ {
+ value: reinterpretU64AsF64(0xbff0_0800_0000_0010n),
+ expected: {
+ f32: [reinterpretU32AsF32(0xbf804001), reinterpretU32AsF32(0xbf804000)],
+ f16: [reinterpretU16AsF16(0xbc03), reinterpretU16AsF16(0xbc02)],
+ },
+ },
+ {
+ value: reinterpretU64AsF64(0xbff0_1000_0000_0020n),
+ expected: {
+ f32: [reinterpretU32AsF32(0xbf808001), reinterpretU32AsF32(0xbf808000)],
+ f16: [reinterpretU16AsF16(0xbc05), reinterpretU16AsF16(0xbc04)],
+ },
+ },
+] as const;
+
+g.test('correctlyRoundedInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<CorrectlyRoundedCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ return [
+ // Edge Cases
+ { value: constants.positive.infinity, expected: kUnboundedBounds },
+ { value: constants.negative.infinity, expected: kUnboundedBounds },
+ { value: constants.positive.max, expected: constants.positive.max },
+ { value: constants.negative.min, expected: constants.negative.min },
+ { value: constants.positive.min, expected: constants.positive.min },
+ { value: constants.negative.max, expected: constants.negative.max },
+
+ // Subnormals
+ { value: constants.positive.subnormal.min, expected: [0, constants.positive.subnormal.min] },
+ { value: constants.positive.subnormal.max, expected: [0, constants.positive.subnormal.max] },
+ { value: constants.negative.subnormal.min, expected: [constants.negative.subnormal.min, 0] },
+ { value: constants.negative.subnormal.max, expected: [constants.negative.subnormal.max, 0] },
+
+ // 64-bit subnormals should be rounded down to 0 or up to smallest subnormal
+ { value: reinterpretU64AsF64(0x0000_0000_0000_0001n), expected: [0, constants.positive.subnormal.min] },
+ { value: reinterpretU64AsF64(0x0000_0000_0000_0002n), expected: [0, constants.positive.subnormal.min] },
+ { value: reinterpretU64AsF64(0x800f_ffff_ffff_ffffn), expected: [constants.negative.subnormal.max, 0] },
+ { value: reinterpretU64AsF64(0x800f_ffff_ffff_fffen), expected: [constants.negative.subnormal.max, 0] },
+
+ // Normals
+ ...kCorrectlyRoundedNormalCases[p.trait],
+
+ // 64-bit normals that fall between two conjunction normal values in target type
+ ...kCorrectlyRoundedF64NormalCases.map(t => { return {value: t.value, expected: t.expected[p.trait]} as CorrectlyRoundedCase;}),
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.correctlyRoundedInterval(t.params.value);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.correctlyRoundedInterval(${t.params.value}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface ULPCase {
+ value: number;
+ num_ulp: number;
+ expected: number | IntervalBounds;
+}
+
+// Special values used for testing ULP error interval
+const kULPErrorValue = {
+ f32: 4096, // 4096 ULP is required for atan accuracy on f32
+ f16: 5, // 5 ULP is required for atan accuracy on f16
+};
+
+g.test('ulpInterval')
+ .params(u =>
+ u
+ .combine('trait', ['abstract', 'f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ULPCase>(p => {
+ const trait = kFPTraitForULP[p.trait];
+ const constants = FP[trait].constants();
+ const ULPValue = kULPErrorValue[trait];
+ const plusOneULP = kPlusOneULPFunctions[trait];
+ const plusNULP = kPlusNULPFunctions[trait];
+ const minusOneULP = kMinusOneULPFunctions[trait];
+ const minusNULP = kMinusNULPFunctions[trait];
+ // prettier-ignore
+ return [
+ // Edge Cases
+ { value: constants.positive.infinity, num_ulp: 0, expected: kUnboundedBounds },
+ { value: constants.positive.infinity, num_ulp: 1, expected: kUnboundedBounds },
+ { value: constants.positive.infinity, num_ulp: ULPValue, expected: kUnboundedBounds },
+ { value: constants.negative.infinity, num_ulp: 0, expected: kUnboundedBounds },
+ { value: constants.negative.infinity, num_ulp: 1, expected: kUnboundedBounds },
+ { value: constants.negative.infinity, num_ulp: ULPValue, expected: kUnboundedBounds },
+ { value: constants.positive.max, num_ulp: 0, expected: constants.positive.max },
+ { value: constants.positive.max, num_ulp: 1, expected: kUnboundedBounds },
+ { value: constants.positive.max, num_ulp: ULPValue, expected: kUnboundedBounds },
+ { value: constants.positive.min, num_ulp: 0, expected: constants.positive.min },
+ { value: constants.positive.min, num_ulp: 1, expected: [0, plusOneULP(constants.positive.min)] },
+ { value: constants.positive.min, num_ulp: ULPValue, expected: [0, plusNULP(constants.positive.min, ULPValue)] },
+ { value: constants.negative.min, num_ulp: 0, expected: constants.negative.min },
+ { value: constants.negative.min, num_ulp: 1, expected: kUnboundedBounds },
+ { value: constants.negative.min, num_ulp: ULPValue, expected: kUnboundedBounds },
+ { value: constants.negative.max, num_ulp: 0, expected: constants.negative.max },
+ { value: constants.negative.max, num_ulp: 1, expected: [minusOneULP(constants.negative.max), 0] },
+ { value: constants.negative.max, num_ulp: ULPValue, expected: [minusNULP(constants.negative.max, ULPValue), 0] },
+
+ // Subnormals
+ { value: constants.positive.subnormal.max, num_ulp: 0, expected: [0, constants.positive.subnormal.max] },
+ { value: constants.positive.subnormal.max, num_ulp: 1, expected: [minusOneULP(0), plusOneULP(constants.positive.subnormal.max)] },
+ { value: constants.positive.subnormal.max, num_ulp: ULPValue, expected: [minusNULP(0, ULPValue), plusNULP(constants.positive.subnormal.max, ULPValue)] },
+ { value: constants.positive.subnormal.min, num_ulp: 0, expected: [0, constants.positive.subnormal.min] },
+ { value: constants.positive.subnormal.min, num_ulp: 1, expected: [minusOneULP(0), plusOneULP(constants.positive.subnormal.min)] },
+ { value: constants.positive.subnormal.min, num_ulp: ULPValue, expected: [minusNULP(0, ULPValue), plusNULP(constants.positive.subnormal.min, ULPValue)] },
+ { value: constants.negative.subnormal.min, num_ulp: 0, expected: [constants.negative.subnormal.min, 0] },
+ { value: constants.negative.subnormal.min, num_ulp: 1, expected: [minusOneULP(constants.negative.subnormal.min), plusOneULP(0)] },
+ { value: constants.negative.subnormal.min, num_ulp: ULPValue, expected: [minusNULP(constants.negative.subnormal.min, ULPValue), plusNULP(0, ULPValue)] },
+ { value: constants.negative.subnormal.max, num_ulp: 0, expected: [constants.negative.subnormal.max, 0] },
+ { value: constants.negative.subnormal.max, num_ulp: 1, expected: [minusOneULP(constants.negative.subnormal.max), plusOneULP(0)] },
+ { value: constants.negative.subnormal.max, num_ulp: ULPValue, expected: [minusNULP(constants.negative.subnormal.max, ULPValue), plusNULP(0, ULPValue)] },
+
+ // 64-bit subnormals
+ { value: reinterpretU64AsF64(0x0000_0000_0000_0001n), num_ulp: 0, expected: [0, constants.positive.subnormal.min] },
+ { value: reinterpretU64AsF64(0x0000_0000_0000_0001n), num_ulp: 1, expected: [minusOneULP(0), plusOneULP(constants.positive.subnormal.min)] },
+ { value: reinterpretU64AsF64(0x0000_0000_0000_0001n), num_ulp: ULPValue, expected: [minusNULP(0, ULPValue), plusNULP(constants.positive.subnormal.min, ULPValue)] },
+ { value: reinterpretU64AsF64(0x0000_0000_0000_0002n), num_ulp: 0, expected: [0, constants.positive.subnormal.min] },
+ { value: reinterpretU64AsF64(0x0000_0000_0000_0002n), num_ulp: 1, expected: [minusOneULP(0), plusOneULP(constants.positive.subnormal.min)] },
+ { value: reinterpretU64AsF64(0x0000_0000_0000_0002n), num_ulp: ULPValue, expected: [minusNULP(0, ULPValue), plusNULP(constants.positive.subnormal.min, ULPValue)] },
+ { value: reinterpretU64AsF64(0x800f_ffff_ffff_ffffn), num_ulp: 0, expected: [constants.negative.subnormal.max, 0] },
+ { value: reinterpretU64AsF64(0x800f_ffff_ffff_ffffn), num_ulp: 1, expected: [minusOneULP(constants.negative.subnormal.max), plusOneULP(0)] },
+ { value: reinterpretU64AsF64(0x800f_ffff_ffff_ffffn), num_ulp: ULPValue, expected: [minusNULP(constants.negative.subnormal.max, ULPValue), plusNULP(0, ULPValue)] },
+ { value: reinterpretU64AsF64(0x800f_ffff_ffff_fffen), num_ulp: 0, expected: [constants.negative.subnormal.max, 0] },
+ { value: reinterpretU64AsF64(0x800f_ffff_ffff_fffen), num_ulp: 1, expected: [minusOneULP(constants.negative.subnormal.max), plusOneULP(0)] },
+ { value: reinterpretU64AsF64(0x800f_ffff_ffff_fffen), num_ulp: ULPValue, expected: [minusNULP(constants.negative.subnormal.max, ULPValue), plusNULP(0, ULPValue)] },
+
+ // Zero
+ { value: 0, num_ulp: 0, expected: 0 },
+ { value: 0, num_ulp: 1, expected: [minusOneULP(0), plusOneULP(0)] },
+ { value: 0, num_ulp: ULPValue, expected: [minusNULP(0, ULPValue), plusNULP(0, ULPValue)] },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.ulpInterval(t.params.value, t.params.num_ulp);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.ulpInterval(${t.params.value}, ${t.params.num_ulp}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// API - Acceptance Intervals
+// List of frequently used JS number in test cases, which are not exactly representable in f32 or f16.
+type ConstantNumberFrequentlyUsedInCases = '0.1' | '-0.1' | '1.9' | '-1.9';
+
+// Correctly rounded expectation of frequently used JS Number value in test cases
+const kConstantCorrectlyRoundedExpectation = {
+ f32: {
+ // 0.1 falls between f32 0x3DCCCCCC and 0x3DCCCCCD
+ '0.1': [reinterpretU32AsF32(0x3dcccccc), reinterpretU32AsF32(0x3dcccccd)],
+ // -0.1 falls between f32 0xBDCCCCCD and 0xBDCCCCCC
+ '-0.1': [reinterpretU32AsF32(0xbdcccccd), reinterpretU32AsF32(0xbdcccccc)],
+ // 1.9 falls between f32 0x3FF33333 and 0x3FF33334
+ '1.9': [reinterpretU32AsF32(0x3ff33333), reinterpretU32AsF32(0x3ff33334)],
+ // -1.9 falls between f32 0xBFF33334 and 0xBFF33333
+ '-1.9': [reinterpretU32AsF32(0xbff33334), reinterpretU32AsF32(0xbff33333)],
+ } as { [value in ConstantNumberFrequentlyUsedInCases]: IntervalBounds },
+ f16: {
+ // 0.1 falls between f16 0x2E66 and 0x2E67
+ '0.1': [reinterpretU16AsF16(0x2e66), reinterpretU16AsF16(0x2e67)],
+ // -0.1 falls between f16 0xAE67 and 0xAE66
+ '-0.1': [reinterpretU16AsF16(0xae67), reinterpretU16AsF16(0xae66)],
+ // 1.9 falls between f16 0x3F99 and 0x3F9A
+ '1.9': [reinterpretU16AsF16(0x3f99), reinterpretU16AsF16(0x3f9a)],
+ // 1.9 falls between f16 0xBF9A and 0xBF99
+ '-1.9': [reinterpretU16AsF16(0xbf9a), reinterpretU16AsF16(0xbf99)],
+ } as { [value in ConstantNumberFrequentlyUsedInCases]: IntervalBounds },
+ // Since abstract is actually f64 and JS number is also f64, the JS number value will map to
+ // identical abstracty value without rounded.
+ abstract: {
+ '0.1': 0.1,
+ '-0.1': -0.1,
+ '1.9': 1.9,
+ '-1.9': -1.9,
+ } as { [value in ConstantNumberFrequentlyUsedInCases]: number },
+} as const;
+
+interface ScalarToIntervalCase {
+ input: number;
+ expected: number | IntervalBounds;
+}
+
+g.test('absInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ return [
+ // Common usages
+ { input: 1, expected: 1 },
+ { input: -1, expected: 1 },
+ // abs(+/-0.1) is correctly rounded interval of 0.1
+ { input: 0.1, expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1']},
+ { input: -0.1, expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1']},
+ // abs(+/-1.9) is correctly rounded interval of 1.9
+ { input: 1.9, expected: kConstantCorrectlyRoundedExpectation[p.trait]['1.9']},
+ { input: -1.9, expected: kConstantCorrectlyRoundedExpectation[p.trait]['1.9']},
+
+ // Edge cases
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.positive.max, expected: constants.positive.max },
+ { input: constants.positive.min, expected: constants.positive.min },
+ { input: constants.negative.min, expected: constants.positive.max },
+ { input: constants.negative.max, expected: constants.positive.min },
+
+ // Subnormals
+ { input: constants.positive.subnormal.max, expected: [0, constants.positive.subnormal.max] },
+ { input: constants.positive.subnormal.min, expected: [0, constants.positive.subnormal.min] },
+ { input: constants.negative.subnormal.min, expected: [0, constants.positive.subnormal.max] },
+ { input: constants.negative.subnormal.max, expected: [0, constants.positive.subnormal.min] },
+
+ // Zero
+ { input: 0, expected: 0 },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.absInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.absInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Acos expectation intervals are bounded by both inherited atan2(sqrt(1.0 - x*x), x) and absolute error.
+// Atan2 introduce 4096ULP for f32 and 5ULP for f16, and sqrt inherited from 1.0/inverseSqrt.
+// prettier-ignore
+const kAcosIntervalCases = {
+ f32: [
+ { input: kPlusOneULPFunctions['f32'](-1), expected: [reinterpretU32AsF32(0x4048fa32), reinterpretU32AsF32(0x40491bdb)] }, // ~π
+ { input: -1/2, expected: [reinterpretU32AsF32(0x4005fa90), reinterpretU32AsF32(0x40061a93)] }, // ~2π/3
+ { input: 1/2, expected: [reinterpretU32AsF32(0x3f85fa8f), reinterpretU32AsF32(0x3f861a94)] }, // ~π/3
+ // Input case to get smallest well-defined expected result, the expectation interval is bounded
+ // by ULP (lower boundary) and absolute error (upper boundary).
+ // f32 1.0-1ULP=0x3F7FFFFF=0.9999999403953552,
+ // acos(0.9999999403953552)=3.4526698478747995220159699019994e-4 rounded to f32 0x39B504F3 or 0x39B504F4,
+ // absolute error interval upper boundary 0x39B504F4+6.77e-5=0.00041296700619608164 i.e. f64 0x3F3B_106F_C933_4FB9.
+ { input: kMinusOneULPFunctions['f32'](1), expected: [reinterpretU64AsF64(0x3f2f_fdff_6000_0000n), reinterpretU64AsF64(0x3f3b_106f_c933_4fb9n)] }, // ~0.0003
+ ] as ScalarToIntervalCase[],
+ f16: [
+ { input: kPlusOneULPFunctions['f16'](-1), expected: [reinterpretU16AsF16(0x4233), reinterpretU16AsF16(0x4243)] }, // ~π
+ { input: -1/2, expected: [reinterpretU16AsF16(0x402a), reinterpretU16AsF16(0x4037)] }, // ~2π/3
+ { input: 1/2, expected: [reinterpretU16AsF16(0x3c29), reinterpretU16AsF16(0x3c38)] }, // ~π/3
+ // Input case to get smallest well-defined expected result, the expectation interval is bounded
+ // by ULP (lower boundary) and absolute error (upper boundary).
+ // f16 1.0-1ULP=0x3BFF=0.99951171875,
+ // acos(0.99951171875)=0.03125127170547389912035676677648 rounded to f16 0x2800 or 0x2801,
+ // absolute error interval upper boundary 0x2801+3.91e-3=0.035190517578125 i.e. f64 0x3FA2_047D_D441_3554.
+ { input: kMinusOneULPFunctions['f16'](1), expected: [reinterpretU16AsF16(0x259d), reinterpretU64AsF64(0x3fa2_047d_d441_3554n)] }, // ~0.03
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('acosInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // The acceptance interval @ x = -1 and 1 is kUnboundedBounds, because
+ // sqrt(1 - x*x) = sqrt(0), and sqrt is defined in terms of inverseqrt
+ // The acceptance interval @ x = 0 is kUnboundedBounds, because atan2 is not
+ // well-defined/implemented at 0.
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.min, expected: kUnboundedBounds },
+ { input: -1, expected: kUnboundedBounds },
+ { input: 0, expected: kUnboundedBounds },
+ { input: 1, expected: kUnboundedBounds },
+ { input: constants.positive.max, expected: kUnboundedBounds },
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+
+ // Cases that bounded by absolute error and inherited from atan2(sqrt(1-x*x), x). Note that
+ // even x is very close to 1.0 and the expected result is close to 0.0, the expected
+ // interval is still bounded by ULP as well as absolute error, specifically lower boundary
+ // comes from ULP error and upper boundary comes from absolute error in those cases.
+ ...kAcosIntervalCases[p.trait],
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.acosInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.acosInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Some of these are hard coded, since the error intervals are difficult to express in a closed
+// human-readable form due to the inherited nature of the errors.
+// prettier-ignore
+const kAcoshAlternativeIntervalCases = {
+ f32: [
+ { input: 1.1, expected: [reinterpretU64AsF64(0x3fdc_6368_8000_0000n), reinterpretU64AsF64(0x3fdc_636f_2000_0000n)] }, // ~0.443..., differs from the primary in the later digits
+ { input: 10, expected: [reinterpretU64AsF64(0x4007_f21e_4000_0000n), reinterpretU64AsF64(0x4007_f21f_6000_0000n)] }, // ~2.993...
+ ] as ScalarToIntervalCase[],
+ f16: [
+ { input: 1.1, expected: [reinterpretU64AsF64(0x3fdb_bc00_0000_0000n), reinterpretU64AsF64(0x3fdd_1000_0000_0000n)] }, // ~0.443..., differs from the primary in the later digits
+ { input: 10, expected: [reinterpretU64AsF64(0x4007_e000_0000_0000n), reinterpretU64AsF64(0x4008_0400_0000_0000n)] }, // ~2.993...
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('acoshAlternativeInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ ...kAcoshAlternativeIntervalCases[p.trait],
+
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.min, expected: kUnboundedBounds },
+ { input: -1, expected: kUnboundedBounds },
+ { input: 0, expected: kUnboundedBounds },
+ { input: 1, expected: kUnboundedBounds }, // 1/0 occurs in inverseSqrt in this formulation
+ { input: constants.positive.max, expected: kUnboundedBounds },
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.acoshAlternativeInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.acoshAlternativeInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Some of these are hard coded, since the error intervals are difficult to express in a closed
+// human-readable form due to the inherited nature of the errors.
+// prettier-ignore
+const kAcoshPrimaryIntervalCases = {
+ f32: [
+ { input: 1.1, expected: [reinterpretU64AsF64(0x3fdc_6368_2000_0000n), reinterpretU64AsF64(0x3fdc_636f_8000_0000n)] }, // ~0.443..., differs from the alternative in the later digits
+ { input: 10, expected: [reinterpretU64AsF64(0x4007_f21e_4000_0000n), reinterpretU64AsF64(0x4007_f21f_6000_0000n)] }, // ~2.993...
+ ] as ScalarToIntervalCase[],
+ f16: [
+ { input: 1.1, expected: [reinterpretU64AsF64(0x3fdb_bc00_0000_0000n), reinterpretU64AsF64(0x3fdd_1c00_0000_0000n)] }, // ~0.443..., differs from the primary in the later digits
+ { input: 10, expected: [reinterpretU64AsF64(0x4007_e000_0000_0000n), reinterpretU64AsF64(0x4008_0400_0000_0000n)] }, // ~2.993...
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('acoshPrimaryInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ ...kAcoshPrimaryIntervalCases[p.trait],
+
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.min, expected: kUnboundedBounds },
+ { input: -1, expected: kUnboundedBounds },
+ { input: 0, expected: kUnboundedBounds },
+ { input: 1, expected: kUnboundedBounds }, // 1/0 occurs in inverseSqrt in this formulation
+ { input: constants.positive.max, expected: kUnboundedBounds },
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.acoshPrimaryInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.acoshPrimaryInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Asin cases that bounded by inherited atan2(x, sqrt(1.0 - x*x)) rather than absolute error.
+// Atan2 introduce 4096ULP for f32 and 5ULP for f16, and sqrt inherited from 1.0/inverseSqrt.
+// prettier-ignore
+const kAsinIntervalInheritedCases = {
+ f32: [
+ { input: -1/2, expected: [reinterpretU32AsF32(0xbf061a96), reinterpretU32AsF32(0xbf05fa8e)] }, // ~-π/6
+ { input: 1/2, expected: [reinterpretU32AsF32(0x3f05fa8e), reinterpretU32AsF32(0x3f061a96)] }, // ~π/6
+ ] as ScalarToIntervalCase[],
+ f16: [
+ { input: -1/2, expected: [reinterpretU16AsF16(0xb83a), reinterpretU16AsF16(0xb827)] }, // ~-π/6
+ { input: 1/2, expected: [reinterpretU16AsF16(0x3827), reinterpretU16AsF16(0x383a)] }, // ~π/6
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('asinInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ const abs_error = p.trait === 'f32' ? 6.77e-5 : 3.91e-3;
+ // prettier-ignore
+ return [
+ // The acceptance interval @ x = -1 and 1 is kUnboundedBounds, because
+ // sqrt(1 - x*x) = sqrt(0), and sqrt is defined in terms of inversqrt.
+ // The acceptance interval @ x = 0 is kUnboundedBounds, because atan2 is not
+ // well-defined/implemented at 0.
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.min, expected: kUnboundedBounds },
+ { input: -1, expected: kUnboundedBounds },
+ // Subnormal input may get flushed to 0, and result in kUnboundedBounds.
+ { input: constants.negative.subnormal.min, expected: kUnboundedBounds },
+ { input: 0, expected: kUnboundedBounds },
+ { input: constants.positive.subnormal.max, expected: kUnboundedBounds },
+ { input: 1, expected: kUnboundedBounds },
+ { input: constants.positive.max, expected: kUnboundedBounds },
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+
+ // When input near 0, the expected result is bounded by absolute error rather than ULP
+ // error. Away from 0 the atan2 inherited error should be larger.
+ { input: constants.negative.max, expected: trait.absoluteErrorInterval(Math.asin(constants.negative.max), abs_error).bounds() }, // ~0
+ { input: constants.positive.min, expected: trait.absoluteErrorInterval(Math.asin(constants.positive.min), abs_error).bounds() }, // ~0
+
+ // Cases that inherited from atan2(x, sqrt(1-x*x))
+ ...kAsinIntervalInheritedCases[p.trait],
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.asinInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.asinInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Some of these are hard coded, since the error intervals are difficult to express in a closed
+// human-readable form due to the inherited nature of the errors.
+// prettier-ignore
+const kAsinhIntervalCases = {
+ f32: [
+ { input: -1, expected: [reinterpretU64AsF64(0xbfec_343a_8000_0000n), reinterpretU64AsF64(0xbfec_3432_8000_0000n)] }, // ~-0.88137...
+ { input: 0, expected: [reinterpretU64AsF64(0xbeaa_0000_2000_0000n), reinterpretU64AsF64(0x3eb1_ffff_d000_0000n)] }, // ~0
+ { input: 1, expected: [reinterpretU64AsF64(0x3fec_3435_4000_0000n), reinterpretU64AsF64(0x3fec_3437_8000_0000n)] }, // ~0.88137...
+ ] as ScalarToIntervalCase[],
+ f16: [
+ { input: -1, expected: [reinterpretU64AsF64(0xbfec_b800_0000_0000n), reinterpretU64AsF64(0xbfeb_b800_0000_0000n)] }, // ~-0.88137...
+ { input: 0, expected: [reinterpretU64AsF64(0xbf85_0200_0000_0000n), reinterpretU64AsF64(0x3f89_fa00_0000_0000n)] }, // ~0
+ { input: 1, expected: [reinterpretU64AsF64(0x3fec_1000_0000_0000n), reinterpretU64AsF64(0x3fec_5400_0000_0000n)] }, // ~0.88137...
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('asinhInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ ...kAsinhIntervalCases[p.trait],
+
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.min, expected: kUnboundedBounds },
+ { input: constants.positive.max, expected: kUnboundedBounds },
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.asinhInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.asinhInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// prettier-ignore
+const kAtanIntervalCases = {
+ f32: [
+ // x=-√3=-1.7320508... quantized to f32 0xBFDDB3D7,
+ // atan(0xBFDDB3D7)=-1.0471975434247854181546378047331 ~ -pi/3 rounded to f32 0xBF860A92 or 0xBF860A91,
+ // kValue.f32.negative.pi.third is 0xBF860A92.
+ { input: reinterpretU32AsF32(0xbfddb3d7), expected: [kValue.f32.negative.pi.third, kPlusOneULPFunctions['f32'](kValue.f32.negative.pi.third)] },
+ // atan(-1)=-0.78539816339744830961566084581988 ~ -pi/4 rounded to f32 0xBF490FDB or 0xBF490FDA,
+ // kValue.f32.negative.pi.quarter is 0xBF490FDB.
+ { input: -1, expected: [kValue.f32.negative.pi.quarter, kPlusOneULPFunctions['f32'](kValue.f32.negative.pi.quarter)] },
+ // x=-1/√3=-0.577350269... quantized to f32 0xBF13CD3A,
+ // atan(0xBF13CD3A)=-0.52359876782648663982267459646249 ~ -pi/6 rounded to f32 0xBF060A92 or 0xBF060A91,
+ // kValue.f32.negative.pi.sixth is 0xBF060A92.
+ { input: reinterpretU32AsF32(0xbf13cd3a), expected: [kValue.f32.negative.pi.sixth, kPlusOneULPFunctions['f32'](kValue.f32.negative.pi.sixth)] },
+ // x=1/√3=0.577350269... quantized to f32 0x3F13CD3A.
+ { input: reinterpretU32AsF32(0x3f13cd3a), expected: [kMinusOneULPFunctions['f32'](kValue.f32.positive.pi.sixth), kValue.f32.positive.pi.sixth] },
+ { input: 1, expected: [kMinusOneULPFunctions['f32'](kValue.f32.positive.pi.quarter), kValue.f32.positive.pi.quarter] },
+ // x=√3=1.7320508... quantized to f32 0x3FDDB3D7.
+ { input: reinterpretU32AsF32(0x3fddb3d7), expected: [kMinusOneULPFunctions['f32'](kValue.f32.positive.pi.third), kValue.f32.positive.pi.third] },
+ ] as ScalarToIntervalCase[],
+ f16: [
+ // x=-√3=-1.7320508... quantized to f16 0xBEED,
+ // atan(0xBEED)=-1.0470461377318847079113932677171 ~ -pi/3 rounded to f16 0xBC31 or 0xBC30,
+ // kValue.f16.negative.pi.third is 0xBC30.
+ { input: reinterpretU16AsF16(0xbeed), expected: [kMinusOneULPFunctions['f16'](kValue.f16.negative.pi.third), kValue.f16.negative.pi.third] },
+ // atan(-1)=-0.78539816339744830961566084581988 ~ -pi/4 rounded to f16 0xBA49 or 0xBA48.
+ // kValue.f16.negative.pi.quarter is 0xBA48.
+ { input: -1, expected: [kMinusOneULPFunctions['f16'](kValue.f16.negative.pi.quarter), kValue.f16.negative.pi.quarter] },
+ // x=-1/√3=-0.577350269... quantized to f16 0xB89E,
+ // atan(0xB89E)=-0.52344738860166563645762619364966 ~ -pi/6 rounded to f16 0xB831 or 0xB830,
+ // kValue.f16.negative.pi.sixth is 0xB830.
+ { input: reinterpretU16AsF16(0xb89e), expected: [kMinusOneULPFunctions['f16'](kValue.f16.negative.pi.sixth), kValue.f16.negative.pi.sixth] },
+ // x=1/√3=0.577350269... quantized to f16 0x389E
+ { input: reinterpretU16AsF16(0x389e), expected: [kValue.f16.positive.pi.sixth, kPlusOneULPFunctions['f16'](kValue.f16.positive.pi.sixth)] },
+ { input: 1, expected: [kValue.f16.positive.pi.quarter, kPlusOneULPFunctions['f16'](kValue.f16.positive.pi.quarter)] },
+ // x=√3=1.7320508... quantized to f16 0x3EED
+ { input: reinterpretU16AsF16(0x3eed), expected: [kValue.f16.positive.pi.third, kPlusOneULPFunctions['f16'](kValue.f16.positive.pi.third)] },
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('atanInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ return [
+ { input: 0, expected: 0 },
+ ...kAtanIntervalCases[p.trait],
+
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+
+ const ulp_error = t.params.trait === 'f32' ? 4096 : 5;
+ const error = (n: number): number => {
+ return ulp_error * trait.oneULP(n);
+ };
+
+ const expected = trait.toInterval(applyError(t.params.expected, error));
+
+ const got = trait.atanInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.atanInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Some of these are hard coded, since the error intervals are difficult to express in a closed
+// human-readable form due to the inherited nature of the errors.
+// prettier-ignore
+const kAtanhIntervalCases = {
+ f32: [
+ { input: -0.1, expected: [reinterpretU64AsF64(0xbfb9_af9a_6000_0000n), reinterpretU64AsF64(0xbfb9_af8c_c000_0000n)] }, // ~-0.1003...
+ { input: 0, expected: [reinterpretU64AsF64(0xbe96_0000_2000_0000n), reinterpretU64AsF64(0x3e98_0000_0000_0000n)] }, // ~0
+ { input: 0.1, expected: [reinterpretU64AsF64(0x3fb9_af8b_8000_0000n), reinterpretU64AsF64(0x3fb9_af9b_0000_0000n)] }, // ~0.1003...
+ ] as ScalarToIntervalCase[],
+ f16: [
+ { input: -0.1, expected: [reinterpretU64AsF64(0xbfbb_0c00_0000_0000n), reinterpretU64AsF64(0xbfb8_5800_0000_0000n)] }, // ~-0.1003...
+ { input: 0, expected: [reinterpretU64AsF64(0xbf73_0400_0000_0000n), reinterpretU64AsF64(0x3f74_0000_0000_0000n)] }, // ~0
+ { input: 0.1, expected: [reinterpretU64AsF64(0x3fb8_3800_0000_0000n), reinterpretU64AsF64(0x3fbb_2400_0000_0000n)] }, // ~0.1003...
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('atanhInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ ...kAtanhIntervalCases[p.trait],
+
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.min, expected: kUnboundedBounds },
+ { input: -1, expected: kUnboundedBounds },
+ { input: 1, expected: kUnboundedBounds },
+ { input: constants.positive.max, expected: kUnboundedBounds },
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.atanhInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.atanhInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Large but still representable integer
+const kCeilIntervalCases = {
+ f32: [
+ { input: 2 ** 30, expected: 2 ** 30 },
+ { input: -(2 ** 30), expected: -(2 ** 30) },
+ { input: 0x80000000, expected: 0x80000000 }, // https://github.com/gpuweb/cts/issues/2766
+ ],
+ f16: [
+ { input: 2 ** 14, expected: 2 ** 14 },
+ { input: -(2 ** 14), expected: -(2 ** 14) },
+ { input: 0x8000, expected: 0x8000 }, // https://github.com/gpuweb/cts/issues/2766
+ ],
+} as const;
+
+g.test('ceilInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ return [
+ { input: 0, expected: 0 },
+ { input: 0.1, expected: 1 },
+ { input: 0.9, expected: 1 },
+ { input: 1.0, expected: 1 },
+ { input: 1.1, expected: 2 },
+ { input: 1.9, expected: 2 },
+ { input: -0.1, expected: 0 },
+ { input: -0.9, expected: 0 },
+ { input: -1.0, expected: -1 },
+ { input: -1.1, expected: -1 },
+ { input: -1.9, expected: -1 },
+
+ // Edge cases
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.positive.max, expected: constants.positive.max },
+ { input: constants.positive.min, expected: 1 },
+ { input: constants.negative.min, expected: constants.negative.min },
+ { input: constants.negative.max, expected: 0 },
+ ...kCeilIntervalCases[p.trait],
+
+ // 32-bit subnormals
+ { input: constants.positive.subnormal.max, expected: [0, 1] },
+ { input: constants.positive.subnormal.min, expected: [0, 1] },
+ { input: constants.negative.subnormal.min, expected: 0 },
+ { input: constants.negative.subnormal.max, expected: 0 },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.ceilInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.ceilInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Cos interval cases on x=π/3, the result of f32 and f16 is different because π/3 quantized to
+// different direction for two types.
+const kCosIntervalThirdPiCases = {
+ // prettier-ignore
+ f32: [
+ // cos(-1.0471975803375244) = 0.499999974763
+ { input: kValue.f32.negative.pi.third, expected: [kMinusOneULPFunctions['f32'](1/2), 1/2] },
+ // cos(1.0471975803375244) = 0.499999974763
+ { input: kValue.f32.positive.pi.third, expected: [kMinusOneULPFunctions['f32'](1/2), 1/2] },
+ ],
+ f16: [
+ // cos(-1.046875) = 0.50027931
+ {
+ input: kValue.f16.negative.pi.third,
+ expected: FP['f16'].correctlyRoundedInterval(0.50027931).bounds(),
+ },
+ // cos(1.046875) = 0.50027931
+ {
+ input: kValue.f16.positive.pi.third,
+ expected: FP['f16'].correctlyRoundedInterval(0.50027931).bounds(),
+ },
+ ],
+};
+
+g.test('cosInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // This test does not include some common cases. i.e. f(x = π/2) = 0,
+ // because the difference between true x and x as a f32 is sufficiently
+ // large, such that the high slope of f @ x causes the results to be
+ // substantially different, so instead of getting 0 you get a value on the
+ // order of 10^-8 away from 0, thus difficult to express in a
+ // human-readable manner.
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.min, expected: kUnboundedBounds },
+ { input: constants.negative.pi.whole, expected: [-1, kPlusOneULPFunctions[p.trait](-1)] },
+ { input: 0, expected: [1, 1] },
+ { input: constants.positive.pi.whole, expected: [-1, kPlusOneULPFunctions[p.trait](-1)] },
+ { input: constants.positive.max, expected: kUnboundedBounds },
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+
+ ...(kCosIntervalThirdPiCases[p.trait] as ScalarToIntervalCase[]),
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+
+ const error = (_: number): number => {
+ return t.params.trait === 'f32' ? 2 ** -11 : 2 ** -7;
+ };
+
+ const expected = trait.toInterval(applyError(t.params.expected, error));
+
+ const got = trait.cosInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.cosInterval(${t.params.input}) returned ${got}. Expected ${expected}, ===${t.params.expected}===`
+ );
+ });
+
+// Some of these are hard coded, since the error intervals are difficult to express in a closed
+// human-readable form due to the inherited nature of the errors.
+// prettier-ignore
+const kCoshIntervalCases = {
+ f32: [
+ { input: -1, expected: [reinterpretU32AsF32(0x3fc583a4), reinterpretU32AsF32(0x3fc583b1)] }, // ~1.1543...
+ { input: 0, expected: [reinterpretU32AsF32(0x3f7ffffd), reinterpretU32AsF32(0x3f800002)] }, // ~1
+ { input: 1, expected: [reinterpretU32AsF32(0x3fc583a4), reinterpretU32AsF32(0x3fc583b1)] }, // ~1.1543...
+ ] as ScalarToIntervalCase[],
+ f16: [
+ { input: -1, expected: [reinterpretU16AsF16(0x3e27), reinterpretU16AsF16(0x3e30)] }, // ~1.1543...
+ { input: 0, expected: [reinterpretU16AsF16(0x3bff), reinterpretU16AsF16(0x3c01)] }, // ~1
+ { input: 1, expected: [reinterpretU16AsF16(0x3e27), reinterpretU16AsF16(0x3e30)] }, // ~1.1543...
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('coshInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ ...kCoshIntervalCases[p.trait],
+
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.min, expected: kUnboundedBounds },
+ { input: constants.positive.max, expected: kUnboundedBounds },
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.coshInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.coshInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// prettier-ignore
+const kDegreesIntervalCases = {
+ f32: [
+ { input: kValue.f32.negative.pi.whole, expected: [kMinusOneULPFunctions['f32'](-180), kPlusOneULPFunctions['f32'](-180)] },
+ { input: kValue.f32.negative.pi.three_quarters, expected: [kMinusOneULPFunctions['f32'](-135), kPlusOneULPFunctions['f32'](-135)] },
+ { input: kValue.f32.negative.pi.half, expected: [kMinusOneULPFunctions['f32'](-90), kPlusOneULPFunctions['f32'](-90)] },
+ { input: kValue.f32.negative.pi.third, expected: [kMinusOneULPFunctions['f32'](-60), kPlusOneULPFunctions['f32'](-60)] },
+ { input: kValue.f32.negative.pi.quarter, expected: [kMinusOneULPFunctions['f32'](-45), kPlusOneULPFunctions['f32'](-45)] },
+ { input: kValue.f32.negative.pi.sixth, expected: [kMinusOneULPFunctions['f32'](-30), kPlusOneULPFunctions['f32'](-30)] },
+ { input: kValue.f32.positive.pi.sixth, expected: [kMinusOneULPFunctions['f32'](30), kPlusOneULPFunctions['f32'](30)] },
+ { input: kValue.f32.positive.pi.quarter, expected: [kMinusOneULPFunctions['f32'](45), kPlusOneULPFunctions['f32'](45)] },
+ { input: kValue.f32.positive.pi.third, expected: [kMinusOneULPFunctions['f32'](60), kPlusOneULPFunctions['f32'](60)] },
+ { input: kValue.f32.positive.pi.half, expected: [kMinusOneULPFunctions['f32'](90), kPlusOneULPFunctions['f32'](90)] },
+ { input: kValue.f32.positive.pi.three_quarters, expected: [kMinusOneULPFunctions['f32'](135), kPlusOneULPFunctions['f32'](135)] },
+ { input: kValue.f32.positive.pi.whole, expected: [kMinusOneULPFunctions['f32'](180), kPlusOneULPFunctions['f32'](180)] },
+ ] as ScalarToIntervalCase[],
+ f16: [
+ { input: kValue.f16.negative.pi.whole, expected: [-180, kPlusOneULPFunctions['f16'](-180)] },
+ { input: kValue.f16.negative.pi.three_quarters, expected: [-135, kPlusOneULPFunctions['f16'](-135)] },
+ { input: kValue.f16.negative.pi.half, expected: [-90, kPlusOneULPFunctions['f16'](-90)] },
+ { input: kValue.f16.negative.pi.third, expected: [-60, kPlusNULPFunctions['f16'](-60, 2)] },
+ { input: kValue.f16.negative.pi.quarter, expected: [-45, kPlusOneULPFunctions['f16'](-45)] },
+ { input: kValue.f16.negative.pi.sixth, expected: [-30, kPlusNULPFunctions['f16'](-30, 2)] },
+ { input: kValue.f16.positive.pi.sixth, expected: [kMinusNULPFunctions['f16'](30, 2), 30] },
+ { input: kValue.f16.positive.pi.quarter, expected: [kMinusOneULPFunctions['f16'](45), 45] },
+ { input: kValue.f16.positive.pi.third, expected: [kMinusNULPFunctions['f16'](60, 2), 60] },
+ { input: kValue.f16.positive.pi.half, expected: [kMinusOneULPFunctions['f16'](90), 90] },
+ { input: kValue.f16.positive.pi.three_quarters, expected: [kMinusOneULPFunctions['f16'](135), 135] },
+ { input: kValue.f16.positive.pi.whole, expected: [kMinusOneULPFunctions['f16'](180), 180] },
+ ] as ScalarToIntervalCase[],
+ abstract: [
+ { input: kValue.f64.negative.pi.whole, expected: -180 },
+ { input: kValue.f64.negative.pi.three_quarters, expected: -135 },
+ { input: kValue.f64.negative.pi.half, expected: -90 },
+ { input: kValue.f64.negative.pi.third, expected: kPlusOneULPFunctions['abstract'](-60) },
+ { input: kValue.f64.negative.pi.quarter, expected: -45 },
+ { input: kValue.f64.negative.pi.sixth, expected: kPlusOneULPFunctions['abstract'](-30) },
+ { input: kValue.f64.positive.pi.sixth, expected: kMinusOneULPFunctions['abstract'](30) },
+ { input: kValue.f64.positive.pi.quarter, expected: 45 },
+ { input: kValue.f64.positive.pi.third, expected: kMinusOneULPFunctions['abstract'](60) },
+ { input: kValue.f64.positive.pi.half, expected: 90 },
+ { input: kValue.f64.positive.pi.three_quarters, expected: 135 },
+ { input: kValue.f64.positive.pi.whole, expected: 180 },
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('degreesInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = p.trait;
+ const constants = FP[trait].constants();
+ // prettier-ignore
+ return [
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.min, expected: kUnboundedBounds },
+ { input: 0, expected: 0 },
+ { input: constants.positive.max, expected: kUnboundedBounds },
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ ...kDegreesIntervalCases[trait]
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.degreesInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.degreesInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// prettier-ignore
+const kExpIntervalCases = {
+ f32: [
+ { input: 1, expected: [kValue.f32.positive.e, kPlusOneULPFunctions['f32'](kValue.f32.positive.e)] },
+ // exp(88) = 1.6516362549940018555283297962649e+38 = 0x7ef882b6/7.
+ { input: 88, expected: [reinterpretU32AsF32(0x7ef882b6), reinterpretU32AsF32(0x7ef882b7)] },
+ // exp(89) overflow f32.
+ { input: 89, expected: kUnboundedBounds },
+ ] as ScalarToIntervalCase[],
+ f16: [
+ { input: 1, expected: [kValue.f16.positive.e, kPlusOneULPFunctions['f16'](kValue.f16.positive.e)] },
+ // exp(11) = 59874.141715197818455326485792258 = 0x7b4f/0x7b50.
+ { input: 11, expected: [reinterpretU16AsF16(0x7b4f), reinterpretU16AsF16(0x7b50)] },
+ // exp(12) = 162754.79141900392080800520489849 overflow f16.
+ { input: 12, expected: kUnboundedBounds },
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('expInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = p.trait;
+ const constants = FP[trait].constants();
+ // prettier-ignore
+ return [
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: 0, expected: 1 },
+ ...kExpIntervalCases[trait],
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const error = (x: number): number => {
+ let ulp_error;
+ switch (t.params.trait) {
+ case 'f32': {
+ ulp_error = 3 + 2 * Math.abs(t.params.input);
+ break;
+ }
+ case 'f16': {
+ ulp_error = 1 + 2 * Math.abs(t.params.input);
+ break;
+ }
+ }
+ return ulp_error * trait.oneULP(x);
+ };
+
+ const expected = trait.toInterval(applyError(t.params.expected, error));
+ const got = trait.expInterval(t.params.input);
+
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.expInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// prettier-ignore
+const kExp2IntervalCases = {
+ f32: [
+ // exp2(127) = 1.7014118346046923173168730371588e+38 = 0x7f000000, 3 + 2 * 127 = 258 ulps.
+ { input: 127, expected: reinterpretU32AsF32(0x7f000000) },
+ // exp2(128) overflow f32.
+ { input: 128, expected: kUnboundedBounds },
+ ] as ScalarToIntervalCase[],
+ f16: [
+ // exp2(15) = 32768 = 0x7800, 1 + 2 * 15 = 31 ulps
+ { input: 15, expected: reinterpretU16AsF16(0x7800) },
+ // exp2(16) = 65536 overflow f16.
+ { input: 16, expected: kUnboundedBounds },
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('exp2Interval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = p.trait;
+ const constants = FP[trait].constants();
+ // prettier-ignore
+ return [
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: 0, expected: 1 },
+ { input: 1, expected: 2 },
+ ...kExp2IntervalCases[trait],
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const error = (x: number): number => {
+ let ulp_error;
+ switch (t.params.trait) {
+ case 'f32': {
+ ulp_error = 3 + 2 * Math.abs(t.params.input);
+ break;
+ }
+ case 'f16': {
+ ulp_error = 1 + 2 * Math.abs(t.params.input);
+ break;
+ }
+ }
+ return ulp_error * trait.oneULP(x);
+ };
+
+ const expected = trait.toInterval(applyError(t.params.expected, error));
+
+ const got = trait.exp2Interval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.exp2Interval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Large but still representable integer
+const kFloorIntervalCases = {
+ f32: [
+ { input: 2 ** 30, expected: 2 ** 30 },
+ { input: -(2 ** 30), expected: -(2 ** 30) },
+ { input: 0x80000000, expected: 0x80000000 }, // https://github.com/gpuweb/cts/issues/2766
+ ],
+ f16: [
+ { input: 2 ** 14, expected: 2 ** 14 },
+ { input: -(2 ** 14), expected: -(2 ** 14) },
+ { input: 0x8000, expected: 0x8000 }, // https://github.com/gpuweb/cts/issues/2766
+ ],
+ abstract: [
+ { input: 2 ** 62, expected: 2 ** 62 },
+ { input: -(2 ** 62), expected: -(2 ** 62) },
+ {
+ input: 0x8000_0000_0000_0000,
+ expected: 0x8000_0000_0000_0000,
+ }, // https://github.com/gpuweb/cts/issues/2766
+ ],
+} as const;
+
+g.test('floorInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ return [
+ { input: 0, expected: 0 },
+ { input: 0.1, expected: 0 },
+ { input: 0.9, expected: 0 },
+ { input: 1.0, expected: 1 },
+ { input: 1.1, expected: 1 },
+ { input: 1.9, expected: 1 },
+ { input: -0.1, expected: -1 },
+ { input: -0.9, expected: -1 },
+ { input: -1.0, expected: -1 },
+ { input: -1.1, expected: -2 },
+ { input: -1.9, expected: -2 },
+
+ // Edge cases
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.positive.max, expected: constants.positive.max },
+ { input: constants.positive.min, expected: 0 },
+ { input: constants.negative.min, expected: constants.negative.min },
+ { input: constants.negative.max, expected: -1 },
+ ...kFloorIntervalCases[p.trait],
+
+ // Subnormals
+ { input: constants.positive.subnormal.max, expected: 0 },
+ { input: constants.positive.subnormal.min, expected: 0 },
+ { input: constants.negative.subnormal.min, expected: [-1, 0] },
+ { input: constants.negative.subnormal.max, expected: [-1, 0] },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.floorInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.floorInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// prettier-ignore
+const kFractIntervalCases = {
+ f32: [
+ { input: 0.1, expected: [kMinusOneULPFunctions['f32'](reinterpretU32AsF32(0x3dcccccd)), reinterpretU32AsF32(0x3dcccccd)] }, // ~0.1
+ { input: 0.9, expected: [reinterpretU32AsF32(0x3f666666), kPlusOneULPFunctions['f32'](reinterpretU32AsF32(0x3f666666))] }, // ~0.9
+ { input: 1.1, expected: [reinterpretU32AsF32(0x3dccccc0), reinterpretU32AsF32(0x3dccccd0)] }, // ~0.1
+ { input: -0.1, expected: [reinterpretU32AsF32(0x3f666666), kPlusOneULPFunctions['f32'](reinterpretU32AsF32(0x3f666666))] }, // ~0.9
+ { input: -0.9, expected: [reinterpretU32AsF32(0x3dccccc8), reinterpretU32AsF32(0x3dccccd0)] }, // ~0.1
+ { input: -1.1, expected: [reinterpretU32AsF32(0x3f666666), reinterpretU32AsF32(0x3f666668)] }, // ~0.9
+
+ // https://github.com/gpuweb/cts/issues/2766
+ { input: 0x80000000, expected: 0 },
+ ] as ScalarToIntervalCase[],
+ f16: [
+ { input: 0.1, expected: [reinterpretU16AsF16(0x2e66), reinterpretU16AsF16(0x2e67)] }, // ~0.1
+ { input: 0.9, expected: [reinterpretU16AsF16(0x3b33), reinterpretU16AsF16(0x3b34)] }, // ~0.9
+ { input: 1.1, expected: [reinterpretU16AsF16(0x2e60), reinterpretU16AsF16(0x2e70)] }, // ~0.1
+ { input: -0.1, expected: [reinterpretU16AsF16(0x3b33), reinterpretU16AsF16(0x3b34)] }, // ~0.9
+ { input: -0.9, expected: [reinterpretU16AsF16(0x2e60), reinterpretU16AsF16(0x2e68)] }, // ~0.1
+ { input: -1.1, expected: [reinterpretU16AsF16(0x3b32), reinterpretU16AsF16(0x3b34)] }, // ~0.9
+ { input: 658.5, expected: 0.5 },
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('fractInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ return [
+ { input: 0, expected: 0 },
+ { input: 1.0, expected: 0 },
+ { input: -1.0, expected: 0 },
+
+ ...kFractIntervalCases[p.trait],
+
+ // Edge cases
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.positive.max, expected: 0 },
+ { input: constants.positive.min, expected: constants.positive.min },
+ { input: constants.negative.min, expected: 0 },
+ { input: constants.negative.max, expected: [constants.positive.less_than_one, 1.0] },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.fractInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.fractInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// prettier-ignore
+const kInverseSqrtIntervalCases = {
+ f32: [
+ // 0.04 rounded to f32 0x3D23D70A or 0x3D23D70B,
+ // 1/sqrt(0x3D23D70B)=4.9999998230487200185270893769213 rounded to f32 0x409FFFFF or 0x40A00000,
+ // 1/sqrt(0x3D23D70A)=5.0000000558793553117506910583908 rounded to f32 0x40A00000 or 0x40A00001.
+ { input: 0.04, expected: [reinterpretU32AsF32(0x409FFFFF), reinterpretU32AsF32(0x40A00001)] }, // ~5.0
+ // Maximium f32 0x7F7FFFFF = 3.4028234663852886e+38,
+ // 1/sqrt(0x7F7FFFFF)=5.4210110239862427800382690921791e-20 rounded to f32 0x1F800000 or 0x1F800001
+ { input: kValue.f32.positive.max, expected: [reinterpretU32AsF32(0x1f800000), reinterpretU32AsF32(0x1f800001)] }, // ~5.421...e-20
+ ] as ScalarToIntervalCase[],
+ f16: [
+ // 0.04 rounded to f16 0x291E or 0x291F,
+ // 1/sqrt(0x291F)=4.9994660279328446295684795818427 rounded to f16 0x44FF or 0x4500,
+ // 1/sqrt(0x291E)=5.001373857053206453045376503367 rounded to f16 0x4500 or 0x4501.
+ { input: 0.04, expected: [reinterpretU16AsF16(0x44FF), reinterpretU16AsF16(0x4501)] }, // ~5.0
+ // Maximium f16 0x7BFF = 65504,
+ // 1/sqrt(0x7BFF)=0.00390720402370454101997160826062 rounded to f16 0x1C00 or 0x1C01
+ { input: kValue.f16.positive.max, expected: [reinterpretU16AsF16(0x1c00), reinterpretU16AsF16(0x1c01)] }, // ~3.9072...e-3
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('inverseSqrtInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // Note that the 2 ULP error is not included here.
+ // prettier-ignore
+ return [
+ // Exactly representable cases
+ { input: 1, expected: 1 },
+ { input: 0.25, expected: 2 },
+ { input: 64, expected: 0.125 },
+
+ // Cases that input and/or result not exactly representable
+ ...kInverseSqrtIntervalCases[p.trait],
+ // 1/sqrt(100.0)=0.1, rounded to corresponding trait
+ { input: 100, expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1'] }, // ~0.1
+
+ // Out of definition domain
+ { input: -1, expected: kUnboundedBounds },
+ { input: 0, expected: kUnboundedBounds },
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+
+ const error = (n: number): number => {
+ return 2 * trait.oneULP(n);
+ };
+
+ const expected = trait.toInterval(applyError(t.params.expected, error));
+
+ const got = trait.inverseSqrtInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.inverseSqrtInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Expectation interval of 1/inverseSqrt(sum(x[i]^2)) on some special values array x for certain
+// float traits, used as expectation for `length` and `distance`.
+// These cases are hard coded, since the error intervals are difficult to express in a closed
+// human-readable form due to the inherited nature of the errors.
+// prettier-ignore
+const kRootSumSquareExpectionInterval = {
+ f32: {
+ '[0.1]': [reinterpretU64AsF64(0x3fb9_9998_9000_0000n), reinterpretU64AsF64(0x3fb9_999a_7000_0000n)], // ~0.1
+ '[1.0]' : [reinterpretU64AsF64(0x3fef_ffff_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_9000_0000n)], // ~1.0
+ '[10]' : [reinterpretU64AsF64(0x4023_ffff_7000_0000n), reinterpretU64AsF64(0x4024_0000_b000_0000n)], // ~10
+ '[1.0, 1.0]' : [reinterpretU64AsF64(0x3ff6_a09d_b000_0000n), reinterpretU64AsF64(0x3ff6_a09f_1000_0000n)], // ~√2
+ '[1.0, 1.0, 1.0]' : [reinterpretU64AsF64(0x3ffb_b67a_1000_0000n), reinterpretU64AsF64(0x3ffb_b67b_b000_0000n)], // ~√3
+ '[1.0, 1.0, 1.0, 1.0]' : [reinterpretU64AsF64(0x3fff_ffff_7000_0000n), reinterpretU64AsF64(0x4000_0000_9000_0000n)], // ~2
+ } as {[s: string]: IntervalBounds},
+ f16: {
+ '[0.1]': [reinterpretU64AsF64(0x3fb9_7e00_0000_0000n), reinterpretU64AsF64(0x3fb9_b600_0000_0000n)], // ~0.1
+ '[1.0]' : [reinterpretU64AsF64(0x3fef_ee00_0000_0000n), reinterpretU64AsF64(0x3ff0_1200_0000_0000n)], // ~1.0
+ '[10]' : [reinterpretU64AsF64(0x4023_ea00_0000_0000n), reinterpretU64AsF64(0x4024_1200_0000_0000n)], // ~10
+ '[1.0, 1.0]' : [reinterpretU64AsF64(0x3ff6_8a00_0000_0000n), reinterpretU64AsF64(0x3ff6_b600_0000_0000n)], // ~√2
+ '[1.0, 1.0, 1.0]' : [reinterpretU64AsF64(0x3ffb_9a00_0000_0000n), reinterpretU64AsF64(0x3ffb_d200_0000_0000n)], // ~√3
+ '[1.0, 1.0, 1.0, 1.0]' : [reinterpretU64AsF64(0x3fff_ee00_0000_0000n), reinterpretU64AsF64(0x4000_1200_0000_0000n)], // ~2
+ } as {[s: string]: IntervalBounds},
+} as const;
+
+g.test('lengthIntervalScalar')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ {input: 1.0, expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ {input: -1.0, expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ {input: 0.1, expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1
+ {input: -0.1, expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1
+ {input: 10.0, expected: kRootSumSquareExpectionInterval[p.trait]['[10]'] }, // ~10
+ {input: -10.0, expected: kRootSumSquareExpectionInterval[p.trait]['[10]'] }, // ~10
+
+ // length(0) = kUnboundedBounds, because length uses sqrt, which is defined as 1/inversesqrt
+ {input: 0, expected: kUnboundedBounds },
+
+ // Subnormal Cases
+ { input: constants.negative.subnormal.min, expected: kUnboundedBounds },
+ { input: constants.negative.subnormal.max, expected: kUnboundedBounds },
+ { input: constants.positive.subnormal.min, expected: kUnboundedBounds },
+ { input: constants.positive.subnormal.max, expected: kUnboundedBounds },
+
+ // Edge cases
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.min, expected: kUnboundedBounds },
+ { input: constants.negative.max, expected: kUnboundedBounds },
+ { input: constants.positive.min, expected: kUnboundedBounds },
+ { input: constants.positive.max, expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.lengthInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.lengthInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// prettier-ignore
+const kLogIntervalCases = {
+ f32: [
+ // kValue.f32.positive.e is 0x402DF854 = 2.7182817459106445,
+ // log(0x402DF854) = 0.99999996963214000677592342891704 rounded to f32 0x3F7FFFFF or 0x3F800000 = 1.0
+ { input: kValue.f32.positive.e, expected: [kMinusOneULPFunctions['f32'](1.0), 1.0] },
+ // kValue.f32.positive.max is 0x7F7FFFFF = 3.4028234663852886e+38,
+ // log(0x7F7FFFFF) = 88.72283905206835305421152826479 rounded to f32 0x42B17217 or 0x42B17218.
+ { input: kValue.f32.positive.max, expected: [kMinusOneULPFunctions['f32'](reinterpretU32AsF32(0x42b17218)), reinterpretU32AsF32(0x42b17218)] },
+ ] as ScalarToIntervalCase[],
+ f16: [
+ // kValue.f16.positive.e is 0x416F = 2.716796875,
+ // log(0x416F) = 0.99945356688393512460279716546501 rounded to f16 0x3BFE or 0x3BFF.
+ { input: kValue.f16.positive.e, expected: [reinterpretU16AsF16(0x3bfe), reinterpretU16AsF16(0x3bff)] },
+ // kValue.f16.positive.max is 0x7BFF = 65504,
+ // log(0x7BFF) = 11.089866488461016076210728979771 rounded to f16 0x498B or 0x498C.
+ { input: kValue.f16.positive.max, expected: [reinterpretU16AsF16(0x498b), reinterpretU16AsF16(0x498c)] },
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('logInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ // prettier-ignore
+ return [
+ { input: -1, expected: kUnboundedBounds },
+ { input: 0, expected: kUnboundedBounds },
+ { input: 1, expected: 0 },
+ ...kLogIntervalCases[p.trait],
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const abs_error = t.params.trait === 'f32' ? 2 ** -21 : 2 ** -7;
+ const error = (n: number): number => {
+ if (t.params.input >= 0.5 && t.params.input <= 2.0) {
+ return abs_error;
+ }
+ return 3 * trait.oneULP(n);
+ };
+
+ const expected = trait.toInterval(applyError(t.params.expected, error));
+
+ const got = trait.logInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.logInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// prettier-ignore
+const kLog2IntervalCases = {
+ f32: [
+ // kValue.f32.positive.max is 0x7F7FFFFF = 3.4028234663852886e+38,
+ // log2(0x7F7FFFFF) = 127.99999991400867200665269600978 rounded to f32 0x42FFFFFF or 0x43000000 = 128.0
+ { input: kValue.f32.positive.max, expected: [kMinusOneULPFunctions['f32'](128.0), 128.0] },
+ ] as ScalarToIntervalCase[],
+ f16: [
+ // kValue.f16.positive.max is 0x7BFF = 65504,
+ // log2(0x7BFF) = 15.999295387023410627258428389903 rounded to f16 0x4BFF or 0x4C00 = 16.0
+ { input: kValue.f16.positive.max, expected: [kMinusOneULPFunctions['f16'](16.0), 16.0] },
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('log2Interval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ // prettier-ignore
+ return [
+ { input: -1, expected: kUnboundedBounds },
+ { input: 0, expected: kUnboundedBounds },
+ { input: 1, expected: 0 },
+ { input: 2, expected: 1 },
+ { input: 16, expected: 4 },
+ ...kLog2IntervalCases[p.trait],
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const abs_error = t.params.trait === 'f32' ? 2 ** -21 : 2 ** -7;
+ const error = (n: number): number => {
+ if (t.params.input >= 0.5 && t.params.input <= 2.0) {
+ return abs_error;
+ }
+ return 3 * trait.oneULP(n);
+ };
+
+ const expected = trait.toInterval(applyError(t.params.expected, error));
+
+ const got = trait.log2Interval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.log2Interval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('negationInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // Edge cases
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.positive.max, expected: constants.negative.min },
+ { input: constants.positive.min, expected: constants.negative.max },
+ { input: constants.negative.min, expected: constants.positive.max },
+ { input: constants.negative.max, expected: constants.positive.min },
+
+ // Normals
+ { input: 0, expected: 0 },
+ { input: 1.0, expected: -1.0 },
+ { input: -1.0, expected: 1 },
+ { input: 0.1, expected: kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'] }, // ~-0.1
+ { input: 1.9, expected: kConstantCorrectlyRoundedExpectation[p.trait]['-1.9'] }, // ~-1.9
+ { input: -0.1, expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1'] }, // ~0.1
+ { input: -1.9, expected: kConstantCorrectlyRoundedExpectation[p.trait]['1.9'] }, // ~1.9
+
+ // Subnormals
+ { input: constants.positive.subnormal.max, expected: [constants.negative.subnormal.min, 0] },
+ { input: constants.positive.subnormal.min, expected: [constants.negative.subnormal.max, 0] },
+ { input: constants.negative.subnormal.min, expected: [0, constants.positive.subnormal.max] },
+ { input: constants.negative.subnormal.max, expected: [0, constants.positive.subnormal.min] },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.negationInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.negationInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('quantizeToF16Interval')
+ .paramsSubcasesOnly<ScalarToIntervalCase>(
+ // prettier-ignore
+ [
+ { input: kValue.f32.negative.infinity, expected: kUnboundedBounds },
+ { input: kValue.f32.negative.min, expected: kUnboundedBounds },
+ { input: kValue.f16.negative.min, expected: kValue.f16.negative.min },
+ { input: -1.9, expected: kConstantCorrectlyRoundedExpectation['f16']['-1.9'] }, // ~-1.9
+ { input: -1, expected: -1 },
+ { input: -0.1, expected: kConstantCorrectlyRoundedExpectation['f16']['-0.1'] }, // ~-0.1
+ { input: kValue.f16.negative.max, expected: kValue.f16.negative.max },
+ { input: kValue.f16.negative.subnormal.min, expected: [kValue.f16.negative.subnormal.min, 0] },
+ { input: kValue.f16.negative.subnormal.max, expected: [kValue.f16.negative.subnormal.max, 0] },
+ { input: kValue.f32.negative.subnormal.max, expected: [kValue.f16.negative.subnormal.max, 0] },
+ { input: 0, expected: 0 },
+ { input: kValue.f32.positive.subnormal.min, expected: [0, kValue.f16.positive.subnormal.min] },
+ { input: kValue.f16.positive.subnormal.min, expected: [0, kValue.f16.positive.subnormal.min] },
+ { input: kValue.f16.positive.subnormal.max, expected: [0, kValue.f16.positive.subnormal.max] },
+ { input: kValue.f16.positive.min, expected: kValue.f16.positive.min },
+ { input: 0.1, expected: kConstantCorrectlyRoundedExpectation['f16']['0.1'] }, // ~0.1
+ { input: 1, expected: 1 },
+ { input: 1.9, expected: kConstantCorrectlyRoundedExpectation['f16']['1.9'] }, // ~1.9
+ { input: kValue.f16.positive.max, expected: kValue.f16.positive.max },
+ { input: kValue.f32.positive.max, expected: kUnboundedBounds },
+ { input: kValue.f32.positive.infinity, expected: kUnboundedBounds },
+ ]
+ )
+ .fn(t => {
+ const expected = FP.f32.toInterval(t.params.expected);
+
+ const got = FP.f32.quantizeToF16Interval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `f32.quantizeToF16Interval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// prettier-ignore
+const kRadiansIntervalCases = {
+ f32: [
+ { input: -180, expected: [kMinusOneULPFunctions['f32'](kValue.f32.negative.pi.whole), kPlusOneULPFunctions['f32'](kValue.f32.negative.pi.whole)] },
+ { input: -135, expected: [kMinusOneULPFunctions['f32'](kValue.f32.negative.pi.three_quarters), kPlusOneULPFunctions['f32'](kValue.f32.negative.pi.three_quarters)] },
+ { input: -90, expected: [kMinusOneULPFunctions['f32'](kValue.f32.negative.pi.half), kPlusOneULPFunctions['f32'](kValue.f32.negative.pi.half)] },
+ { input: -60, expected: [kMinusOneULPFunctions['f32'](kValue.f32.negative.pi.third), kPlusOneULPFunctions['f32'](kValue.f32.negative.pi.third)] },
+ { input: -45, expected: [kMinusOneULPFunctions['f32'](kValue.f32.negative.pi.quarter), kPlusOneULPFunctions['f32'](kValue.f32.negative.pi.quarter)] },
+ { input: -30, expected: [kMinusOneULPFunctions['f32'](kValue.f32.negative.pi.sixth), kPlusOneULPFunctions['f32'](kValue.f32.negative.pi.sixth)] },
+ { input: 30, expected: [kMinusOneULPFunctions['f32'](kValue.f32.positive.pi.sixth), kPlusOneULPFunctions['f32'](kValue.f32.positive.pi.sixth)] },
+ { input: 45, expected: [kMinusOneULPFunctions['f32'](kValue.f32.positive.pi.quarter), kPlusOneULPFunctions['f32'](kValue.f32.positive.pi.quarter)] },
+ { input: 60, expected: [kMinusOneULPFunctions['f32'](kValue.f32.positive.pi.third), kPlusOneULPFunctions['f32'](kValue.f32.positive.pi.third)] },
+ { input: 90, expected: [kMinusOneULPFunctions['f32'](kValue.f32.positive.pi.half), kPlusOneULPFunctions['f32'](kValue.f32.positive.pi.half)] },
+ { input: 135, expected: [kMinusOneULPFunctions['f32'](kValue.f32.positive.pi.three_quarters), kPlusOneULPFunctions['f32'](kValue.f32.positive.pi.three_quarters)] },
+ { input: 180, expected: [kMinusOneULPFunctions['f32'](kValue.f32.positive.pi.whole), kPlusOneULPFunctions['f32'](kValue.f32.positive.pi.whole)] },
+ ] as ScalarToIntervalCase[],
+ f16: [
+ { input: -180, expected: [kMinusOneULPFunctions['f16'](kValue.f16.negative.pi.whole), kPlusOneULPFunctions['f16'](kValue.f16.negative.pi.whole)] },
+ { input: -135, expected: [kMinusOneULPFunctions['f16'](kValue.f16.negative.pi.three_quarters), kPlusOneULPFunctions['f16'](kValue.f16.negative.pi.three_quarters)] },
+ { input: -90, expected: [kMinusOneULPFunctions['f16'](kValue.f16.negative.pi.half), kPlusOneULPFunctions['f16'](kValue.f16.negative.pi.half)] },
+ { input: -60, expected: [kMinusOneULPFunctions['f16'](kValue.f16.negative.pi.third), kPlusOneULPFunctions['f16'](kValue.f16.negative.pi.third)] },
+ { input: -45, expected: [kMinusOneULPFunctions['f16'](kValue.f16.negative.pi.quarter), kPlusOneULPFunctions['f16'](kValue.f16.negative.pi.quarter)] },
+ { input: -30, expected: [kMinusOneULPFunctions['f16'](kValue.f16.negative.pi.sixth), kPlusOneULPFunctions['f16'](kValue.f16.negative.pi.sixth)] },
+ { input: 30, expected: [kMinusOneULPFunctions['f16'](kValue.f16.positive.pi.sixth), kPlusOneULPFunctions['f16'](kValue.f16.positive.pi.sixth)] },
+ { input: 45, expected: [kMinusOneULPFunctions['f16'](kValue.f16.positive.pi.quarter), kPlusOneULPFunctions['f16'](kValue.f16.positive.pi.quarter)] },
+ { input: 60, expected: [kMinusOneULPFunctions['f16'](kValue.f16.positive.pi.third), kPlusOneULPFunctions['f16'](kValue.f16.positive.pi.third)] },
+ { input: 90, expected: [kMinusOneULPFunctions['f16'](kValue.f16.positive.pi.half), kPlusOneULPFunctions['f16'](kValue.f16.positive.pi.half)] },
+ { input: 135, expected: [kMinusOneULPFunctions['f16'](kValue.f16.positive.pi.three_quarters), kPlusOneULPFunctions['f16'](kValue.f16.positive.pi.three_quarters)] },
+ { input: 180, expected: [kMinusOneULPFunctions['f16'](kValue.f16.positive.pi.whole), kPlusOneULPFunctions['f16'](kValue.f16.positive.pi.whole)] },
+ ] as ScalarToIntervalCase[],
+ abstract: [
+ { input: -180, expected: kValue.f64.negative.pi.whole },
+ { input: -135, expected: kValue.f64.negative.pi.three_quarters },
+ { input: -90, expected: kValue.f64.negative.pi.half },
+ { input: -60, expected: kValue.f64.negative.pi.third },
+ { input: -45, expected: kValue.f64.negative.pi.quarter },
+ { input: -30, expected: kValue.f64.negative.pi.sixth },
+ { input: 30, expected: kValue.f64.positive.pi.sixth },
+ { input: 45, expected: kValue.f64.positive.pi.quarter },
+ { input: 60, expected: kValue.f64.positive.pi.third },
+ { input: 90, expected: kValue.f64.positive.pi.half },
+ { input: 135, expected: kValue.f64.positive.pi.three_quarters },
+ { input: 180, expected: kValue.f64.positive.pi.whole },
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('radiansInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = p.trait;
+ const constants = FP[trait].constants();
+ // prettier-ignore
+ return [
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ { input: 0, expected: 0 },
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ ...kRadiansIntervalCases[trait]
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.radiansInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.radiansInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Large but still representable integer
+const kRoundIntervalCases = {
+ f32: [
+ { input: 2 ** 30, expected: 2 ** 30 },
+ { input: -(2 ** 30), expected: -(2 ** 30) },
+ { input: 0x80000000, expected: 0x80000000 }, // https://github.com/gpuweb/cts/issues/2766
+ ],
+ f16: [
+ { input: 2 ** 14, expected: 2 ** 14 },
+ { input: -(2 ** 14), expected: -(2 ** 14) },
+ { input: 0x8000, expected: 0x8000 }, // https://github.com/gpuweb/cts/issues/2766
+ ],
+} as const;
+
+g.test('roundInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ return [
+ { input: 0, expected: 0 },
+ { input: 0.1, expected: 0 },
+ { input: 0.5, expected: 0 }, // Testing tie breaking
+ { input: 0.9, expected: 1 },
+ { input: 1.0, expected: 1 },
+ { input: 1.1, expected: 1 },
+ { input: 1.5, expected: 2 }, // Testing tie breaking
+ { input: 1.9, expected: 2 },
+ { input: -0.1, expected: 0 },
+ { input: -0.5, expected: 0 }, // Testing tie breaking
+ { input: -0.9, expected: -1 },
+ { input: -1.0, expected: -1 },
+ { input: -1.1, expected: -1 },
+ { input: -1.5, expected: -2 }, // Testing tie breaking
+ { input: -1.9, expected: -2 },
+
+ // Edge cases
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.positive.max, expected: constants.positive.max },
+ { input: constants.positive.min, expected: 0 },
+ { input: constants.negative.min, expected: constants.negative.min },
+ { input: constants.negative.max, expected: 0 },
+ ...kRoundIntervalCases[p.trait],
+
+ // 32-bit subnormals
+ { input: constants.positive.subnormal.max, expected: 0 },
+ { input: constants.positive.subnormal.min, expected: 0 },
+ { input: constants.negative.subnormal.min, expected: 0 },
+ { input: constants.negative.subnormal.max, expected: 0 },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.roundInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.roundInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('saturateInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ return [
+ // Normals
+ { input: 0, expected: 0 },
+ { input: 0.1, expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1'] },
+ { input: 1, expected: 1.0 },
+ { input: -0.1, expected: 0 },
+ { input: -1, expected: 0 },
+ { input: -10, expected: 0 },
+ { input: 10, expected: 1.0 },
+ { input: 11.1, expected: 1.0 },
+ { input: constants.positive.max, expected: 1.0 },
+ { input: constants.positive.min, expected: constants.positive.min },
+ { input: constants.negative.max, expected: 0.0 },
+ { input: constants.negative.min, expected: 0.0 },
+
+ // Subnormals
+ { input: constants.positive.subnormal.max, expected: [0.0, constants.positive.subnormal.max] },
+ { input: constants.positive.subnormal.min, expected: [0.0, constants.positive.subnormal.min] },
+ { input: constants.negative.subnormal.min, expected: [constants.negative.subnormal.min, 0.0] },
+ { input: constants.negative.subnormal.max, expected: [constants.negative.subnormal.max, 0.0] },
+
+ // Infinities
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.saturateInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.saturationInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('signInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ return [
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.min, expected: -1 },
+ { input: -10, expected: -1 },
+ { input: -1, expected: -1 },
+ { input: -0.1, expected: -1 },
+ { input: constants.negative.max, expected: -1 },
+ { input: constants.negative.subnormal.min, expected: [-1, 0] },
+ { input: constants.negative.subnormal.max, expected: [-1, 0] },
+ { input: 0, expected: 0 },
+ { input: constants.positive.subnormal.max, expected: [0, 1] },
+ { input: constants.positive.subnormal.min, expected: [0, 1] },
+ { input: constants.positive.min, expected: 1 },
+ { input: 0.1, expected: 1 },
+ { input: 1, expected: 1 },
+ { input: 10, expected: 1 },
+ { input: constants.positive.max, expected: 1 },
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.signInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.signInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('sinInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ return [
+ // This test does not include some common cases, i.e. f(x = -π|π) = 0,
+ // because the difference between true x and x as a f32 is sufficiently
+ // large, such that the high slope of f @ x causes the results to be
+ // substantially different, so instead of getting 0 you get a value on the
+ // order of 10^-8 away from it, thus difficult to express in a
+ // human-readable manner.
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.min, expected: kUnboundedBounds },
+ { input: constants.negative.pi.half, expected: [-1, kPlusOneULPFunctions[p.trait](-1)] },
+ { input: 0, expected: 0 },
+ { input: constants.positive.pi.half, expected: [kMinusOneULPFunctions[p.trait](1), 1] },
+ { input: constants.positive.max, expected: kUnboundedBounds },
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+
+ const error = (_: number): number => {
+ return t.params.trait === 'f32' ? 2 ** -11 : 2 ** -7;
+ };
+
+ const expected = trait.toInterval(applyError(t.params.expected, error));
+
+ const got = trait.sinInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.sinInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Some of these are hard coded, since the error intervals are difficult to express in a closed
+// human-readable form due to the inherited nature of the errors.
+// prettier-ignore
+const kSinhIntervalCases = {
+ f32: [
+ { input: -1, expected: [reinterpretU32AsF32(0xbf966d05), reinterpretU32AsF32(0xbf966cf8)] }, // ~-1.175...
+ { input: 0, expected: [reinterpretU32AsF32(0xb4600000), reinterpretU32AsF32(0x34600000)] }, // ~0
+ { input: 1, expected: [reinterpretU32AsF32(0x3f966cf8), reinterpretU32AsF32(0x3f966d05)] }, // ~1.175...
+ ] as ScalarToIntervalCase[],
+ f16: [
+ { input: -1, expected: [reinterpretU16AsF16(0xbcb8), reinterpretU16AsF16(0xbcaf)] }, // ~-1.175...
+ { input: 0, expected: [reinterpretU16AsF16(0x9200), reinterpretU16AsF16(0x1200)] }, // ~0
+ { input: 1, expected: [reinterpretU16AsF16(0x3caf), reinterpretU16AsF16(0x3cb8)] }, // ~1.175...
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('sinhInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ ...kSinhIntervalCases[p.trait],
+
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.min, expected: kUnboundedBounds },
+ { input: constants.positive.max, expected: kUnboundedBounds },
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.sinhInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.sinhInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// For sqrt interval inherited from 1.0 / inverseSqrt(x), errors come from:
+// 1. Rounding of input x, if any;
+// 2. 2 ULP from inverseSqrt;
+// 3. And 2.5 ULP from division.
+// The last 2.5ULP is handled in test and not included in the expected values here.
+// prettier-ignore
+const kSqrtIntervalCases = {
+ f32: [
+ // 0.01 rounded to f32 0x3C23D70A or 0x3C23D70B.
+ // For inverseSqrt interval, floor_f32(1.0/sqrt(0x3C23D70B))-2ULP=0x411FFFFD,
+ // ceil_f32(1.0/sqrt(0x3C23D70A))+2ULP=0x41200003.
+ // For division, 1.0/0x41200003=0.09999997138977868544997855067803 rounded to f32 0x3DCCCCC8 or 0x3DCCCCC9,
+ // 1.0/0x411FFFFD=0.100000028610237685454662304067 rounded to f32 0x3DCCCCD0 or 0x3DCCCCD1.
+ { input: 0.01, expected: [reinterpretU32AsF32(0x3DCCCCC8), reinterpretU32AsF32(0x3DCCCCD1)] }, // ~0.1
+ // For inverseSqrt interval, 1.0/sqrt(1.0)-2ULP=0x3F7FFFFE, 1.0/sqrt(1.0)+2ULP=0x3F800001.
+ // For division, 1.0/0x3F800001=0.9999998807907246108530328709735 rounded to f32 0x3F7FFFFE or 0x3F7FFFFF,
+ // 1.0/0x3F7FFFFE=1.0000001192093038108564210027667 rounded to f32 0x3F800001 or 0x3F800002.
+ { input: 1, expected: [reinterpretU32AsF32(0x3F7FFFFE), reinterpretU32AsF32(0x3F800002)] }, // ~1
+ // For inverseSqrt interval, 1.0/sqrt(4.0)-2ULP=0x3EFFFFFE, 1.0/sqrt(4.0)+2ULP=0x3F000001.
+ // For division, 1.0/0x3F000001=1.999999761581449221706065741947 rounded to f32 0x3FFFFFFE or 0x3FFFFFFF,
+ // 1.0/0x3EFFFFFE=2.0000002384186076217128420055334 rounded to f32 0x40000001 or 0x40000002.
+ { input: 4, expected: [reinterpretU32AsF32(0x3FFFFFFE), reinterpretU32AsF32(0x40000002)] }, // ~2
+ // For inverseSqrt interval, floor_f32(1.0/sqrt(100.0))-2ULP=0x3DCCCCCA,
+ // ceil_f32(1.0/sqrt(100.0))+2ULP=0x3DCCCCCF.
+ // For division, 1.0/0x3DCCCCCF=9.9999983608725376739278142322684 rounded to f32 0x411FFFFE or 0x411FFFFF,
+ // 1.0/0x3DCCCCCA=10.000002086163002207516386565905 rounded to f32 0x41200002 or 0x41200003.
+ { input: 100, expected: [reinterpretU32AsF32(0x411FFFFE), reinterpretU32AsF32(0x41200003)] }, // ~10
+ ] as ScalarToIntervalCase[],
+ f16: [
+ // 0.01 rounded to f16 0x211E or 0x211F.
+ // For inverseSqrt interval, floor_f16(1.0/sqrt(0x211F))-2ULP=0x48FD,
+ // ceil_f16(1.0/sqrt(0x211E))+2ULP=0x4903.
+ // For division, 1.0/0x4903=0.09976617303195635229929851909587 rounded to f16 0x2E62 or 0x2E63,
+ // 1.0/0x48FD=0.10023492560689115113547376664056 rounded to f16 0x2E6A or 0x2E6B.
+ { input: 0.01, expected: [reinterpretU16AsF16(0x2E62), reinterpretU16AsF16(0x2E6B)] }, // ~0.1
+ // For inverseSqrt interval, 1.0/sqrt(1.0)-2ULP=0x3BFE, 1.0/sqrt(1.0)+2ULP=0x3C01.
+ // For division, 1.0/0x3C01=0.99902439024390243902439024390244 rounded to f16 0x3BFE or 0x3BFF,
+ // 1.0/0x3BFE=1.000977517106549364613880742913 rounded to f16 0x3C01 or 0x3C02.
+ { input: 1, expected: [reinterpretU16AsF16(0x3BFE), reinterpretU16AsF16(0x3C02)] }, // ~1
+ // For inverseSqrt interval, 1.0/sqrt(4.0)-2ULP=0x37FE, 1.0/sqrt(4.0)+2ULP=0x3801.
+ // For division, 1.0/0x3801=1.9980487804878048780487804878049 rounded to f16 0x3FFE or 0x3FFF,
+ // 1.0/0x37FE=2.001955034213098729227761485826 rounded to f16 0x4001 or 0x4002.
+ { input: 4, expected: [reinterpretU16AsF16(0x3FFE), reinterpretU16AsF16(0x4002)] }, // ~2
+ // For inverseSqrt interval, floor_f16(1.0/sqrt(100.0))-2ULP=0x2E64,
+ // ceil_f16(1.0/sqrt(100.0))+2ULP=0x2E69.
+ // For division, 1.0/0x2E69=9.9841560024374942258493264279108 rounded to f16 0x48FD or 0x48FE,
+ // 1.0/0x2E64=10.014669926650366748166259168704 rounded to f16 0x4901 or 0x4902.
+ { input: 100, expected: [reinterpretU16AsF16(0x48FD), reinterpretU16AsF16(0x4902)] }, // ~10
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('sqrtInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // Cases that input and/or result not exactly representable
+ ...kSqrtIntervalCases[p.trait],
+
+ // Cases out of definition domain
+ { input: -1, expected: kUnboundedBounds },
+ { input: 0, expected: kUnboundedBounds },
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+
+ // The expected error interval is inherited from 1.0 / inverseSqrt(x), the 2.5ULP for division
+ // is handled here.
+ const error = (n: number): number => {
+ return 2.5 * trait.oneULP(n);
+ };
+
+ const expected = trait.toInterval(applyError(t.params.expected, error));
+
+ const got = trait.sqrtInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `FP.${t.params.trait}.sqrtInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// All of these are hard coded, since the error intervals are difficult to express in a closed
+// human-readable form.
+// Some easy looking cases like f(x = -π|π) = 0 are actually quite difficult. This is because the
+// interval is calculated from the results of sin(x)/cos(x), which becomes very messy at x = -π|π,
+// since π is irrational, thus does not have an exact representation as a float.
+//
+// Even at 0, which has a precise f32/f16 value, there is still the problem that result of sin(0)
+// and cos(0) will be intervals due to the inherited nature of errors, so the proper interval will
+// be an interval calculated from dividing an interval by another interval and applying an error
+// function to that.
+//
+// This complexity is why the entire interval framework was developed.
+//
+// The examples here have been manually traced to confirm the expectation values are correct.
+// prettier-ignore
+const kTanIntervalCases = {
+ f32: [
+ { input: kValue.f32.negative.pi.whole, expected: [reinterpretU64AsF64(0xbf40_02bc_9000_0000n), reinterpretU64AsF64(0x3f40_0144_f000_0000n)] }, // ~0.0
+ { input: kValue.f32.negative.pi.three_quarters, expected: [reinterpretU64AsF64(0x3fef_f4b1_3000_0000n), reinterpretU64AsF64(0x3ff0_05a9_9000_0000n)] }, // ~1.0
+ { input: kValue.f32.negative.pi.third, expected: [reinterpretU64AsF64(0xbffb_c16b_d000_0000n), reinterpretU64AsF64(0xbffb_ab8f_9000_0000n)] }, // ~-√3
+ { input: kValue.f32.negative.pi.quarter, expected: [reinterpretU64AsF64(0xbff0_05a9_b000_0000n), reinterpretU64AsF64(0xbfef_f4b1_5000_0000n)] }, // ~-1.0
+ { input: kValue.f32.negative.pi.sixth, expected: [reinterpretU64AsF64(0xbfe2_80f1_f000_0000n), reinterpretU64AsF64(0xbfe2_725e_d000_0000n)] }, // ~-1/√3
+ { input: 0, expected: [reinterpretU64AsF64(0xbf40_0200_b000_0000n), reinterpretU64AsF64(0x3f40_0200_b000_0000n)] }, // ~0.0
+ { input: kValue.f32.positive.pi.sixth, expected: [reinterpretU64AsF64(0x3fe2_725e_d000_0000n), reinterpretU64AsF64(0x3fe2_80f1_f000_0000n)] }, // ~1/√3
+ { input: kValue.f32.positive.pi.quarter, expected: [reinterpretU64AsF64(0x3fef_f4b1_5000_0000n), reinterpretU64AsF64(0x3ff0_05a9_b000_0000n)] }, // ~1.0
+ { input: kValue.f32.positive.pi.third, expected: [reinterpretU64AsF64(0x3ffb_ab8f_9000_0000n), reinterpretU64AsF64(0x3ffb_c16b_d000_0000n)] }, // ~√3
+ { input: kValue.f32.positive.pi.three_quarters, expected: [reinterpretU64AsF64(0xbff0_05a9_9000_0000n), reinterpretU64AsF64(0xbfef_f4b1_3000_0000n)] }, // ~-1.0
+ { input: kValue.f32.positive.pi.whole, expected: [reinterpretU64AsF64(0xbf40_0144_f000_0000n), reinterpretU64AsF64(0x3f40_02bc_9000_0000n)] }, // ~0.0
+ ] as ScalarToIntervalCase[],
+ f16: [
+ { input: kValue.f16.negative.pi.whole, expected: [reinterpretU64AsF64(0xbf7c_5600_0000_0000n), reinterpretU64AsF64(0x3f82_2e00_0000_0000n)] }, // ~0.0
+ { input: kValue.f16.negative.pi.three_quarters, expected: [reinterpretU64AsF64(0x3fef_4600_0000_0000n), reinterpretU64AsF64(0x3ff0_7200_0000_0000n)] }, // ~1.0
+ { input: kValue.f16.negative.pi.third, expected: [reinterpretU64AsF64(0xbffc_7600_0000_0000n), reinterpretU64AsF64(0xbffa_f600_0000_0000n)] }, // ~-√3
+ { input: kValue.f16.negative.pi.quarter, expected: [reinterpretU64AsF64(0xbff0_6600_0000_0000n), reinterpretU64AsF64(0xbfef_3600_0000_0000n)] }, // ~-1.0
+ { input: kValue.f16.negative.pi.sixth, expected: [reinterpretU64AsF64(0xbfe2_fe00_0000_0000n), reinterpretU64AsF64(0xbfe1_f600_0000_0000n)] }, // ~-1/√3
+ { input: 0, expected: [reinterpretU64AsF64(0xbf80_2e00_0000_0000n), reinterpretU64AsF64(0x3f80_2e00_0000_0000n)] }, // ~0.0
+ { input: kValue.f16.positive.pi.sixth, expected: [reinterpretU64AsF64(0x3fe1_f600_0000_0000n), reinterpretU64AsF64(0x3fe2_fe00_0000_0000n)] }, // ~1/√3
+ { input: kValue.f16.positive.pi.quarter, expected: [reinterpretU64AsF64(0x3fef_3600_0000_0000n), reinterpretU64AsF64(0x3ff0_6600_0000_0000n)] }, // ~1.0
+ { input: kValue.f16.positive.pi.third, expected: [reinterpretU64AsF64(0x3ffa_f600_0000_0000n), reinterpretU64AsF64(0x3ffc_7600_0000_0000n)] }, // ~√3
+ { input: kValue.f16.positive.pi.three_quarters, expected: [reinterpretU64AsF64(0xbff0_7200_0000_0000n), reinterpretU64AsF64(0xbfef_4600_0000_0000n)] }, // ~-1.0
+ { input: kValue.f16.positive.pi.whole, expected: [reinterpretU64AsF64(0xbf82_2e00_0000_0000n), reinterpretU64AsF64(0x3f7c_5600_0000_0000n)] }, // ~0.0
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('tanInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ ...kTanIntervalCases[p.trait],
+
+ // Cases that result in unbounded interval.
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.min, expected: kUnboundedBounds },
+ { input: constants.negative.pi.half, expected: kUnboundedBounds },
+ { input: constants.positive.pi.half, expected: kUnboundedBounds },
+ { input: constants.positive.max, expected: kUnboundedBounds },
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.tanInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.tanInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Some of these are hard coded, since the error intervals are difficult to express in a closed
+// human-readable form due to the inherited nature of the errors.
+// prettier-ignore
+const kTanhIntervalCases = {
+ f32: [
+ { input: -1, expected: [reinterpretU64AsF64(0xbfe8_5efd_1000_0000n), reinterpretU64AsF64(0xbfe8_5ef8_9000_0000n)] }, // ~-0.7615...
+ { input: 0, expected: [reinterpretU64AsF64(0xbe8c_0000_b000_0000n), reinterpretU64AsF64(0x3e8c_0000_b000_0000n)] }, // ~0
+ { input: 1, expected: [reinterpretU64AsF64(0x3fe8_5ef8_9000_0000n), reinterpretU64AsF64(0x3fe8_5efd_1000_0000n)] }, // ~0.7615...
+ ] as ScalarToIntervalCase[],
+ f16: [
+ { input: -1, expected: [reinterpretU64AsF64(0xbfe8_9600_0000_0000n), reinterpretU64AsF64(0xbfe8_2e00_0000_0000n)] }, // ~-0.7615...
+ { input: 0, expected: [reinterpretU64AsF64(0xbf48_0e00_0000_0000n), reinterpretU64AsF64(0x3f48_0e00_0000_0000n)] }, // ~0
+ { input: 1, expected: [reinterpretU64AsF64(0x3fe8_2e00_0000_0000n), reinterpretU64AsF64(0x3fe8_9600_0000_0000n)] }, // ~0.7615...
+ ] as ScalarToIntervalCase[],
+} as const;
+
+g.test('tanhInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ ...kTanhIntervalCases[p.trait],
+
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.min, expected: kUnboundedBounds },
+ { input: constants.positive.max, expected: kUnboundedBounds },
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.tanhInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.tanhInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('truncInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // Normals
+ { input: 0, expected: 0 },
+ { input: 0.1, expected: 0 },
+ { input: 0.9, expected: 0 },
+ { input: 1.0, expected: 1 },
+ { input: 1.1, expected: 1 },
+ { input: 1.9, expected: 1 },
+ { input: -0.1, expected: 0 },
+ { input: -0.9, expected: 0 },
+ { input: -1.0, expected: -1 },
+ { input: -1.1, expected: -1 },
+ { input: -1.9, expected: -1 },
+
+ // Subnormals
+ { input: constants.positive.subnormal.max, expected: 0 },
+ { input: constants.positive.subnormal.min, expected: 0 },
+ { input: constants.negative.subnormal.min, expected: 0 },
+ { input: constants.negative.subnormal.max, expected: 0 },
+
+ // Edge cases
+ { input: constants.positive.infinity, expected: kUnboundedBounds },
+ { input: constants.negative.infinity, expected: kUnboundedBounds },
+ { input: constants.positive.max, expected: constants.positive.max },
+ { input: constants.positive.min, expected: 0 },
+ { input: constants.negative.min, expected: constants.negative.min },
+ { input: constants.negative.max, expected: 0 },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.truncInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `FP.${t.params.trait}.truncInterval(${t.params.input}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface ScalarPairToIntervalCase {
+ // input is a pair of independent values, not a range, so should not be
+ // converted to a FPInterval.
+ input: [number, number];
+ expected: number | IntervalBounds;
+}
+
+// prettier-ignore
+const kAdditionInterval64BitsNormalCases = {
+ f32: [
+ // 0.1 falls between f32 0x3DCCCCCC and 0x3DCCCCCD, -0.1 falls between f32 0xBDCCCCCD and 0xBDCCCCCC
+ // f32 0x3DCCCCCC+0x3DCCCCCC=0x3E4CCCCC, 0x3DCCCCCD+0x3DCCCCCD=0x3E4CCCCD
+ { input: [0.1, 0.1], expected: [reinterpretU32AsF32(0x3e4ccccc), reinterpretU32AsF32(0x3e4ccccd)] }, // ~0.2
+ // f32 0xBDCCCCCD+0xBDCCCCCD=0xBE4CCCCD, 0xBDCCCCCC+0xBDCCCCCC=0xBE4CCCCD
+ { input: [-0.1, -0.1], expected: [reinterpretU32AsF32(0xbe4ccccd), reinterpretU32AsF32(0xbe4ccccc)] }, // ~-0.2
+ // 0.1+(-0.1) expect f32 interval [0x3DCCCCCC+0xBDCCCCCD, 0x3DCCCCCD+0xBDCCCCCC]
+ { input: [0.1, -0.1], expected: [reinterpretU32AsF32(0x3dcccccc)+reinterpretU32AsF32(0xbdcccccd), reinterpretU32AsF32(0x3dcccccd)+reinterpretU32AsF32(0xbdcccccc)] }, // ~0.0
+ // -0.1+0.1 expect f32 interval [0xBDCCCCCD+0x3DCCCCCC, 0xBDCCCCCC+0x3DCCCCCD]
+ { input: [-0.1, 0.1], expected: [reinterpretU32AsF32(0xbdcccccd)+reinterpretU32AsF32(0x3dcccccc), reinterpretU32AsF32(0xbdcccccc)+reinterpretU32AsF32(0x3dcccccd)] }, // ~0.0
+ ] as ScalarPairToIntervalCase[],
+ f16: [
+ // 0.1 falls between f16 0x2E66 and 0x2E67, -0.1 falls between f16 0xAE67 and 0xAE66
+ // f16 0x2E66+0x2E66=0x3266, 0x2E67+0x2E67=0x3267
+ { input: [0.1, 0.1], expected: [reinterpretU16AsF16(0x3266), reinterpretU16AsF16(0x3267)] }, // ~0.2
+ // f16 0xAE67+0xAE67=0xB267, 0xAE66+0xAE66=0xB266
+ { input: [-0.1, -0.1], expected: [reinterpretU16AsF16(0xb267), reinterpretU16AsF16(0xb266)] }, // ~-0.2
+ // 0.1+(-0.1) expect f16 interval [0x2E66+0xAE67, 0x2E67+0xAE66]
+ { input: [0.1, -0.1], expected: [reinterpretU16AsF16(0x2e66)+reinterpretU16AsF16(0xae67), reinterpretU16AsF16(0x2e67)+reinterpretU16AsF16(0xae66)] }, // ~0.0
+ // -0.1+0.1 expect f16 interval [0xAE67+0x2E66, 0xAE66+0x2E67]
+ { input: [-0.1, 0.1], expected: [reinterpretU16AsF16(0xae67)+reinterpretU16AsF16(0x2e66), reinterpretU16AsF16(0xae66)+reinterpretU16AsF16(0x2e67)] }, // ~0.0
+ ] as ScalarPairToIntervalCase[],
+ abstract: [
+ // 0.1 isn't exactly representable in f64, but will be quantized to an
+ // exact value when storing to a 'number' (0x3FB999999999999A).
+ // This is why below the expectations are not intervals.
+ // f64 0x3FB999999999999A+0x3FB999999999999A = 0x3FC999999999999A
+ { input: [0.1, 0.1], expected: reinterpretU64AsF64(0x3FC999999999999An) }, // ~0.2
+ // f64 0xBFB999999999999A+0xBFB999999999999A = 0xBFC999999999999A
+ { input: [-0.1, -0.1], expected: reinterpretU64AsF64(0xBFC999999999999An) }, // ~-0.2
+ { input: [0.1, -0.1], expected: 0 },
+ { input: [-0.1, 0.1], expected: 0 },
+ ] as ScalarPairToIntervalCase[],
+} as const;
+
+g.test('additionInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarPairToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // Representable normals
+ { input: [0, 0], expected: 0 },
+ { input: [1, 0], expected: 1 },
+ { input: [0, 1], expected: 1 },
+ { input: [-1, 0], expected: -1 },
+ { input: [0, -1], expected: -1 },
+ { input: [1, 1], expected: 2 },
+ { input: [1, -1], expected: 0 },
+ { input: [-1, 1], expected: 0 },
+ { input: [-1, -1], expected: -2 },
+
+ // 0.1 should be correctly rounded
+ { input: [0.1, 0], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1'] },
+ { input: [0, 0.1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1'] },
+ // -0.1 should be correctly rounded
+ { input: [-0.1, 0], expected: kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'] },
+ { input: [0, -0.1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'] },
+
+ // 64-bit normals that can not be exactly represented
+ ...kAdditionInterval64BitsNormalCases[p.trait],
+
+ // Subnormals
+ { input: [constants.positive.subnormal.max, 0], expected: [0, constants.positive.subnormal.max] },
+ { input: [0, constants.positive.subnormal.max], expected: [0, constants.positive.subnormal.max] },
+ { input: [constants.positive.subnormal.min, 0], expected: [0, constants.positive.subnormal.min] },
+ { input: [0, constants.positive.subnormal.min], expected: [0, constants.positive.subnormal.min] },
+ { input: [constants.negative.subnormal.max, 0], expected: [constants.negative.subnormal.max, 0] },
+ { input: [0, constants.negative.subnormal.max], expected: [constants.negative.subnormal.max, 0] },
+ { input: [constants.negative.subnormal.min, 0], expected: [constants.negative.subnormal.min, 0] },
+ { input: [0, constants.negative.subnormal.min], expected: [constants.negative.subnormal.min, 0] },
+
+ // Infinities
+ { input: [0, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, 0], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [0, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, 0], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.additionInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.additionInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Cases for Atan2Interval. The positive x & y quadrant is tested in more detail, and the other
+// quadrants are spot checked that values are pointing in the right direction.
+// Note: atan2's parameters are labelled (y, x) instead of (x, y)
+// prettier-ignore
+const kAtan2IntervalCases = {
+ // atan has 4096ULP error boundary for f32.
+ f32: [
+ // positive y, positive x
+ // √3 rounded to f32 0x3FDDB3D7, atan2(1, 0x3FDDB3D7)=0.52359877749051820266056630237827 ~ pi/6 rounded to f32 0x3F060A91 or 0x3F060A92,
+ // kValue.f32.positive.pi.sixth is 0x3F060A92.
+ { input: [1, reinterpretU32AsF32(0x3fddb3d7)], expected: [kMinusNULPFunctions['f32'](kValue.f32.positive.pi.sixth, 4097), kPlusNULPFunctions['f32'](kValue.f32.positive.pi.sixth, 4096)] },
+ // atan2(1, 1)=0.78539816339744830961566084581988 ~ pi/4 rounded to f32 0x3F490FDA or 0x3F490FDB,
+ // kValue.f32.positive.pi.quarter is 0x3F490FDB.
+ { input: [1, 1], expected: [kMinusNULPFunctions['f32'](kValue.f32.positive.pi.quarter, 4097), kPlusNULPFunctions['f32'](kValue.f32.positive.pi.quarter, 4096)] },
+ // √3 rounded to f32 0x3FDDB3D7, atan2(0x3FDDB3D7, 1) = 1.0471975493043784165707553892615 ~ pi/3 rounded to f32 0x3F860A91 or 0x3F860A92,
+ // kValue.f32.positive.pi.third is 0x3F860A92.
+ { input: [reinterpretU32AsF32(0x3fddb3d7), 1], expected: [kMinusNULPFunctions['f32'](kValue.f32.positive.pi.third, 4097), kPlusNULPFunctions['f32'](kValue.f32.positive.pi.third, 4096)] },
+
+ // positive y, negative x
+ // atan2(1, -1)=pi*3/4=2.3561944901923449288469825374591 rounded to f32 0x4016CBE3 or 0x4016CBE4,
+ // kValue.f32.positive.pi.three_quarters is 0x4016CBE4.
+ { input: [1, -1], expected: [kMinusNULPFunctions['f32'](kValue.f32.positive.pi.three_quarters, 4097), kPlusNULPFunctions['f32'](kValue.f32.positive.pi.three_quarters, 4096)] },
+
+ // negative y, negative x
+ // atan2(-1, -1)=-pi*3/4=-2.3561944901923449288469825374591 rounded to f32 0xC016CBE4 or 0xC016CBE3,
+ // kValue.f32.negative.pi.three_quarters is 0xC016CBE4.
+ { input: [-1, -1], expected: [kMinusNULPFunctions['f32'](kValue.f32.negative.pi.three_quarters, 4096), kPlusNULPFunctions['f32'](kValue.f32.negative.pi.three_quarters, 4097)] },
+
+ // negative y, positive x
+ // atan2(-1, 1)=-pi/4=-0.78539816339744830961566084581988 rounded to f32 0xBF490FDB or 0xBF490FDA,
+ // kValue.f32.negative.pi.quarter is 0xBF490FDB.
+ { input: [-1, 1], expected: [kMinusNULPFunctions['f32'](kValue.f32.negative.pi.quarter, 4096), kPlusNULPFunctions['f32'](kValue.f32.negative.pi.quarter, 4097)] },
+
+ // When y/x ~ 0, test that ULP applied to result of atan2, not the intermediate y/x value.
+ // y/x ~ 0, y<0, x<0, atan2(y,x) ~ -pi rounded to f32 0xC0490FDB or 0xC0490FDA,
+ // kValue.f32.negative.pi.whole is 0xC0490FDB.
+ {input: [kValue.f32.negative.max, -1], expected: [kMinusNULPFunctions['f32'](kValue.f32.negative.pi.whole, 4096), kPlusNULPFunctions['f32'](kValue.f32.negative.pi.whole, 4097)] },
+ // y/x ~ 0, y>0, x<0, atan2(y,x) ~ pi rounded to f32 0x40490FDA or 0x40490FDB,
+ // kValue.f32.positive.pi.whole is 0x40490FDB.
+ {input: [kValue.f32.positive.min, -1], expected: [kMinusNULPFunctions['f32'](kValue.f32.positive.pi.whole, 4097), kPlusNULPFunctions['f32'](kValue.f32.positive.pi.whole, 4096)] },
+ ] as ScalarPairToIntervalCase[],
+ // atan has 5ULP error boundary for f16.
+ f16: [
+ // positive y, positive x
+ // √3 rounded to f16 0x3EED, atan2(1, 0x3EED)=0.52375018906301191131992842392268 ~ pi/6 rounded to f16 0x3830 or 0x3831,
+ // kValue.f16.positive.pi.sixth is 0x3830.
+ { input: [1, reinterpretU16AsF16(0x3eed)], expected: [kMinusNULPFunctions['f16'](kValue.f16.positive.pi.sixth, 5), kPlusNULPFunctions['f16'](kValue.f16.positive.pi.sixth, 6)] },
+ // atan2(1, 1)=0.78539816339744830961566084581988 ~ pi/4 rounded to f16 0x3A48 or 0x3A49,
+ // kValue.f16.positive.pi.quarter is 0x3A48.
+ { input: [1, 1], expected: [kMinusNULPFunctions['f16'](kValue.f16.positive.pi.quarter, 5), kPlusNULPFunctions['f16'](kValue.f16.positive.pi.quarter, 6)] },
+ // √3 rounded to f16 0x3EED, atan2(0x3EED, 1) = 1.0470461377318847079113932677171 ~ pi/3 rounded to f16 0x3C30 or 0x3C31,
+ // kValue.f16.positive.pi.third is 0x3C30.
+ { input: [reinterpretU16AsF16(0x3eed), 1], expected: [kMinusNULPFunctions['f16'](kValue.f16.positive.pi.third, 5), kPlusNULPFunctions['f16'](kValue.f16.positive.pi.third, 6)] },
+
+ // positive y, negative x
+ // atan2(1, -1)=pi*3/4=2.3561944901923449288469825374591 rounded to f16 0x40B6 or 0x40B7,
+ // kValue.f16.positive.pi.three_quarters is 0x40B6.
+ { input: [1, -1], expected: [kMinusNULPFunctions['f16'](kValue.f16.positive.pi.three_quarters, 5), kPlusNULPFunctions['f16'](kValue.f16.positive.pi.three_quarters, 6)] },
+
+ // negative y, negative x
+ // atan2(-1, -1)=-pi*3/4=-2.3561944901923449288469825374591 rounded to f16 0xC0B7 or 0xC0B6,
+ // kValue.f16.negative.pi.three_quarters is 0xC0B6.
+ { input: [-1, -1], expected: [kMinusNULPFunctions['f16'](kValue.f16.negative.pi.three_quarters, 6), kPlusNULPFunctions['f16'](kValue.f16.negative.pi.three_quarters, 5)] },
+
+ // negative y, positive x
+ // atan2(-1, 1)=-pi/4=-0.78539816339744830961566084581988 rounded to f16 0xBA49 or 0xBA48,
+ // kValue.f16.negative.pi.quarter is 0xBA48.
+ { input: [-1, 1], expected: [kMinusNULPFunctions['f16'](kValue.f16.negative.pi.quarter, 6), kPlusNULPFunctions['f16'](kValue.f16.negative.pi.quarter, 5)] },
+
+ // When y/x ~ 0, test that ULP applied to result of atan2, not the intermediate y/x value.
+ // y/x ~ 0, y<0, x<0, atan2(y,x) ~ -pi rounded to f16 0xC249 or 0xC248,
+ // kValue.f16.negative.pi.whole is 0xC248.
+ {input: [kValue.f16.negative.max, -1], expected: [kMinusNULPFunctions['f16'](kValue.f16.negative.pi.whole, 6), kPlusNULPFunctions['f16'](kValue.f16.negative.pi.whole, 5)] },
+ // y/x ~ 0, y>0, x<0, atan2(y,x) ~ pi rounded to f16 0x4248 or 0x4249,
+ // kValue.f16.positive.pi.whole is 0x4248.
+ {input: [kValue.f16.positive.min, -1], expected: [kMinusNULPFunctions['f16'](kValue.f16.positive.pi.whole, 5), kPlusNULPFunctions['f16'](kValue.f16.positive.pi.whole, 6)] },
+ ] as ScalarPairToIntervalCase[],
+} as const;
+
+g.test('atan2Interval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarPairToIntervalCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ return [
+ ...kAtan2IntervalCases[p.trait],
+
+ // Cases that y out of bound.
+ // positive y, positive x
+ { input: [Number.POSITIVE_INFINITY, 1], expected: kUnboundedBounds },
+ // positive y, negative x
+ { input: [Number.POSITIVE_INFINITY, -1], expected: kUnboundedBounds },
+ // negative y, negative x
+ { input: [Number.NEGATIVE_INFINITY, -1], expected: kUnboundedBounds },
+ // negative y, positive x
+ { input: [Number.NEGATIVE_INFINITY, 1], expected: kUnboundedBounds },
+
+ // Discontinuity @ origin (0,0)
+ { input: [0, 0], expected: kUnboundedBounds },
+ { input: [0, constants.positive.subnormal.max], expected: kUnboundedBounds },
+ { input: [0, constants.negative.subnormal.min], expected: kUnboundedBounds },
+ { input: [0, constants.positive.min], expected: kUnboundedBounds },
+ { input: [0, constants.negative.max], expected: kUnboundedBounds },
+ { input: [0, constants.positive.max], expected: kUnboundedBounds },
+ { input: [0, constants.negative.min], expected: kUnboundedBounds },
+ { input: [0, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [0, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [0, 1], expected: kUnboundedBounds },
+ { input: [constants.positive.subnormal.max, 1], expected: kUnboundedBounds },
+ { input: [constants.negative.subnormal.min, 1], expected: kUnboundedBounds },
+
+ // Very large |x| values should cause kUnboundedBounds to be returned, due to the restrictions on division
+ { input: [1, constants.positive.max], expected: kUnboundedBounds },
+ { input: [1, constants.positive.nearest_max], expected: kUnboundedBounds },
+ { input: [1, constants.negative.min], expected: kUnboundedBounds },
+ { input: [1, constants.negative.nearest_min], expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const [y, x] = t.params.input;
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.atan2Interval(y, x);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.atan2Interval(${y}, ${x}) returned ${got}]. Expected ${expected}`
+ );
+ });
+
+g.test('distanceIntervalScalar')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarPairToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ { input: [1.0, 0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [0.0, 1.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [-0.0, -1.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [0.0, -1.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [0.1, 0], expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1
+ { input: [0, 0.1], expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1
+ { input: [-0.1, 0], expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1
+ { input: [0, -0.1], expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1
+ { input: [10.0, 0], expected: kRootSumSquareExpectionInterval[p.trait]['[10]'] }, // ~10
+ { input: [0, 10.0], expected: kRootSumSquareExpectionInterval[p.trait]['[10]'] }, // ~10
+ { input: [-10.0, 0], expected: kRootSumSquareExpectionInterval[p.trait]['[10]'] }, // ~10
+ { input: [0, -10.0], expected: kRootSumSquareExpectionInterval[p.trait]['[10]'] }, // ~10
+
+ // distance(x, y), where x - y = 0 has an acceptance interval of kUnboundedBounds,
+ // because distance(x, y) = length(x - y), and length(0) = kUnboundedBounds
+ { input: [0, 0], expected: kUnboundedBounds },
+ { input: [1.0, 1.0], expected: kUnboundedBounds },
+ { input: [-1.0, -1.0], expected: kUnboundedBounds },
+
+ // Subnormal Cases
+ { input: [constants.negative.subnormal.min, 0], expected: kUnboundedBounds },
+ { input: [constants.negative.subnormal.max, 0], expected: kUnboundedBounds },
+ { input: [constants.positive.subnormal.min, 0], expected: kUnboundedBounds },
+ { input: [constants.positive.subnormal.max, 0], expected: kUnboundedBounds },
+
+ // Edge cases
+ { input: [constants.positive.infinity, 0], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, 0], expected: kUnboundedBounds },
+ { input: [constants.negative.min, 0], expected: kUnboundedBounds },
+ { input: [constants.negative.max, 0], expected: kUnboundedBounds },
+ { input: [constants.positive.min, 0], expected: kUnboundedBounds },
+ { input: [constants.positive.max, 0], expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.distanceInterval(...t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.distanceInterval(${t.params.input[0]}, ${t.params.input[1]}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// prettier-ignore
+const kDivisionInterval64BitsNormalCases = {
+ f32: [
+ // Zero divided by any non-zero finite value results in zero.
+ { input: [0, 0.1], expected: 0 },
+ { input: [0, -0.1], expected: 0 },
+ // 0.1 rounded to f32 0x3DCCCCCC or 0x3DCCCCCD,
+ // 1.0/0x3DCCCCCD = 9.9999998509883902204460179966303 rounded to f32 0x411FFFFF or 0x41200000,
+ // 1.0/0x3DCCCCCC = 10.000000596046483527138934924167 rounded to f32 0x41200000 or 0x41200001.
+ { input: [1, 0.1], expected: [reinterpretU32AsF32(0x411fffff), reinterpretU32AsF32(0x41200001)] }, // ~10.0
+ // The same for -1/-0.1
+ { input: [-1, -0.1], expected: [reinterpretU32AsF32(0x411fffff), reinterpretU32AsF32(0x41200001)] }, // ~10.0
+ // -10.000000596046483527138934924167 rounded to f32 0xC1200001 or 0xC1200000,
+ // -9.9999998509883902204460179966303 rounded to f32 0xC1200000 or 0xC11FFFFF.
+ { input: [-1, 0.1], expected: [reinterpretU32AsF32(0xc1200001), reinterpretU32AsF32(0xc11fffff)] }, // ~-10.0
+ { input: [1, -0.1], expected: [reinterpretU32AsF32(0xc1200001), reinterpretU32AsF32(0xc11fffff)] }, // ~-10.0
+ // Cases that expected interval larger than +-1ULP.
+ // 0.000001 rounded to f32 0x358637BD or 0x358637BE,
+ // 1.0/0x358637BE = 999999.88883793195700674522548684 rounded to f32 0x497423FE or 0x497423FF,
+ // 1.0/0x358637BD = 1000000.0025247573063743994399971 rounded to f32 0x49742400 or 0x49742401.
+ { input: [1, 0.000001], expected: [reinterpretU32AsF32(0x497423fe), reinterpretU32AsF32(0x49742401)] }, // ~1000000.0
+ { input: [1, -0.000001], expected: [reinterpretU32AsF32(0xc9742401), reinterpretU32AsF32(0xc97423fe)] }, // ~-1000000.0
+ ] as ScalarPairToIntervalCase[],
+ f16: [
+ // Zero divided by any non-zero finite value results in zero.
+ { input: [0, 0.1], expected: 0 },
+ { input: [0, -0.1], expected: 0 },
+ // 0.1 rounded to f16 0x2E66 or 0x2E67,
+ // 1.0/0x2E67 = 9.9963392312385600976205003050641 rounded to f16 0x48FF or 0x4900,
+ // 1.0/0x2E66 = 10.002442002442002442002442002442 rounded to f16 0x4900 or 0x4901.
+ { input: [1, 0.1], expected: [reinterpretU16AsF16(0x48ff), reinterpretU16AsF16(0x4901)] }, // ~10.0
+ // The same for -1/-0.1
+ { input: [-1, -0.1], expected: [reinterpretU16AsF16(0x48ff), reinterpretU16AsF16(0x4901)] }, // ~10.0
+ // -10.002442002442002442002442002442 rounded to f16 0xC901 or 0xC900,
+ // -9.9963392312385600976205003050641 rounded to f16 0xC900 or 0xC8FF.
+ { input: [-1, 0.1], expected: [reinterpretU16AsF16(0xc901), reinterpretU16AsF16(0xc8ff)] }, // ~-10.0
+ { input: [1, -0.1], expected: [reinterpretU16AsF16(0xc901), reinterpretU16AsF16(0xc8ff)] }, // ~-10.0
+ // Cases that expected interval larger than +-1ULP.
+ // 0.001 rounded to f16 0x1418 or 0x1419,
+ // 1.0/0x1419 = 999.59580552907535977846384072716 rounded to f16 0x63CF or 0x63D0,
+ // 1.0/0x1418 = 1000.5496183206106870229007633588 rounded to f16 0x63D1 or 0x63D2.
+ { input: [1, 0.001], expected: [reinterpretU16AsF16(0x63cf), reinterpretU16AsF16(0x63d2)] }, // ~1000.0
+ { input: [1, -0.001], expected: [reinterpretU16AsF16(0xe3d2), reinterpretU16AsF16(0xe3cf)] }, // ~-1000.0
+ ] as ScalarPairToIntervalCase[],
+} as const;
+
+g.test('divisionInterval')
+ .params(u =>
+ u
+ .combine('trait', ['abstract', 'f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarPairToIntervalCase>(p => {
+ // This is a ULP based interval, so abstract should behave like f32, so
+ // swizzling the trait as needed.
+ const trait = p.trait === 'abstract' ? 'f32' : p.trait;
+ const fp = FP[trait];
+ const constants = fp.constants();
+ // prettier-ignore
+ return [
+ // Representable normals
+ { input: [0, 1], expected: 0 },
+ { input: [0, -1], expected: 0 },
+ { input: [1, 1], expected: 1 },
+ { input: [1, -1], expected: -1 },
+ { input: [-1, 1], expected: -1 },
+ { input: [-1, -1], expected: 1 },
+ { input: [4, 2], expected: 2 },
+ { input: [-4, 2], expected: -2 },
+ { input: [4, -2], expected: -2 },
+ { input: [-4, -2], expected: 2 },
+
+ // 64-bit normals that can not be exactly represented
+ ...kDivisionInterval64BitsNormalCases[trait],
+
+ // Denominator out of range
+ { input: [1, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [1, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [1, constants.positive.max], expected: kUnboundedBounds },
+ { input: [1, constants.negative.min], expected: kUnboundedBounds },
+ { input: [1, 0], expected: kUnboundedBounds },
+ { input: [1, constants.positive.subnormal.max], expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ // This is a ULP based interval, so abstract should behave like f32, so
+ // swizzling the trait as needed for calculating the expected result.
+ const trait = t.params.trait === 'abstract' ? 'f32' : t.params.trait;
+ const fp = FP[trait];
+
+ const error = (n: number): number => {
+ return 2.5 * fp.oneULP(n);
+ };
+
+ const [x, y] = t.params.input;
+
+ // Do not swizzle here, so the correct implementation under test is called.
+ const expected = FP[t.params.trait].toInterval(applyError(t.params.expected, error));
+ const got = FP[t.params.trait].divisionInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.divisionInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+const kLdexpIntervalCases = {
+ f32: [
+ // 64-bit normals
+ { input: [1.0000000001, 1], expected: [2, kPlusNULPFunctions['f32'](2, 2)] }, // ~2, additional ULP error due to first param not being f32 precise
+ { input: [-1.0000000001, 1], expected: [kMinusNULPFunctions['f32'](-2, 2), -2] }, // ~-2, additional ULP error due to first param not being f32 precise
+ // Edge Cases
+ // f32 0b0_01111111_11111111111111111111111 = 1.9999998807907104,
+ // 1.9999998807907104 * 2 ** 127 = f32.positive.max
+ { input: [1.9999998807907104, 127], expected: kValue.f32.positive.max },
+ // f32.positive.min = 1 * 2 ** -126
+ { input: [1, -126], expected: kValue.f32.positive.min },
+ // f32.positive.subnormal.max = 0.9999998807907104 * 2 ** -126
+ { input: [0.9999998807907104, -126], expected: [0, kValue.f32.positive.subnormal.max] },
+ // f32.positive.subnormal.min = 1.1920928955078125e-07 * 2 ** -126
+ { input: [1.1920928955078125e-7, -126], expected: [0, kValue.f32.positive.subnormal.min] },
+ { input: [-1.1920928955078125e-7, -126], expected: [kValue.f32.negative.subnormal.max, 0] },
+ { input: [-0.9999998807907104, -126], expected: [kValue.f32.negative.subnormal.min, 0] },
+ { input: [-1, -126], expected: kValue.f32.negative.max },
+ { input: [-1.9999998807907104, 127], expected: kValue.f32.negative.min },
+ // e2 + bias <= 0, expect correctly rounded intervals.
+ { input: [2 ** 120, -130], expected: 2 ** -10 },
+ // Out of Bounds
+ { input: [1, 128], expected: kUnboundedBounds },
+ { input: [-1, 128], expected: kUnboundedBounds },
+ { input: [100, 126], expected: kUnboundedBounds },
+ { input: [-100, 126], expected: kUnboundedBounds },
+ { input: [2 ** 100, 100], expected: kUnboundedBounds },
+ ] as ScalarPairToIntervalCase[],
+ f16: [
+ // 64-bit normals
+ { input: [1.0000000001, 1], expected: [2, kPlusNULPFunctions['f16'](2, 2)] }, // ~2, additional ULP error due to first param not being f16 precise
+ { input: [-1.0000000001, 1], expected: [kMinusNULPFunctions['f16'](-2, 2), -2] }, // ~-2, additional ULP error due to first param not being f16 precise
+ // Edge Cases
+ // f16 0b0_01111_1111111111 = 1.9990234375, 1.9990234375 * 2 ** 15 = f16.positive.max
+ { input: [1.9990234375, 15], expected: kValue.f16.positive.max },
+ // f16.positive.min = 1 * 2 ** -14
+ { input: [1, -14], expected: kValue.f16.positive.min },
+ // f16.positive.subnormal.max = 0.9990234375 * 2 ** -14
+ { input: [0.9990234375, -14], expected: [0, kValue.f16.positive.subnormal.max] },
+ // f16.positive.subnormal.min = 1 * 2 ** -10 * 2 ** -14 = 0.0009765625 * 2 ** -14
+ { input: [0.0009765625, -14], expected: [0, kValue.f16.positive.subnormal.min] },
+ { input: [-0.0009765625, -14], expected: [kValue.f16.negative.subnormal.max, 0] },
+ { input: [-0.9990234375, -14], expected: [kValue.f16.negative.subnormal.min, 0] },
+ { input: [-1, -14], expected: kValue.f16.negative.max },
+ { input: [-1.9990234375, 15], expected: kValue.f16.negative.min },
+ // e2 + bias <= 0, expect correctly rounded intervals.
+ { input: [2 ** 12, -18], expected: 2 ** -6 },
+ // Out of Bounds
+ { input: [1, 16], expected: kUnboundedBounds },
+ { input: [-1, 16], expected: kUnboundedBounds },
+ { input: [100, 14], expected: kUnboundedBounds },
+ { input: [-100, 14], expected: kUnboundedBounds },
+ { input: [2 ** 10, 10], expected: kUnboundedBounds },
+ ] as ScalarPairToIntervalCase[],
+} as const;
+
+g.test('ldexpInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarPairToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // always exactly represeantable cases
+ { input: [0, 0], expected: 0 },
+ { input: [0, 1], expected: 0 },
+ { input: [0, -1], expected: 0 },
+ { input: [1, 1], expected: 2 },
+ { input: [1, -1], expected: 0.5 },
+ { input: [-1, 1], expected: -2 },
+ { input: [-1, -1], expected: -0.5 },
+
+ ...kLdexpIntervalCases[p.trait],
+
+ // Extremely negative e2, any float value should be scale to 0.0 as the ground truth
+ // f64 e1 * 2 ** e2 would be 0.0 for e2 = -2147483648.
+ { input: [constants.positive.max, kValue.i32.negative.min], expected: 0 },
+ { input: [constants.negative.min, kValue.i32.negative.min], expected: 0 },
+ // Out of Bounds
+ { input: [constants.positive.max, kValue.i32.positive.max], expected: kUnboundedBounds },
+ { input: [constants.negative.min, kValue.i32.positive.max], expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.ldexpInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.ldexpInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('maxInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarPairToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // Representable normals
+ { input: [0, 0], expected: 0 },
+ { input: [1, 0], expected: 1 },
+ { input: [0, 1], expected: 1 },
+ { input: [-1, 0], expected: 0 },
+ { input: [0, -1], expected: 0 },
+ { input: [1, 1], expected: 1 },
+ { input: [1, -1], expected: 1 },
+ { input: [-1, 1], expected: 1 },
+ { input: [-1, -1], expected: -1 },
+
+ // 0.1 and -0.1 should be correctly rounded
+ { input: [-0.1, 0], expected: 0 },
+ { input: [0, -0.1], expected: 0 },
+ { input: [0.1, 0], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1'] }, // ~0.1
+ { input: [0, 0.1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1'] }, // ~0.1
+ { input: [0.1, 0.1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1'] }, // ~0.1
+ { input: [0.1, -0.1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1'] }, // ~0.1
+ { input: [-0.1, 0.1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1'] }, // ~0.1
+ { input: [-0.1, -0.1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'] }, // ~-0.1
+
+ // Representable subnormals
+ { input: [constants.positive.subnormal.max, 0], expected: [0, constants.positive.subnormal.max] },
+ { input: [0, constants.positive.subnormal.max], expected: [0, constants.positive.subnormal.max] },
+ { input: [constants.positive.subnormal.min, 0], expected: [0, constants.positive.subnormal.min] },
+ { input: [0, constants.positive.subnormal.min], expected: [0, constants.positive.subnormal.min] },
+ { input: [constants.negative.subnormal.max, 0], expected: [constants.negative.subnormal.max, 0] },
+ { input: [0, constants.negative.subnormal.max], expected: [constants.negative.subnormal.max, 0] },
+ { input: [constants.negative.subnormal.min, 0], expected: [constants.negative.subnormal.min, 0] },
+ { input: [0, constants.negative.subnormal.min], expected: [constants.negative.subnormal.min, 0] },
+ { input: [1, constants.positive.subnormal.max], expected: 1 },
+ { input: [constants.negative.subnormal.min, constants.positive.subnormal.max], expected: [constants.negative.subnormal.min, constants.positive.subnormal.max] },
+
+ // Infinities
+ { input: [0, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, 0], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [0, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, 0], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const [x, y] = t.params.input;
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.maxInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.maxInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('minInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarPairToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // Representable normals
+ { input: [0, 0], expected: 0 },
+ { input: [1, 0], expected: 0 },
+ { input: [0, 1], expected: 0 },
+ { input: [-1, 0], expected: -1 },
+ { input: [0, -1], expected: -1 },
+ { input: [1, 1], expected: 1 },
+ { input: [1, -1], expected: -1 },
+ { input: [-1, 1], expected: -1 },
+ { input: [-1, -1], expected: -1 },
+
+ // 64-bit normals that not exactly representable
+ { input: [0.1, 0], expected: 0 },
+ { input: [0, 0.1], expected: 0 },
+ { input: [-0.1, 0], expected: kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'] }, // ~-0.1
+ { input: [0, -0.1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'] }, // ~-0.1
+ { input: [0.1, 0.1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1'] }, // ~0.1
+ { input: [0.1, -0.1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'] }, // ~-0.1
+ { input: [-0.1, 0.1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'] }, // ~-0.1
+ { input: [-0.1, -0.1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'] }, // ~-0.1
+
+ // Representable subnormals
+ { input: [constants.positive.subnormal.max, 0], expected: [0, constants.positive.subnormal.max] },
+ { input: [0, constants.positive.subnormal.max], expected: [0, constants.positive.subnormal.max] },
+ { input: [constants.positive.subnormal.min, 0], expected: [0, constants.positive.subnormal.min] },
+ { input: [0, constants.positive.subnormal.min], expected: [0, constants.positive.subnormal.min] },
+ { input: [constants.negative.subnormal.max, 0], expected: [constants.negative.subnormal.max, 0] },
+ { input: [0, constants.negative.subnormal.max], expected: [constants.negative.subnormal.max, 0] },
+ { input: [constants.negative.subnormal.min, 0], expected: [constants.negative.subnormal.min, 0] },
+ { input: [0, constants.negative.subnormal.min], expected: [constants.negative.subnormal.min, 0] },
+ { input: [-1, constants.positive.subnormal.max], expected: -1 },
+ { input: [constants.negative.subnormal.min, constants.positive.subnormal.max], expected: [constants.negative.subnormal.min, constants.positive.subnormal.max] },
+
+ // Infinities
+ { input: [0, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, 0], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [0, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, 0], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const [x, y] = t.params.input;
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.minInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.minInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// prettier-ignore
+const kMultiplicationInterval64BitsNormalCases = {
+ f32: [
+ // 0.1*0.1, 0.1 falls between f32 0x3DCCCCCC and 0x3DCCCCCD,
+ // min result 0x3DCCCCCC*0x3DCCCCCC=0.00999999880790713952713681734167 rounded to f32 0x3C23D708 or 0x3C23D709,
+ // max result 0x3DCCCCCD*0x3DCCCCCD=0.01000000029802322622044605108385 rounded to f32 0x3C23D70A or 0x3C23D70B.
+ { input: [0.1, 0.1], expected: [reinterpretU32AsF32(0x3c23d708), reinterpretU32AsF32(0x3c23d70b)] }, // ~0.01
+ { input: [-0.1, -0.1], expected: [reinterpretU32AsF32(0x3c23d708), reinterpretU32AsF32(0x3c23d70b)] }, // ~0.01
+ // -0.01000000029802322622044605108385 rounded to f32 0xBC23D70B or 0xBC23D70A,
+ // -0.00999999880790713952713681734167 rounded to f32 0xBC23D709 or 0xBC23D708.
+ { input: [0.1, -0.1], expected: [reinterpretU32AsF32(0xbc23d70b), reinterpretU32AsF32(0xbc23d708)] }, // ~-0.01
+ { input: [-0.1, 0.1], expected: [reinterpretU32AsF32(0xbc23d70b), reinterpretU32AsF32(0xbc23d708)] }, // ~-0.01
+ ] as ScalarPairToIntervalCase[],
+ f16: [
+ // 0.1*0.1, 0.1 falls between f16 0x2E66 and 0x2E67,
+ // min result 0x2E66*0x2E66=0.00999511778354644775390625 rounded to f16 0x211E or 0x211F,
+ // max result 0x2E67*0x2E67=0.0100073255598545074462890625 rounded to f16 0x211F or 0x2120.
+ { input: [0.1, 0.1], expected: [reinterpretU16AsF16(0x211e), reinterpretU16AsF16(0x2120)] }, // ~0.01
+ { input: [-0.1, -0.1], expected: [reinterpretU16AsF16(0x211e), reinterpretU16AsF16(0x2120)] }, // ~0.01
+ // -0.0100073255598545074462890625 rounded to f16 0xA120 or 0xA11F,
+ // -0.00999511778354644775390625 rounded to f16 0xA11F or 0xA11E.
+ { input: [0.1, -0.1], expected: [reinterpretU16AsF16(0xa120), reinterpretU16AsF16(0xa11e)] }, // ~-0.01
+ { input: [-0.1, 0.1], expected: [reinterpretU16AsF16(0xa120), reinterpretU16AsF16(0xa11e)] }, // ~-0.01
+ ] as ScalarPairToIntervalCase[],
+ abstract: [
+ // 0.1 isn't exactly representable in f64, but will be quantized to an
+ // exact value when storing to a 'number' (0x3FB999999999999A).
+ // This is why below the expectations are not intervals.
+ // f64 0.1 * 0.1 = 0x3f847ae147ae147c,
+ { input: [0.1, 0.1], expected: reinterpretU64AsF64(0x3f847ae147ae147cn) }, // ~0.01
+ { input: [-0.1, -0.1], expected: reinterpretU64AsF64(0x3f847ae147ae147cn) }, // ~0.01
+ { input: [0.1, -0.1], expected: reinterpretU64AsF64(0xbf847ae147ae147cn) }, // ~-0.01
+ { input: [-0.1, 0.1], expected: reinterpretU64AsF64(0xbf847ae147ae147cn) }, // ~-0.01
+ ] as ScalarPairToIntervalCase[],
+} as const;
+
+g.test('multiplicationInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarPairToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // Representable normals
+ { input: [0, 0], expected: 0 },
+ { input: [1, 0], expected: 0 },
+ { input: [0, 1], expected: 0 },
+ { input: [-1, 0], expected: 0 },
+ { input: [0, -1], expected: 0 },
+ { input: [1, 1], expected: 1 },
+ { input: [1, -1], expected: -1 },
+ { input: [-1, 1], expected: -1 },
+ { input: [-1, -1], expected: 1 },
+ { input: [2, 1], expected: 2 },
+ { input: [1, -2], expected: -2 },
+ { input: [-2, 1], expected: -2 },
+ { input: [-2, -1], expected: 2 },
+ { input: [2, 2], expected: 4 },
+ { input: [2, -2], expected: -4 },
+ { input: [-2, 2], expected: -4 },
+ { input: [-2, -2], expected: 4 },
+
+ // 64-bit normals that can not be exactly represented
+ // Finite values multiply zero result in zero
+ { input: [0.1, 0], expected: 0 },
+ { input: [0, 0.1], expected: 0 },
+ { input: [-0.1, 0], expected: 0 },
+ { input: [0, -0.1], expected: 0 },
+ // Finite value multiply +/-1.0
+ { input: [0.1, 1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1'] },
+ { input: [-1, -0.1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1'] },
+ { input: [-0.1, 1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'] },
+ { input: [-1, 0.1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'] },
+ // Other cases
+ ...kMultiplicationInterval64BitsNormalCases[p.trait],
+
+ // Infinities
+ { input: [0, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [1, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [-1, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [0, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [1, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [-1, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+
+ // Edges
+ { input: [constants.positive.max, constants.positive.max], expected: kUnboundedBounds },
+ { input: [constants.negative.min, constants.negative.min], expected: kUnboundedBounds },
+ { input: [constants.positive.max, constants.negative.min], expected: kUnboundedBounds },
+ { input: [constants.negative.min, constants.positive.max], expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.multiplicationInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.multiplicationInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Some of these are hard coded, since the error intervals are difficult to express in a closed
+// human-readable form due to the inherited nature of the errors.
+// prettier-ignore
+const kPowIntervalCases = {
+ f32 : [
+ { input: [1, 0], expected: [kMinusNULPFunctions['f32'](1, 3), reinterpretU64AsF64(0x3ff0_0000_3000_0000n)] }, // ~1
+ { input: [2, 0], expected: [kMinusNULPFunctions['f32'](1, 3), reinterpretU64AsF64(0x3ff0_0000_3000_0000n)] }, // ~1
+ { input: [kValue.f32.positive.max, 0], expected: [kMinusNULPFunctions['f32'](1, 3), reinterpretU64AsF64(0x3ff0_0000_3000_0000n)] }, // ~1
+ { input: [1, 1], expected: [reinterpretU64AsF64(0x3fef_fffe_dfff_fe00n), reinterpretU64AsF64(0x3ff0_0000_c000_0200n)] }, // ~1
+ { input: [1, 100], expected: [reinterpretU64AsF64(0x3fef_ffba_3fff_3800n), reinterpretU64AsF64(0x3ff0_0023_2000_c800n)] }, // ~1
+ { input: [2, 1], expected: [reinterpretU64AsF64(0x3fff_fffe_a000_0200n), reinterpretU64AsF64(0x4000_0001_0000_0200n)] }, // ~2
+ { input: [2, 2], expected: [reinterpretU64AsF64(0x400f_fffd_a000_0400n), reinterpretU64AsF64(0x4010_0001_a000_0400n)] }, // ~4
+ { input: [10, 10], expected: [reinterpretU64AsF64(0x4202_a04f_51f7_7000n), reinterpretU64AsF64(0x4202_a070_ee08_e000n)] }, // ~10000000000
+ { input: [10, 1], expected: [reinterpretU64AsF64(0x4023_fffe_0b65_8b00n), reinterpretU64AsF64(0x4024_0002_149a_7c00n)] }, // ~10
+ ] as ScalarPairToIntervalCase[],
+ f16 : [
+ { input: [1, 0], expected: [reinterpretU64AsF64(0x3fef_fc00_0000_0000n), reinterpretU64AsF64(0x3ff0_0200_0000_0000n)] }, // ~1
+ { input: [2, 0], expected: [reinterpretU64AsF64(0x3fef_fc00_0000_0000n), reinterpretU64AsF64(0x3ff0_0200_0000_0000n)] }, // ~1
+ { input: [kValue.f16.positive.max, 0], expected: [reinterpretU64AsF64(0x3fef_fc00_0000_0000n), reinterpretU64AsF64(0x3ff0_0200_0000_0000n)] }, // ~1
+ { input: [1, 1], expected: [reinterpretU64AsF64(0x3fef_cbf0_0000_0000n), reinterpretU64AsF64(0x3ff0_1c10_0000_0000n)] }, // ~1
+ { input: [1, 100], expected: [reinterpretU64AsF64(0x3fe2_91c0_0000_0000n), reinterpretU64AsF64(0x3ffb_8a40_0000_0000n)] }, // ~1
+ { input: [2, 1], expected: [reinterpretU64AsF64(0x3fff_c410_0000_0000n), reinterpretU64AsF64(0x4000_2410_0000_0000n)] }, // ~2
+ { input: [2, 2], expected: [reinterpretU64AsF64(0x400f_9020_0000_0000n), reinterpretU64AsF64(0x4010_4420_0000_0000n)] }, // ~4
+ { input: [5, 5], expected: [reinterpretU64AsF64(0x40a7_5f70_0000_0000n), reinterpretU64AsF64(0x40a9_5520_0000_0000n)] }, // ~3125
+ { input: [10, 1], expected: [reinterpretU64AsF64(0x4023_c57c_0000_0000n), reinterpretU64AsF64(0x4024_36a0_0000_0000n)] }, // ~10
+ ] as ScalarPairToIntervalCase[],
+} as const;
+
+g.test('powInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarPairToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ { input: [-1, 0], expected: kUnboundedBounds },
+ { input: [0, 0], expected: kUnboundedBounds },
+ { input: [0, 1], expected: kUnboundedBounds },
+ { input: [1, constants.positive.max], expected: kUnboundedBounds },
+ { input: [constants.positive.max, 1], expected: kUnboundedBounds },
+
+ ...kPowIntervalCases[p.trait],
+ ];
+ })
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.powInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.powInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// prettier-ignore
+const kRemainderCases = {
+ f32: [
+ { input: [1, 0.1], expected: [reinterpretU32AsF32(0xb4000000), reinterpretU32AsF32(0x3dccccd8)] }, // ~[0, 0.1]
+ { input: [-1, 0.1], expected: [reinterpretU32AsF32(0xbdccccd8), reinterpretU32AsF32(0x34000000)] }, // ~[-0.1, 0]
+ { input: [1, -0.1], expected: [reinterpretU32AsF32(0xb4000000), reinterpretU32AsF32(0x3dccccd8)] }, // ~[0, 0.1]
+ { input: [-1, -0.1], expected: [reinterpretU32AsF32(0xbdccccd8), reinterpretU32AsF32(0x34000000)] }, // ~[-0.1, 0]
+ ] as ScalarPairToIntervalCase[],
+ f16: [
+ { input: [1, 0.1], expected: [reinterpretU16AsF16(0x9400), reinterpretU16AsF16(0x2e70)] }, // ~[0, 0.1]
+ { input: [-1, 0.1], expected: [reinterpretU16AsF16(0xae70), reinterpretU16AsF16(0x1400)] }, // ~[-0.1, 0]
+ { input: [1, -0.1], expected: [reinterpretU16AsF16(0x9400), reinterpretU16AsF16(0x2e70)] }, // ~[0, 0.1]
+ { input: [-1, -0.1], expected: [reinterpretU16AsF16(0xae70), reinterpretU16AsF16(0x1400)] }, // ~[-0.1, 0]
+ ] as ScalarPairToIntervalCase[],
+} as const;
+
+g.test('remainderInterval')
+ .params(u =>
+ u
+ .combine('trait', ['abstract', 'f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarPairToIntervalCase>(p => {
+ const trait = kFPTraitForULP[p.trait];
+ const constants = FP[trait].constants();
+
+ // prettier-ignore
+ return [
+ ...kRemainderCases[trait],
+ // Normals
+ { input: [0, 1], expected: 0 },
+ { input: [0, -1], expected: 0 },
+ { input: [1, 1], expected: [0, 1] },
+ { input: [1, -1], expected: [0, 1] },
+ { input: [-1, 1], expected: [-1, 0] },
+ { input: [-1, -1], expected: [-1, 0] },
+ { input: [4, 2], expected: [0, 2] },
+ { input: [-4, 2], expected: [-2, 0] },
+ { input: [4, -2], expected: [0, 2] },
+ { input: [-4, -2], expected: [-2, 0] },
+ { input: [2, 4], expected: [2, 2] },
+ { input: [-2, 4], expected: -2 },
+ { input: [2, -4], expected: 2 },
+ { input: [-2, -4], expected: [-2, -2] },
+ { input: [0, 0.1], expected: 0 },
+ { input: [0, -0.1], expected: 0 },
+ { input: [8.5, 2], expected: 0.5 },
+ { input: [1.125, 1], expected: 0.125 },
+
+ // Denominator out of range
+ { input: [1, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [1, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [1, constants.positive.max], expected: kUnboundedBounds },
+ { input: [1, constants.negative.min], expected: kUnboundedBounds },
+ { input: [1, 0], expected: kUnboundedBounds },
+ { input: [1, constants.positive.subnormal.max], expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const [x, y] = t.params.input;
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.remainderInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.remainderInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('stepInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarPairToIntervalCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ return [
+ // 32-bit normals
+ { input: [0, 0], expected: 1 },
+ { input: [1, 1], expected: 1 },
+ { input: [0, 1], expected: 1 },
+ { input: [1, 0], expected: 0 },
+ { input: [-1, -1], expected: 1 },
+ { input: [0, -1], expected: 0 },
+ { input: [-1, 0], expected: 1 },
+ { input: [-1, 1], expected: 1 },
+ { input: [1, -1], expected: 0 },
+
+ // 64-bit normals
+ { input: [0.1, 0.1], expected: [0, 1] },
+ { input: [0, 0.1], expected: 1 },
+ { input: [0.1, 0], expected: 0 },
+ { input: [0.1, 1], expected: 1 },
+ { input: [1, 0.1], expected: 0 },
+ { input: [-0.1, -0.1], expected: [0, 1] },
+ { input: [0, -0.1], expected: 0 },
+ { input: [-0.1, 0], expected: 1 },
+ { input: [-0.1, -1], expected: 0 },
+ { input: [-1, -0.1], expected: 1 },
+
+ // Subnormals
+ { input: [0, constants.positive.subnormal.max], expected: 1 },
+ { input: [0, constants.positive.subnormal.min], expected: 1 },
+ { input: [0, constants.negative.subnormal.max], expected: [0, 1] },
+ { input: [0, constants.negative.subnormal.min], expected: [0, 1] },
+ { input: [1, constants.positive.subnormal.max], expected: 0 },
+ { input: [1, constants.positive.subnormal.min], expected: 0 },
+ { input: [1, constants.negative.subnormal.max], expected: 0 },
+ { input: [1, constants.negative.subnormal.min], expected: 0 },
+ { input: [-1, constants.positive.subnormal.max], expected: 1 },
+ { input: [-1, constants.positive.subnormal.min], expected: 1 },
+ { input: [-1, constants.negative.subnormal.max], expected: 1 },
+ { input: [-1, constants.negative.subnormal.min], expected: 1 },
+ { input: [constants.positive.subnormal.max, 0], expected: [0, 1] },
+ { input: [constants.positive.subnormal.min, 0], expected: [0, 1] },
+ { input: [constants.negative.subnormal.max, 0], expected: 1 },
+ { input: [constants.negative.subnormal.min, 0], expected: 1 },
+ { input: [constants.positive.subnormal.max, 1], expected: 1 },
+ { input: [constants.positive.subnormal.min, 1], expected: 1 },
+ { input: [constants.negative.subnormal.max, 1], expected: 1 },
+ { input: [constants.negative.subnormal.min, 1], expected: 1 },
+ { input: [constants.positive.subnormal.max, -1], expected: 0 },
+ { input: [constants.positive.subnormal.min, -1], expected: 0 },
+ { input: [constants.negative.subnormal.max, -1], expected: 0 },
+ { input: [constants.negative.subnormal.min, -1], expected: 0 },
+ { input: [constants.negative.subnormal.min, constants.positive.subnormal.max], expected: 1 },
+ { input: [constants.positive.subnormal.max, constants.negative.subnormal.min], expected: [0, 1] },
+
+ // Infinities
+ { input: [0, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, 0], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [0, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, 0], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const [edge, x] = t.params.input;
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.stepInterval(edge, x);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.stepInterval(${edge}, ${x}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// prettier-ignore
+const kSubtractionInterval64BitsNormalCases = {
+ f32: [
+ // 0.1 falls between f32 0x3DCCCCCC and 0x3DCCCCCD, -0.1 falls between f32 0xBDCCCCCD and 0xBDCCCCCC
+ // Expect f32 interval [0x3DCCCCCC-0x3DCCCCCD, 0x3DCCCCCD-0x3DCCCCCC]
+ { input: [0.1, 0.1], expected: [reinterpretU32AsF32(0x3dcccccc)-reinterpretU32AsF32(0x3dcccccd), reinterpretU32AsF32(0x3dcccccd)-reinterpretU32AsF32(0x3dcccccc)] },
+ // Expect f32 interval [0xBDCCCCCD-0xBDCCCCCC, 0xBDCCCCCC-0xBDCCCCCD]
+ { input: [-0.1, -0.1], expected: [reinterpretU32AsF32(0xbdcccccd)-reinterpretU32AsF32(0xbdcccccc), reinterpretU32AsF32(0xbdcccccc)-reinterpretU32AsF32(0xbdcccccd)] },
+ // Expect f32 interval [0x3DCCCCCC-0xBDCCCCCC, 0x3DCCCCCD-0xBDCCCCCD]
+ { input: [0.1, -0.1], expected: [reinterpretU32AsF32(0x3dcccccc)-reinterpretU32AsF32(0xbdcccccc), reinterpretU32AsF32(0x3dcccccd)-reinterpretU32AsF32(0xbdcccccd)] },
+ // Expect f32 interval [0xBDCCCCCD-0x3DCCCCCD, 0xBDCCCCCC-0x3DCCCCCC]
+ { input: [-0.1, 0.1], expected: [reinterpretU32AsF32(0xbdcccccd)-reinterpretU32AsF32(0x3dcccccd), reinterpretU32AsF32(0xbdcccccc)-reinterpretU32AsF32(0x3dcccccc)] },
+ ] as ScalarPairToIntervalCase[],
+ f16: [
+ // 0.1 falls between f16 0x2E66 and 0x2E67, -0.1 falls between f16 0xAE67 and 0xAE66
+ // Expect f16 interval [0x2E66-0x2E67, 0x2E67-0x2E66]
+ { input: [0.1, 0.1], expected: [reinterpretU16AsF16(0x2e66)-reinterpretU16AsF16(0x2e67), reinterpretU16AsF16(0x2e67)-reinterpretU16AsF16(0x2e66)] },
+ // Expect f16 interval [0xAE67-0xAE66, 0xAE66-0xAE67]
+ { input: [-0.1, -0.1], expected: [reinterpretU16AsF16(0xae67)-reinterpretU16AsF16(0xae66), reinterpretU16AsF16(0xae66)-reinterpretU16AsF16(0xae67)] },
+ // Expect f16 interval [0x2E66-0xAE66, 0x2E67-0xAE67]
+ { input: [0.1, -0.1], expected: [reinterpretU16AsF16(0x2e66)-reinterpretU16AsF16(0xae66), reinterpretU16AsF16(0x2e67)-reinterpretU16AsF16(0xae67)] },
+ // Expect f16 interval [0xAE67-0x2E67, 0xAE66-0x2E66]
+ { input: [-0.1, 0.1], expected: [reinterpretU16AsF16(0xae67)-reinterpretU16AsF16(0x2e67), reinterpretU16AsF16(0xae66)-reinterpretU16AsF16(0x2e66)] },
+ ] as ScalarPairToIntervalCase[],
+ abstract: [
+ // 0.1 isn't exactly representable in f64, but will be quantized to an
+ // exact value when storing to a 'number' (0x3FB999999999999A).
+ // This is why below the expectations are not intervals.
+ { input: [0.1, 0.1], expected: 0 },
+ { input: [-0.1, -0.1], expected: 0 },
+ // f64 0x3FB999999999999A - 0xBFB999999999999A = 0x3FC999999999999A
+ { input: [0.1, -0.1], expected: reinterpretU64AsF64(0x3fc999999999999an) }, // ~0.2
+ // f64 0xBFB999999999999A - 0x3FB999999999999A = 0xBFC999999999999A
+ { input: [-0.1, 0.1], expected: reinterpretU64AsF64(0xbfc999999999999an) }, // ~-0.2,
+ ] as ScalarPairToIntervalCase[],
+} as const;
+
+g.test('subtractionInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarPairToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // Representable normals
+ { input: [0, 0], expected: 0 },
+ { input: [1, 0], expected: 1 },
+ { input: [0, 1], expected: -1 },
+ { input: [-1, 0], expected: -1 },
+ { input: [0, -1], expected: 1 },
+ { input: [1, 1], expected: 0 },
+ { input: [1, -1], expected: 2 },
+ { input: [-1, 1], expected: -2 },
+ { input: [-1, -1], expected: 0 },
+
+ // 64-bit normals that can not be exactly represented in f32/f16
+ { input: [0.1, 0], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1'] },
+ { input: [0, -0.1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1'] },
+ { input: [-0.1, 0], expected: kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'] },
+ { input: [0, 0.1], expected: kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'] },
+ ...kSubtractionInterval64BitsNormalCases[p.trait],
+
+ // Subnormals
+ { input: [constants.positive.subnormal.max, 0], expected: [0, constants.positive.subnormal.max] },
+ { input: [0, constants.positive.subnormal.max], expected: [constants.negative.subnormal.min, 0] },
+ { input: [constants.positive.subnormal.min, 0], expected: [0, constants.positive.subnormal.min] },
+ { input: [0, constants.positive.subnormal.min], expected: [constants.negative.subnormal.max, 0] },
+ { input: [constants.negative.subnormal.max, 0], expected: [constants.negative.subnormal.max, 0] },
+ { input: [0, constants.negative.subnormal.max], expected: [0, constants.positive.subnormal.min] },
+ { input: [constants.negative.subnormal.min, 0], expected: [constants.negative.subnormal.min, 0] },
+ { input: [0, constants.negative.subnormal.min], expected: [0, constants.positive.subnormal.max] },
+
+ // Infinities
+ { input: [0, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, 0], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [0, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, 0], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.subtractionInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.subtractionInterval(${x}, ${y}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface ScalarTripleToIntervalCase {
+ input: [number, number, number];
+ expected: number | IntervalBounds;
+}
+
+g.test('clampMedianInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarTripleToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // Normals
+ { input: [0, 0, 0], expected: 0 },
+ { input: [1, 0, 0], expected: 0 },
+ { input: [0, 1, 0], expected: 0 },
+ { input: [0, 0, 1], expected: 0 },
+ { input: [1, 0, 1], expected: 1 },
+ { input: [1, 1, 0], expected: 1 },
+ { input: [0, 1, 1], expected: 1 },
+ { input: [1, 1, 1], expected: 1 },
+ { input: [1, 10, 100], expected: 10 },
+ { input: [10, 1, 100], expected: 10 },
+ { input: [100, 1, 10], expected: 10 },
+ { input: [-10, 1, 100], expected: 1 },
+ { input: [10, 1, -100], expected: 1 },
+ { input: [-10, 1, -100], expected: -10 },
+ { input: [-10, -10, -10], expected: -10 },
+
+ // Subnormals
+ { input: [constants.positive.subnormal.max, 0, 0], expected: 0 },
+ { input: [0, constants.positive.subnormal.max, 0], expected: 0 },
+ { input: [0, 0, constants.positive.subnormal.max], expected: 0 },
+ { input: [constants.positive.subnormal.max, 0, constants.positive.subnormal.max], expected: [0, constants.positive.subnormal.max] },
+ { input: [constants.positive.subnormal.max, constants.positive.subnormal.max, 0], expected: [0, constants.positive.subnormal.max] },
+ { input: [0, constants.positive.subnormal.max, constants.positive.subnormal.max], expected: [0, constants.positive.subnormal.max] },
+ { input: [constants.positive.subnormal.max, constants.positive.subnormal.max, constants.positive.subnormal.max], expected: [0, constants.positive.subnormal.max] },
+ { input: [constants.positive.subnormal.max, constants.positive.subnormal.min, constants.negative.subnormal.max], expected: [0, constants.positive.subnormal.min] },
+ { input: [constants.positive.subnormal.max, constants.negative.subnormal.min, constants.negative.subnormal.max], expected: [constants.negative.subnormal.max, 0] },
+ { input: [constants.positive.max, constants.positive.max, constants.positive.subnormal.min], expected: constants.positive.max },
+
+ // Infinities
+ { input: [0, 1, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [0, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const [x, y, z] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.clampMedianInterval(x, y, z);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.clampMedianInterval(${x}, ${y}, ${z}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+g.test('clampMinMaxInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarTripleToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // Normals
+ { input: [0, 0, 0], expected: 0 },
+ { input: [1, 0, 0], expected: 0 },
+ { input: [0, 1, 0], expected: 0 },
+ { input: [0, 0, 1], expected: 0 },
+ { input: [1, 0, 1], expected: 1 },
+ { input: [1, 1, 0], expected: 0 },
+ { input: [0, 1, 1], expected: 1 },
+ { input: [1, 1, 1], expected: 1 },
+ { input: [1, 10, 100], expected: 10 },
+ { input: [10, 1, 100], expected: 10 },
+ { input: [100, 1, 10], expected: 10 },
+ { input: [-10, 1, 100], expected: 1 },
+ { input: [10, 1, -100], expected: -100 },
+ { input: [-10, 1, -100], expected: -100 },
+ { input: [-10, -10, -10], expected: -10 },
+
+ // Subnormals
+ { input: [constants.positive.subnormal.max, 0, 0], expected: [0, constants.positive.subnormal.max] },
+ { input: [0, constants.positive.subnormal.max, 0], expected: [0, constants.positive.subnormal.max] },
+ { input: [0, 0, constants.positive.subnormal.max], expected: [0, constants.positive.subnormal.max] },
+ { input: [constants.positive.subnormal.max, 0, constants.positive.subnormal.max], expected: [0, constants.positive.subnormal.max] },
+ { input: [constants.positive.subnormal.max, constants.positive.subnormal.max, 0], expected: [0, constants.positive.subnormal.max] },
+ { input: [0, constants.positive.subnormal.max, constants.positive.subnormal.max], expected: [0, constants.positive.subnormal.max] },
+ { input: [constants.positive.subnormal.max, constants.positive.subnormal.max, constants.positive.subnormal.max], expected: [0, constants.positive.subnormal.max] },
+ { input: [constants.positive.subnormal.max, constants.positive.subnormal.min, constants.negative.subnormal.max], expected: [constants.negative.subnormal.max, constants.positive.subnormal.max] },
+ { input: [constants.positive.subnormal.max, constants.negative.subnormal.min, constants.negative.subnormal.max], expected: [constants.negative.subnormal.min, constants.positive.subnormal.max] },
+ { input: [constants.positive.max, constants.positive.max, constants.positive.subnormal.min], expected: [0, constants.positive.subnormal.min] },
+
+ // Infinities
+ { input: [0, 1, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [0, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const [x, y, z] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.clampMinMaxInterval(x, y, z);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.clampMinMaxInterval(${x}, ${y}, ${z}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// prettier-ignore
+const kFmaIntervalCases = {
+ f32: [
+ // positive.subnormal.max * positive.subnormal.max is much smaller than positive.subnormal.min but larger than 0, rounded to [0, positive.subnormal.min]
+ { input: [kValue.f32.positive.subnormal.max, kValue.f32.positive.subnormal.max, 0], expected: [0, kValue.f32.positive.subnormal.min] },
+ // positive.subnormal.max * positive.subnormal.max rounded to 0 or positive.subnormal.min,
+ // 0 + constants.positive.subnormal.max rounded to [0, constants.positive.subnormal.max],
+ // positive.subnormal.min + constants.positive.subnormal.max = constants.positive.min.
+ { input: [kValue.f32.positive.subnormal.max, kValue.f32.positive.subnormal.max, kValue.f32.positive.subnormal.max], expected: [0, kValue.f32.positive.min] },
+ // positive.subnormal.max * positive.subnormal.max rounded to 0 or positive.subnormal.min,
+ // negative.subnormal.max may flushed to 0,
+ // minimum case: 0 + negative.subnormal.max rounded to [negative.subnormal.max, 0],
+ // maximum case: positive.subnormal.min + 0 rounded to [0, positive.subnormal.min].
+ { input: [kValue.f32.positive.subnormal.max, kValue.f32.positive.subnormal.min, kValue.f32.negative.subnormal.max], expected: [kValue.f32.negative.subnormal.max, kValue.f32.positive.subnormal.min] },
+ // positive.subnormal.max * negative.subnormal.min rounded to -0.0 or negative.subnormal.max = -1 * [subnormal ulp],
+ // negative.subnormal.max = -1 * [subnormal ulp] may flushed to -0.0,
+ // minimum case: -1 * [subnormal ulp] + -1 * [subnormal ulp] rounded to [-2 * [subnormal ulp], 0],
+ // maximum case: -0.0 + -0.0 = 0.
+ { input: [kValue.f32.positive.subnormal.max, kValue.f32.negative.subnormal.min, kValue.f32.negative.subnormal.max], expected: [-2 * FP['f32'].oneULP(0, 'no-flush'), 0] },
+ ] as ScalarTripleToIntervalCase[],
+ f16: [
+ // positive.subnormal.max * positive.subnormal.max is much smaller than positive.subnormal.min but larger than 0, rounded to [0, positive.subnormal.min]
+ { input: [kValue.f16.positive.subnormal.max, kValue.f16.positive.subnormal.max, 0], expected: [0, kValue.f16.positive.subnormal.min] },
+ // positive.subnormal.max * positive.subnormal.max rounded to 0 or positive.subnormal.min,
+ // 0 + constants.positive.subnormal.max rounded to [0, constants.positive.subnormal.max],
+ // positive.subnormal.min + constants.positive.subnormal.max = constants.positive.min.
+ { input: [kValue.f16.positive.subnormal.max, kValue.f16.positive.subnormal.max, kValue.f16.positive.subnormal.max], expected: [0, kValue.f16.positive.min] },
+ // positive.subnormal.max * positive.subnormal.max rounded to 0 or positive.subnormal.min,
+ // negative.subnormal.max may flushed to 0,
+ // minimum case: 0 + negative.subnormal.max rounded to [negative.subnormal.max, 0],
+ // maximum case: positive.subnormal.min + 0 rounded to [0, positive.subnormal.min].
+ { input: [kValue.f16.positive.subnormal.max, kValue.f16.positive.subnormal.min, kValue.f16.negative.subnormal.max], expected: [kValue.f16.negative.subnormal.max, kValue.f16.positive.subnormal.min] },
+ // positive.subnormal.max * negative.subnormal.min rounded to -0.0 or negative.subnormal.max = -1 * [subnormal ulp],
+ // negative.subnormal.max = -1 * [subnormal ulp] may flushed to -0.0,
+ // minimum case: -1 * [subnormal ulp] + -1 * [subnormal ulp] rounded to [-2 * [subnormal ulp], 0],
+ // maximum case: -0.0 + -0.0 = 0.
+ { input: [kValue.f16.positive.subnormal.max, kValue.f16.negative.subnormal.min, kValue.f16.negative.subnormal.max], expected: [-2 * FP['f16'].oneULP(0, 'no-flush'), 0] }, ] as ScalarTripleToIntervalCase[],
+ abstract: [
+ // These operations break down in the CTS, because `number` is a f64 under the hood, so precision is sometimes lost
+ // if intermediate results are closer to 0 than the smallest subnormal will be precisely 0.
+ // See https://github.com/gpuweb/cts/issues/2993 for details
+ { input: [kValue.f64.positive.subnormal.max, kValue.f64.positive.subnormal.max, 0], expected: 0 },
+ { input: [kValue.f64.positive.subnormal.max, kValue.f64.positive.subnormal.max, kValue.f64.positive.subnormal.max], expected: [0, kValue.f64.positive.subnormal.max] },
+ { input: [kValue.f64.positive.subnormal.max, kValue.f64.positive.subnormal.min, kValue.f64.negative.subnormal.max], expected: [kValue.f64.negative.subnormal.max, 0] },
+ { input: [kValue.f64.positive.subnormal.max, kValue.f64.negative.subnormal.min, kValue.f64.negative.subnormal.max], expected: [kValue.f64.negative.subnormal.max, 0] },
+ ] as ScalarTripleToIntervalCase[],
+} as const;
+
+g.test('fmaInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarTripleToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // Normals
+ { input: [0, 0, 0], expected: 0 },
+ { input: [1, 0, 0], expected: 0 },
+ { input: [0, 1, 0], expected: 0 },
+ { input: [0, 0, 1], expected: 1 },
+ { input: [1, 0, 1], expected: 1 },
+ { input: [1, 1, 0], expected: 1 },
+ { input: [0, 1, 1], expected: 1 },
+ { input: [1, 1, 1], expected: 2 },
+ { input: [1, 10, 100], expected: 110 },
+ { input: [10, 1, 100], expected: 110 },
+ { input: [100, 1, 10], expected: 110 },
+ { input: [-10, 1, 100], expected: 90 },
+ { input: [10, 1, -100], expected: -90 },
+ { input: [-10, 1, -100], expected: -110 },
+ { input: [-10, -10, -10], expected: 90 },
+
+ // Subnormals
+ { input: [constants.positive.subnormal.max, 0, 0], expected: 0 },
+ { input: [0, constants.positive.subnormal.max, 0], expected: 0 },
+ { input: [0, 0, constants.positive.subnormal.max], expected: [0, constants.positive.subnormal.max] },
+ { input: [constants.positive.subnormal.max, 0, constants.positive.subnormal.max], expected: [0, constants.positive.subnormal.max] },
+ { input: [0, constants.positive.subnormal.max, constants.positive.subnormal.max], expected: [0, constants.positive.subnormal.max] },
+
+ // Infinities
+ { input: [0, 1, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [0, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.positive.infinity, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.positive.infinity, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.positive.max, constants.positive.max, constants.positive.subnormal.min], expected: kUnboundedBounds },
+ ...kFmaIntervalCases[p.trait],
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.fmaInterval(...t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.fmaInterval(${t.params.input.join(
+ ','
+ )}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Some of these are hard coded, since the error intervals are difficult to express in a closed
+// human-readable form due to the inherited nature of the errors.
+// prettier-ignore
+const kMixImpreciseIntervalCases = {
+ f32: [
+ // [0.0, 1.0] cases
+ { input: [0.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0x3fb9_9999_8000_0000n), reinterpretU64AsF64(0x3fb9_9999_a000_0000n)] }, // ~0.1
+ { input: [0.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fec_cccc_c000_0000n), reinterpretU64AsF64(0x3fec_cccc_e000_0000n)] }, // ~0.9
+ // [1.0, 0.0] cases
+ { input: [1.0, 0.0, 0.1], expected: [reinterpretU64AsF64(0x3fec_cccc_c000_0000n), reinterpretU64AsF64(0x3fec_cccc_e000_0000n)] }, // ~0.9
+ { input: [1.0, 0.0, 0.9], expected: [reinterpretU64AsF64(0x3fb9_9999_0000_0000n), reinterpretU64AsF64(0x3fb9_999a_0000_0000n)] }, // ~0.1
+ // [0.0, 10.0] cases
+ { input: [0.0, 10.0, 0.1], expected: [reinterpretU64AsF64(0x3fef_ffff_e000_0000n), reinterpretU64AsF64(0x3ff0_0000_2000_0000n)] }, // ~1
+ { input: [0.0, 10.0, 0.9], expected: [reinterpretU64AsF64(0x4021_ffff_e000_0000n), reinterpretU64AsF64(0x4022_0000_2000_0000n)] }, // ~9
+ // [2.0, 10.0] cases
+ { input: [2.0, 10.0, 0.1], expected: [reinterpretU64AsF64(0x4006_6666_6000_0000n), reinterpretU64AsF64(0x4006_6666_8000_0000n)] }, // ~2.8
+ { input: [2.0, 10.0, 0.9], expected: [reinterpretU64AsF64(0x4022_6666_6000_0000n), reinterpretU64AsF64(0x4022_6666_8000_0000n)] }, // ~9.2
+ // [-1.0, 1.0] cases
+ { input: [-1.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0xbfe9_9999_a000_0000n), reinterpretU64AsF64(0xbfe9_9999_8000_0000n)] }, // ~-0.8
+ { input: [-1.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fe9_9999_8000_0000n), reinterpretU64AsF64(0x3fe9_9999_c000_0000n)] }, // ~0.8
+
+ // Showing how precise and imprecise versions diff
+ // Note that this expectation is 0 only in f32 as 10.0 is much smaller that f32.negative.min,
+ // So that 10 - f32.negative.min == f32.negative.min even in f64. But for f16, there is not
+ // a exactly-represenatble f16 value v that make v - f16.negative.min == f16.negative.min
+ // in f64, in fact that require v being smaller than 2**-37.
+ { input: [kValue.f32.negative.min, 10.0, 1.0], expected: 0.0 },
+ // -10.0 is the same, much smaller than f32.negative.min
+ { input: [kValue.f32.negative.min, -10.0, 1.0], expected: 0.0 },
+ ] as ScalarTripleToIntervalCase[],
+ f16: [
+ // [0.0, 1.0] cases
+ { input: [0.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0x3fb9_9800_0000_0000n), reinterpretU64AsF64(0x3fb9_9c00_0000_0000n)] }, // ~0.1
+ { input: [0.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fec_cc00_0000_0000n), reinterpretU64AsF64(0x3fec_d000_0000_0000n)] }, // ~0.9
+ // [1.0, 0.0] cases
+ { input: [1.0, 0.0, 0.1], expected: [reinterpretU64AsF64(0x3fec_cc00_0000_0000n), reinterpretU64AsF64(0x3fec_d000_0000_0000n)] }, // ~0.9
+ { input: [1.0, 0.0, 0.9], expected: [reinterpretU64AsF64(0x3fb9_8000_0000_0000n), reinterpretU64AsF64(0x3fb9_a000_0000_0000n)] }, // ~0.1
+ // [0.0, 10.0] cases
+ { input: [0.0, 10.0, 0.1], expected: [reinterpretU64AsF64(0x3fef_fc00_0000_0000n), reinterpretU64AsF64(0x3ff0_0400_0000_0000n)] }, // ~1
+ { input: [0.0, 10.0, 0.9], expected: [reinterpretU64AsF64(0x4021_fc00_0000_0000n), reinterpretU64AsF64(0x4022_0400_0000_0000n)] }, // ~9
+ // [2.0, 10.0] cases
+ { input: [2.0, 10.0, 0.1], expected: [reinterpretU64AsF64(0x4006_6400_0000_0000n), reinterpretU64AsF64(0x4006_6800_0000_0000n)] }, // ~2.8
+ { input: [2.0, 10.0, 0.9], expected: [reinterpretU64AsF64(0x4022_6400_0000_0000n), reinterpretU64AsF64(0x4022_6800_0000_0000n)] }, // ~9.2
+ // [-1.0, 1.0] cases
+ { input: [-1.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0xbfe9_9c00_0000_0000n), reinterpretU64AsF64(0xbfe9_9800_0000_0000n)] }, // ~-0.8
+ { input: [-1.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fe9_9800_0000_0000n), reinterpretU64AsF64(0x3fe9_a000_0000_0000n)] }, // ~0.8
+
+ // Showing how precise and imprecise versions diff
+ // In imprecise version, we compute (y - x), where y = 10 and x = -65504, the result is 65514
+ // and cause an overflow in f16.
+ { input: [kValue.f16.negative.min, 10.0, 1.0], expected: kUnboundedBounds },
+ // (y - x) * 1.0, where y = -10 and x = -65504, the result is 65494 rounded to 65472 or 65504.
+ // The result is -65504 + 65472 = -32 or -65504 + 65504 = 0.
+ { input: [kValue.f16.negative.min, -10.0, 1.0], expected: [-32, 0] },
+ ] as ScalarTripleToIntervalCase[],
+} as const;
+
+g.test('mixImpreciseInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarTripleToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ ...kMixImpreciseIntervalCases[p.trait],
+
+ // [0.0, 1.0] cases
+ { input: [0.0, 1.0, -1.0], expected: -1.0 },
+ { input: [0.0, 1.0, 0.0], expected: 0.0 },
+ { input: [0.0, 1.0, 0.5], expected: 0.5 },
+ { input: [0.0, 1.0, 1.0], expected: 1.0 },
+ { input: [0.0, 1.0, 2.0], expected: 2.0 },
+
+ // [1.0, 0.0] cases
+ { input: [1.0, 0.0, -1.0], expected: 2.0 },
+ { input: [1.0, 0.0, 0.0], expected: 1.0 },
+ { input: [1.0, 0.0, 0.5], expected: 0.5 },
+ { input: [1.0, 0.0, 1.0], expected: 0.0 },
+ { input: [1.0, 0.0, 2.0], expected: -1.0 },
+
+ // [0.0, 10.0] cases
+ { input: [0.0, 10.0, -1.0], expected: -10.0 },
+ { input: [0.0, 10.0, 0.0], expected: 0.0 },
+ { input: [0.0, 10.0, 0.5], expected: 5.0 },
+ { input: [0.0, 10.0, 1.0], expected: 10.0 },
+ { input: [0.0, 10.0, 2.0], expected: 20.0 },
+
+ // [2.0, 10.0] cases
+ { input: [2.0, 10.0, -1.0], expected: -6.0 },
+ { input: [2.0, 10.0, 0.0], expected: 2.0 },
+ { input: [2.0, 10.0, 0.5], expected: 6.0 },
+ { input: [2.0, 10.0, 1.0], expected: 10.0 },
+ { input: [2.0, 10.0, 2.0], expected: 18.0 },
+
+ // [-1.0, 1.0] cases
+ { input: [-1.0, 1.0, -2.0], expected: -5.0 },
+ { input: [-1.0, 1.0, 0.0], expected: -1.0 },
+ { input: [-1.0, 1.0, 0.5], expected: 0.0 },
+ { input: [-1.0, 1.0, 1.0], expected: 1.0 },
+ { input: [-1.0, 1.0, 2.0], expected: 3.0 },
+
+ // Infinities
+ { input: [0.0, constants.positive.infinity, 0.5], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, 0.0, 0.5], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, 1.0, 0.5], expected: kUnboundedBounds },
+ { input: [1.0, constants.negative.infinity, 0.5], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.positive.infinity, 0.5], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, constants.negative.infinity, 0.5], expected: kUnboundedBounds },
+ { input: [0.0, 1.0, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [1.0, 0.0, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [0.0, 1.0, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [1.0, 0.0, constants.positive.infinity], expected: kUnboundedBounds },
+
+ // The [negative.min, +/-10.0, 1.0] cases has different result for different trait on
+ // imprecise version.
+ ];
+ })
+ )
+ .fn(t => {
+ const [x, y, z] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.mixImpreciseInterval(x, y, z);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.mixImpreciseInterval(${x}, ${y}, ${z}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Some of these are hard coded, since the error intervals are difficult to express in a closed
+// human-readable form due to the inherited nature of the errors.
+// prettier-ignore
+const kMixPreciseIntervalCases = {
+ f32: [
+ // [0.0, 1.0] cases
+ { input: [0.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0x3fb9_9999_8000_0000n), reinterpretU64AsF64(0x3fb9_9999_a000_0000n)] }, // ~0.1
+ { input: [0.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fec_cccc_c000_0000n), reinterpretU64AsF64(0x3fec_cccc_e000_0000n)] }, // ~0.9
+ // [1.0, 0.0] cases
+ { input: [1.0, 0.0, 0.1], expected: [reinterpretU64AsF64(0x3fec_cccc_c000_0000n), reinterpretU64AsF64(0x3fec_cccc_e000_0000n)] }, // ~0.9
+ { input: [1.0, 0.0, 0.9], expected: [reinterpretU64AsF64(0x3fb9_9999_0000_0000n), reinterpretU64AsF64(0x3fb9_999a_0000_0000n)] }, // ~0.1
+ // [0.0, 10.0] cases
+ { input: [0.0, 10.0, 0.1], expected: [reinterpretU64AsF64(0x3fef_ffff_e000_0000n), reinterpretU64AsF64(0x3ff0_0000_2000_0000n)] }, // ~1
+ { input: [0.0, 10.0, 0.9], expected: [reinterpretU64AsF64(0x4021_ffff_e000_0000n), reinterpretU64AsF64(0x4022_0000_2000_0000n)] }, // ~9
+ // [2.0, 10.0] cases
+ { input: [2.0, 10.0, 0.1], expected: [reinterpretU64AsF64(0x4006_6666_4000_0000n), reinterpretU64AsF64(0x4006_6666_8000_0000n)] }, // ~2.8
+ { input: [2.0, 10.0, 0.9], expected: [reinterpretU64AsF64(0x4022_6666_4000_0000n), reinterpretU64AsF64(0x4022_6666_a000_0000n)] }, // ~9.2
+ // [-1.0, 1.0] cases
+ { input: [-1.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0xbfe9_9999_c000_0000n), reinterpretU64AsF64(0xbfe9_9999_8000_0000n)] }, // ~-0.8
+ { input: [-1.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fe9_9999_8000_0000n), reinterpretU64AsF64(0x3fe9_9999_c000_0000n)] }, // ~0.8
+ ] as ScalarTripleToIntervalCase[],
+ f16: [
+ // [0.0, 1.0] cases
+ { input: [0.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0x3fb9_9800_0000_0000n), reinterpretU64AsF64(0x3fb9_9c00_0000_0000n)] }, // ~0.1
+ { input: [0.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fec_cc00_0000_0000n), reinterpretU64AsF64(0x3fec_d000_0000_0000n)] }, // ~0.9
+ // [1.0, 0.0] cases
+ { input: [1.0, 0.0, 0.1], expected: [reinterpretU64AsF64(0x3fec_cc00_0000_0000n), reinterpretU64AsF64(0x3fec_d000_0000_0000n)] }, // ~0.9
+ { input: [1.0, 0.0, 0.9], expected: [reinterpretU64AsF64(0x3fb9_8000_0000_0000n), reinterpretU64AsF64(0x3fb9_a000_0000_0000n)] }, // ~0.1
+ // [0.0, 10.0] cases
+ { input: [0.0, 10.0, 0.1], expected: [reinterpretU64AsF64(0x3fef_fc00_0000_0000n), reinterpretU64AsF64(0x3ff0_0400_0000_0000n)] }, // ~1
+ { input: [0.0, 10.0, 0.9], expected: [reinterpretU64AsF64(0x4021_fc00_0000_0000n), reinterpretU64AsF64(0x4022_0400_0000_0000n)] }, // ~9
+ // [2.0, 10.0] cases
+ { input: [2.0, 10.0, 0.1], expected: [reinterpretU64AsF64(0x4006_6400_0000_0000n), reinterpretU64AsF64(0x4006_6c00_0000_0000n)] }, // ~2.8
+ { input: [2.0, 10.0, 0.9], expected: [reinterpretU64AsF64(0x4022_6000_0000_0000n), reinterpretU64AsF64(0x4022_6c00_0000_0000n)] }, // ~9.2
+ // [-1.0, 1.0] cases
+ { input: [-1.0, 1.0, 0.1], expected: [reinterpretU64AsF64(0xbfe9_a000_0000_0000n), reinterpretU64AsF64(0xbfe9_9800_0000_0000n)] }, // ~-0.8
+ { input: [-1.0, 1.0, 0.9], expected: [reinterpretU64AsF64(0x3fe9_9800_0000_0000n), reinterpretU64AsF64(0x3fe9_a000_0000_0000n)] }, // ~0.8
+ ] as ScalarTripleToIntervalCase[],
+} as const;
+
+g.test('mixPreciseInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarTripleToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ ...kMixPreciseIntervalCases[p.trait],
+
+ // [0.0, 1.0] cases
+ { input: [0.0, 1.0, -1.0], expected: -1.0 },
+ { input: [0.0, 1.0, 0.0], expected: 0.0 },
+ { input: [0.0, 1.0, 0.5], expected: 0.5 },
+ { input: [0.0, 1.0, 1.0], expected: 1.0 },
+ { input: [0.0, 1.0, 2.0], expected: 2.0 },
+
+ // [1.0, 0.0] cases
+ { input: [1.0, 0.0, -1.0], expected: 2.0 },
+ { input: [1.0, 0.0, 0.0], expected: 1.0 },
+ { input: [1.0, 0.0, 0.5], expected: 0.5 },
+ { input: [1.0, 0.0, 1.0], expected: 0.0 },
+ { input: [1.0, 0.0, 2.0], expected: -1.0 },
+
+ // [0.0, 10.0] cases
+ { input: [0.0, 10.0, -1.0], expected: -10.0 },
+ { input: [0.0, 10.0, 0.0], expected: 0.0 },
+ { input: [0.0, 10.0, 0.5], expected: 5.0 },
+ { input: [0.0, 10.0, 1.0], expected: 10.0 },
+ { input: [0.0, 10.0, 2.0], expected: 20.0 },
+
+ // [2.0, 10.0] cases
+ { input: [2.0, 10.0, -1.0], expected: -6.0 },
+ { input: [2.0, 10.0, 0.0], expected: 2.0 },
+ { input: [2.0, 10.0, 0.5], expected: 6.0 },
+ { input: [2.0, 10.0, 1.0], expected: 10.0 },
+ { input: [2.0, 10.0, 2.0], expected: 18.0 },
+
+ // [-1.0, 1.0] cases
+ { input: [-1.0, 1.0, -2.0], expected: -5.0 },
+ { input: [-1.0, 1.0, 0.0], expected: -1.0 },
+ { input: [-1.0, 1.0, 0.5], expected: 0.0 },
+ { input: [-1.0, 1.0, 1.0], expected: 1.0 },
+ { input: [-1.0, 1.0, 2.0], expected: 3.0 },
+
+ // Infinities
+ { input: [0.0, constants.positive.infinity, 0.5], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, 0.0, 0.5], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, 1.0, 0.5], expected: kUnboundedBounds },
+ { input: [1.0, constants.negative.infinity, 0.5], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, constants.positive.infinity, 0.5], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, constants.negative.infinity, 0.5], expected: kUnboundedBounds },
+ { input: [0.0, 1.0, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [1.0, 0.0, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [0.0, 1.0, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [1.0, 0.0, constants.positive.infinity], expected: kUnboundedBounds },
+
+ // Showing how precise and imprecise versions diff
+ { input: [constants.negative.min, 10.0, 1.0], expected: 10.0 },
+ { input: [constants.negative.min, -10.0, 1.0], expected: -10.0 },
+ ];
+ })
+ )
+ .fn(t => {
+ const [x, y, z] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.mixPreciseInterval(x, y, z);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.mixPreciseInterval(${x}, ${y}, ${z}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// Some of these are hard coded, since the error intervals are difficult to express in a closed
+// human-readable form due to the inherited nature of the errors.
+// prettier-ignore
+const kSmoothStepIntervalCases = {
+ f32: [
+ // Normals
+ { input: [0, 1, 0], expected: [0, kValue.f32.positive.subnormal.min] },
+ { input: [0, 1, 1], expected: [reinterpretU32AsF32(0x3f7ffffa), reinterpretU32AsF32(0x3f800003)] }, // ~1
+ { input: [0, 2, 1], expected: [reinterpretU32AsF32(0x3efffff8), reinterpretU32AsF32(0x3f000007)] }, // ~0.5
+ { input: [0, 2, 0.5], expected: [reinterpretU32AsF32(0x3e1ffffb), reinterpretU32AsF32(0x3e200007)] }, // ~0.15625...
+ { input: [2, 0, 1], expected: [reinterpretU32AsF32(0x3efffff8), reinterpretU32AsF32(0x3f000007)] }, // ~0.5
+ { input: [2, 0, 1.5], expected: [reinterpretU32AsF32(0x3e1ffffb), reinterpretU32AsF32(0x3e200007)] }, // ~0.15625...
+ { input: [0, 100, 50], expected: [reinterpretU32AsF32(0x3efffff8), reinterpretU32AsF32(0x3f000007)] }, // ~0.5
+ { input: [0, 100, 25], expected: [reinterpretU32AsF32(0x3e1ffffb), reinterpretU32AsF32(0x3e200007)] }, // ~0.15625...
+ { input: [0, -2, -1], expected: [reinterpretU32AsF32(0x3efffff8), reinterpretU32AsF32(0x3f000007)] }, // ~0.5
+ { input: [0, -2, -0.5], expected: [reinterpretU32AsF32(0x3e1ffffb), reinterpretU32AsF32(0x3e200007)] }, // ~0.15625...
+ // Subnormals
+ { input: [kValue.f32.positive.subnormal.max, 2, 1], expected: [reinterpretU32AsF32(0x3efffff8), reinterpretU32AsF32(0x3f000007)] }, // ~0.5
+ { input: [kValue.f32.positive.subnormal.min, 2, 1], expected: [reinterpretU32AsF32(0x3efffff8), reinterpretU32AsF32(0x3f000007)] }, // ~0.5
+ { input: [kValue.f32.negative.subnormal.max, 2, 1], expected: [reinterpretU32AsF32(0x3efffff8), reinterpretU32AsF32(0x3f000007)] }, // ~0.5
+ { input: [kValue.f32.negative.subnormal.min, 2, 1], expected: [reinterpretU32AsF32(0x3efffff8), reinterpretU32AsF32(0x3f000007)] }, // ~0.5
+ { input: [0, 2, kValue.f32.positive.subnormal.max], expected: [0, kValue.f32.positive.subnormal.min] },
+ { input: [0, 2, kValue.f32.positive.subnormal.min], expected: [0, kValue.f32.positive.subnormal.min] },
+ { input: [0, 2, kValue.f32.negative.subnormal.max], expected: [0, kValue.f32.positive.subnormal.min] },
+ { input: [0, 2, kValue.f32.negative.subnormal.min], expected: [0, kValue.f32.positive.subnormal.min] },
+ ] as ScalarTripleToIntervalCase[],
+ f16: [
+ // Normals
+ { input: [0, 1, 0], expected: [0, reinterpretU16AsF16(0x0002)] },
+ { input: [0, 1, 1], expected: [reinterpretU16AsF16(0x3bfa), reinterpretU16AsF16(0x3c03)] }, // ~1
+ { input: [0, 2, 1], expected: [reinterpretU16AsF16(0x37f8), reinterpretU16AsF16(0x3807)] }, // ~0.5
+ { input: [0, 2, 0.5], expected: [reinterpretU16AsF16(0x30fb), reinterpretU16AsF16(0x3107)] }, // ~0.15625...
+ { input: [2, 0, 1], expected: [reinterpretU16AsF16(0x37f8), reinterpretU16AsF16(0x3807)] }, // ~0.5
+ { input: [2, 0, 1.5], expected: [reinterpretU16AsF16(0x30fb), reinterpretU16AsF16(0x3107)] }, // ~0.15625...
+ { input: [0, 100, 50], expected: [reinterpretU16AsF16(0x37f8), reinterpretU16AsF16(0x3807)] }, // ~0.5
+ { input: [0, 100, 25], expected: [reinterpretU16AsF16(0x30fb), reinterpretU16AsF16(0x3107)] }, // ~0.15625...
+ { input: [0, -2, -1], expected: [reinterpretU16AsF16(0x37f8), reinterpretU16AsF16(0x3807)] }, // ~0.5
+ { input: [0, -2, -0.5], expected: [reinterpretU16AsF16(0x30fb), reinterpretU16AsF16(0x3107)] }, // ~0.15625...
+ // Subnormals
+ { input: [kValue.f16.positive.subnormal.max, 2, 1], expected: [reinterpretU16AsF16(0x37f4), reinterpretU16AsF16(0x380b)] }, // ~0.5
+ { input: [kValue.f16.positive.subnormal.min, 2, 1], expected: [reinterpretU16AsF16(0x37f4), reinterpretU16AsF16(0x380b)] }, // ~0.5
+ { input: [kValue.f16.negative.subnormal.max, 2, 1], expected: [reinterpretU16AsF16(0x37f2), reinterpretU16AsF16(0x380c)] }, // ~0.5
+ { input: [kValue.f16.negative.subnormal.min, 2, 1], expected: [reinterpretU16AsF16(0x37f2), reinterpretU16AsF16(0x380c)] }, // ~0.5
+ { input: [0, 2, kValue.f16.positive.subnormal.max], expected: [0, reinterpretU16AsF16(0x0002)] },
+ { input: [0, 2, kValue.f16.positive.subnormal.min], expected: [0, reinterpretU16AsF16(0x0002)] },
+ { input: [0, 2, kValue.f32.negative.subnormal.max], expected: [0, reinterpretU16AsF16(0x0002)] },
+ { input: [0, 2, kValue.f32.negative.subnormal.min], expected: [0, reinterpretU16AsF16(0x0002)] },
+ ] as ScalarTripleToIntervalCase[],
+} as const;
+
+g.test('smoothStepInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<ScalarTripleToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ ...kSmoothStepIntervalCases[p.trait],
+
+ // Normals
+ { input: [0, 1, 10], expected: 1 },
+ { input: [0, 1, -10], expected: 0 },
+
+ // Subnormals
+ { input: [0, constants.positive.subnormal.max, 1], expected: kUnboundedBounds },
+ { input: [0, constants.positive.subnormal.min, 1], expected: kUnboundedBounds },
+ { input: [0, constants.negative.subnormal.max, 1], expected: kUnboundedBounds },
+ { input: [0, constants.negative.subnormal.min, 1], expected: kUnboundedBounds },
+
+ // Infinities
+ { input: [0, 2, constants.positive.infinity], expected: kUnboundedBounds },
+ { input: [0, 2, constants.negative.infinity], expected: kUnboundedBounds },
+ { input: [constants.positive.infinity, 2, 1], expected: kUnboundedBounds },
+ { input: [constants.negative.infinity, 2, 1], expected: kUnboundedBounds },
+ { input: [0, constants.positive.infinity, 1], expected: kUnboundedBounds },
+ { input: [0, constants.negative.infinity, 1], expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const [low, high, x] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.smoothStepInterval(low, high, x);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.smoothStepInterval(${low}, ${high}, ${x}) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface ScalarToVectorCase {
+ input: number;
+ expected: (number | IntervalBounds)[];
+}
+
+g.test('unpack2x16floatInterval')
+ .paramsSubcasesOnly<ScalarToVectorCase>(
+ // prettier-ignore
+ [
+ // f16 normals
+ { input: 0x00000000, expected: [0, 0] },
+ { input: 0x80000000, expected: [0, 0] },
+ { input: 0x00008000, expected: [0, 0] },
+ { input: 0x80008000, expected: [0, 0] },
+ { input: 0x00003c00, expected: [1, 0] },
+ { input: 0x3c000000, expected: [0, 1] },
+ { input: 0x3c003c00, expected: [1, 1] },
+ { input: 0xbc00bc00, expected: [-1, -1] },
+ { input: 0x49004900, expected: [10, 10] },
+ { input: 0xc900c900, expected: [-10, -10] },
+
+ // f16 subnormals
+ { input: 0x000003ff, expected: [[0, kValue.f16.positive.subnormal.max], 0] },
+ { input: 0x000083ff, expected: [[kValue.f16.negative.subnormal.min, 0], 0] },
+
+ // f16 out of bounds
+ { input: 0x7c000000, expected: [kUnboundedBounds, kUnboundedBounds] },
+ { input: 0xffff0000, expected: [kUnboundedBounds, kUnboundedBounds] },
+ ]
+ )
+ .fn(t => {
+ const expected = FP.f32.toVector(t.params.expected);
+ const got = FP.f32.unpack2x16floatInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `unpack2x16floatInterval(${t.params.input}) returned [${got}]. Expected [${expected}]`
+ );
+ });
+
+// Scope for unpack2x16snormInterval tests so that they can have constants for
+// magic numbers that don't pollute the global namespace or have unwieldy long
+// names.
+{
+ const kZeroBounds: IntervalBounds = [
+ reinterpretU32AsF32(0x81400000),
+ reinterpretU32AsF32(0x01400000),
+ ];
+ const kOneBoundsSnorm: IntervalBounds = [
+ reinterpretU64AsF64(0x3fef_ffff_a000_0000n),
+ reinterpretU64AsF64(0x3ff0_0000_3000_0000n),
+ ];
+ const kNegOneBoundsSnorm: IntervalBounds = [
+ reinterpretU64AsF64(0xbff0_0000_3000_0000n),
+ reinterpretU64AsF64(0xbfef_ffff_a000_0000n),
+ ];
+
+ const kHalfBounds2x16snorm: IntervalBounds = [
+ reinterpretU64AsF64(0x3fe0_001f_a000_0000n),
+ reinterpretU64AsF64(0x3fe0_0020_8000_0000n),
+ ]; // ~0.5..., due to lack of precision in i16
+ const kNegHalfBounds2x16snorm: IntervalBounds = [
+ reinterpretU64AsF64(0xbfdf_ffc0_6000_0000n),
+ reinterpretU64AsF64(0xbfdf_ffbf_8000_0000n),
+ ]; // ~-0.5..., due to lack of precision in i16
+
+ g.test('unpack2x16snormInterval')
+ .paramsSubcasesOnly<ScalarToVectorCase>(
+ // prettier-ignore
+ [
+ { input: 0x00000000, expected: [kZeroBounds, kZeroBounds] },
+ { input: 0x00007fff, expected: [kOneBoundsSnorm, kZeroBounds] },
+ { input: 0x7fff0000, expected: [kZeroBounds, kOneBoundsSnorm] },
+ { input: 0x7fff7fff, expected: [kOneBoundsSnorm, kOneBoundsSnorm] },
+ { input: 0x80018001, expected: [kNegOneBoundsSnorm, kNegOneBoundsSnorm] },
+ { input: 0x40004000, expected: [kHalfBounds2x16snorm, kHalfBounds2x16snorm] },
+ { input: 0xc001c001, expected: [kNegHalfBounds2x16snorm, kNegHalfBounds2x16snorm] },
+ ]
+ )
+ .fn(t => {
+ const expected = FP.f32.toVector(t.params.expected);
+ const got = FP.f32.unpack2x16snormInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `unpack2x16snormInterval(${t.params.input}) returned [${got}]. Expected [${expected}]`
+ );
+ });
+}
+
+// Scope for unpack2x16unormInterval tests so that they can have constants for
+// magic numbers that don't pollute the global namespace or have unwieldy long
+// names.
+{
+ const kZeroBounds: IntervalBounds = [
+ reinterpretU32AsF32(0x8140_0000),
+ reinterpretU32AsF32(0x0140_0000),
+ ]; // ~0
+ const kOneBounds: IntervalBounds = [
+ reinterpretU64AsF64(0x3fef_ffff_a000_0000n),
+ reinterpretU64AsF64(0x3ff0_0000_3000_0000n),
+ ]; // ~1
+ const kHalfBounds: IntervalBounds = [
+ reinterpretU64AsF64(0x3fe0_000f_a000_0000n),
+ reinterpretU64AsF64(0x3fe0_0010_8000_0000n),
+ ]; // ~0.5..., due to the lack of accuracy in u16
+
+ g.test('unpack2x16unormInterval')
+ .paramsSubcasesOnly<ScalarToVectorCase>(
+ // prettier-ignore
+ [
+ { input: 0x00000000, expected: [kZeroBounds, kZeroBounds] },
+ { input: 0x0000ffff, expected: [kOneBounds, kZeroBounds] },
+ { input: 0xffff0000, expected: [kZeroBounds, kOneBounds] },
+ { input: 0xffffffff, expected: [kOneBounds, kOneBounds] },
+ { input: 0x80008000, expected: [kHalfBounds, kHalfBounds] },
+ ]
+ )
+ .fn(t => {
+ const expected = FP.f32.toVector(t.params.expected);
+ const got = FP.f32.unpack2x16unormInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `unpack2x16unormInterval(${t.params.input})\n\tReturned [${got}]\n\tExpected [${expected}]`
+ );
+ });
+}
+
+// Scope for unpack4x8snormInterval tests so that they can have constants for
+// magic numbers that don't pollute the global namespace or have unwieldy long
+// names.
+{
+ const kZeroBounds: IntervalBounds = [
+ reinterpretU32AsF32(0x8140_0000),
+ reinterpretU32AsF32(0x0140_0000),
+ ]; // ~0
+ const kOneBounds: IntervalBounds = [
+ reinterpretU64AsF64(0x3fef_ffff_a000_0000n),
+ reinterpretU64AsF64(0x3ff0_0000_3000_0000n),
+ ]; // ~1
+ const kNegOneBounds: IntervalBounds = [
+ reinterpretU64AsF64(0xbff0_0000_3000_0000n),
+ reinterpretU64AsF64(0xbfef_ffff_a0000_000n),
+ ]; // ~-1
+ const kHalfBounds: IntervalBounds = [
+ reinterpretU64AsF64(0x3fe0_2040_2000_0000n),
+ reinterpretU64AsF64(0x3fe0_2041_0000_0000n),
+ ]; // ~0.50196..., due to lack of precision in i8
+ const kNegHalfBounds: IntervalBounds = [
+ reinterpretU64AsF64(0xbfdf_bf7f_6000_0000n),
+ reinterpretU64AsF64(0xbfdf_bf7e_8000_0000n),
+ ]; // ~-0.49606..., due to lack of precision in i8
+
+ g.test('unpack4x8snormInterval')
+ .paramsSubcasesOnly<ScalarToVectorCase>(
+ // prettier-ignore
+ [
+ { input: 0x00000000, expected: [kZeroBounds, kZeroBounds, kZeroBounds, kZeroBounds] },
+ { input: 0x0000007f, expected: [kOneBounds, kZeroBounds, kZeroBounds, kZeroBounds] },
+ { input: 0x00007f00, expected: [kZeroBounds, kOneBounds, kZeroBounds, kZeroBounds] },
+ { input: 0x007f0000, expected: [kZeroBounds, kZeroBounds, kOneBounds, kZeroBounds] },
+ { input: 0x7f000000, expected: [kZeroBounds, kZeroBounds, kZeroBounds, kOneBounds] },
+ { input: 0x00007f7f, expected: [kOneBounds, kOneBounds, kZeroBounds, kZeroBounds] },
+ { input: 0x7f7f0000, expected: [kZeroBounds, kZeroBounds, kOneBounds, kOneBounds] },
+ { input: 0x7f007f00, expected: [kZeroBounds, kOneBounds, kZeroBounds, kOneBounds] },
+ { input: 0x007f007f, expected: [kOneBounds, kZeroBounds, kOneBounds, kZeroBounds] },
+ { input: 0x7f7f7f7f, expected: [kOneBounds, kOneBounds, kOneBounds, kOneBounds] },
+ {
+ input: 0x81818181,
+ expected: [kNegOneBounds, kNegOneBounds, kNegOneBounds, kNegOneBounds]
+ },
+ {
+ input: 0x40404040,
+ expected: [kHalfBounds, kHalfBounds, kHalfBounds, kHalfBounds]
+ },
+ {
+ input: 0xc1c1c1c1,
+ expected: [kNegHalfBounds, kNegHalfBounds, kNegHalfBounds, kNegHalfBounds]
+ },
+ ]
+ )
+ .fn(t => {
+ const expected = FP.f32.toVector(t.params.expected);
+ const got = FP.f32.unpack4x8snormInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `unpack4x8snormInterval(${t.params.input})\n\tReturned [${got}]\n\tExpected [${expected}]`
+ );
+ });
+}
+
+// Scope for unpack4x8unormInterval tests so that they can have constants for
+// magic numbers that don't pollute the global namespace or have unwieldy long
+// names.
+{
+ const kZeroBounds: IntervalBounds = [
+ reinterpretU32AsF32(0x8140_0000),
+ reinterpretU32AsF32(0x0140_0000),
+ ]; // ~0
+ const kOneBounds: IntervalBounds = [
+ reinterpretU64AsF64(0x3fef_ffff_a000_0000n),
+ reinterpretU64AsF64(0x3ff0_0000_3000_0000n),
+ ]; // ~1
+ const kHalfBounds: IntervalBounds = [
+ reinterpretU64AsF64(0x3fe0_100f_a000_0000n),
+ reinterpretU64AsF64(0x3fe0_1010_8000_0000n),
+ ]; // ~0.50196..., due to lack of precision in u8
+
+ g.test('unpack4x8unormInterval')
+ .paramsSubcasesOnly<ScalarToVectorCase>(
+ // prettier-ignore
+ [
+ { input: 0x00000000, expected: [kZeroBounds, kZeroBounds, kZeroBounds, kZeroBounds] },
+ { input: 0x000000ff, expected: [kOneBounds, kZeroBounds, kZeroBounds, kZeroBounds] },
+ { input: 0x0000ff00, expected: [kZeroBounds, kOneBounds, kZeroBounds, kZeroBounds] },
+ { input: 0x00ff0000, expected: [kZeroBounds, kZeroBounds, kOneBounds, kZeroBounds] },
+ { input: 0xff000000, expected: [kZeroBounds, kZeroBounds, kZeroBounds, kOneBounds] },
+ { input: 0x0000ffff, expected: [kOneBounds, kOneBounds, kZeroBounds, kZeroBounds] },
+ { input: 0xffff0000, expected: [kZeroBounds, kZeroBounds, kOneBounds, kOneBounds] },
+ { input: 0xff00ff00, expected: [kZeroBounds, kOneBounds, kZeroBounds, kOneBounds] },
+ { input: 0x00ff00ff, expected: [kOneBounds, kZeroBounds, kOneBounds, kZeroBounds] },
+ { input: 0xffffffff, expected: [kOneBounds, kOneBounds, kOneBounds, kOneBounds] },
+ {
+ input: 0x80808080,
+ expected: [kHalfBounds, kHalfBounds, kHalfBounds, kHalfBounds]
+ },
+ ]
+ )
+ .fn(t => {
+ const expected = FP.f32.toVector(t.params.expected);
+ const got = FP.f32.unpack4x8unormInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `unpack4x8unormInterval(${t.params.input})\n\tReturned [${got}]\n\tExpected [${expected}]`
+ );
+ });
+}
+
+interface VectorToIntervalCase {
+ input: number[];
+ expected: number | IntervalBounds;
+}
+
+g.test('lengthIntervalVector')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<VectorToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // vec2
+ {input: [1.0, 0.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ {input: [0.0, 1.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ {input: [1.0, 1.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0]'] }, // ~√2
+ {input: [-1.0, -1.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0]'] }, // ~√2
+ {input: [-1.0, 1.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0]'] }, // ~√2
+ {input: [0.1, 0.0], expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1
+
+ // vec3
+ {input: [1.0, 0.0, 0.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ {input: [0.0, 1.0, 0.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ {input: [0.0, 0.0, 1.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ {input: [1.0, 1.0, 1.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0, 1.0]'] }, // ~√3
+ {input: [-1.0, -1.0, -1.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0, 1.0]'] }, // ~√3
+ {input: [1.0, -1.0, -1.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0, 1.0]'] }, // ~√3
+ {input: [0.1, 0.0, 0.0], expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1
+
+ // vec4
+ {input: [1.0, 0.0, 0.0, 0.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ {input: [0.0, 1.0, 0.0, 0.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ {input: [0.0, 0.0, 1.0, 0.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ {input: [0.0, 0.0, 0.0, 1.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ {input: [1.0, 1.0, 1.0, 1.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0, 1.0, 1.0]'] }, // ~2
+ {input: [-1.0, -1.0, -1.0, -1.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0, 1.0, 1.0]'] }, // ~2
+ {input: [-1.0, 1.0, -1.0, 1.0], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0, 1.0, 1.0]'] }, // ~2
+ {input: [0.1, 0.0, 0.0, 0.0], expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1
+
+ // Test that dot going OOB bounds in the intermediate calculations propagates
+ { input: [constants.positive.nearest_max, constants.positive.max, constants.negative.min], expected: kUnboundedBounds },
+ { input: [constants.positive.max, constants.positive.nearest_max, constants.negative.min], expected: kUnboundedBounds },
+ { input: [constants.negative.min, constants.positive.max, constants.positive.nearest_max], expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.lengthInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.lengthInterval([${t.params.input}]) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface VectorPairToIntervalCase {
+ input: [number[], number[]];
+ expected: number | IntervalBounds;
+}
+
+g.test('distanceIntervalVector')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<VectorPairToIntervalCase>(p => {
+ // prettier-ignore
+ return [
+ // distance(x, y), where x - y = 0 has an acceptance interval of kUnboundedBounds,
+ // because distance(x, y) = length(x - y), and length(0) = kUnboundedBounds.
+
+ // vec2
+ { input: [[1.0, 0.0], [1.0, 0.0]], expected: kUnboundedBounds },
+ { input: [[1.0, 0.0], [0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[0.0, 0.0], [1.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[-1.0, 0.0], [0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[0.0, 0.0], [-1.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[0.0, 1.0], [-1.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0]'] }, // ~√2
+ { input: [[0.1, 0.0], [0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1
+
+ // vec3
+ { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: kUnboundedBounds },
+ { input: [[1.0, 0.0, 0.0], [0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[0.0, 1.0, 0.0], [0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[0.0, 0.0, 1.0], [0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[0.0, 0.0, 0.0], [0.0, 1.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[1.0, 1.0, 1.0], [0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0, 1.0]'] }, // ~√3
+ { input: [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0, 1.0]'] }, // ~√3
+ { input: [[-1.0, -1.0, -1.0], [0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0, 1.0]'] }, // ~√3
+ { input: [[0.0, 0.0, 0.0], [-1.0, -1.0, -1.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0, 1.0]'] }, // ~√3
+ { input: [[0.1, 0.0, 0.0], [0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1
+ { input: [[0.0, 0.0, 0.0], [0.1, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1
+
+ // vec4
+ { input: [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: kUnboundedBounds },
+ { input: [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0]'] }, // ~1
+ { input: [[1.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0, 1.0, 1.0]'] }, // ~2
+ { input: [[0.0, 0.0, 0.0, 0.0], [1.0, 1.0, 1.0, 1.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0, 1.0, 1.0]'] }, // ~2
+ { input: [[-1.0, 1.0, -1.0, 1.0], [0.0, 0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0, 1.0, 1.0]'] }, // ~2
+ { input: [[0.0, 0.0, 0.0, 0.0], [1.0, -1.0, 1.0, -1.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[1.0, 1.0, 1.0, 1.0]'] }, // ~2
+ { input: [[0.1, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1
+ { input: [[0.0, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0]], expected: kRootSumSquareExpectionInterval[p.trait]['[0.1]'] }, // ~0.1
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.distanceInterval(...t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.lengthInterval([${t.params.input[0]}, ${t.params.input[1]}]) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// prettier-ignore
+const kDotIntervalCases = {
+ f32: [
+ // Inputs with large values but cancel out to finite result. In these cases, 2.0*2.0 = 4.0 and
+ // 3.0*3.0 = 9.0 is much smaller than kValue.f32.positive.max, as a result
+ // kValue.f32.positive.max + 9.0 = kValue.f32.positive.max in f32 and even f64. So, if the
+ // positive and negative large number cancel each other first, the result would be
+ // 2.0*2.0+3.0*3.0 = 13. Otherwise, the resule would be 0.0 or 4.0 or 9.0.
+ // https://github.com/gpuweb/cts/issues/2155
+ { input: [[kValue.f32.positive.max, 1.0, 2.0, 3.0], [-1.0, kValue.f32.positive.max, -2.0, -3.0]], expected: [-13, 0] },
+ { input: [[kValue.f32.positive.max, 1.0, 2.0, 3.0], [1.0, kValue.f32.negative.min, 2.0, 3.0]], expected: [0, 13] },
+ ] as VectorPairToIntervalCase[],
+ f16: [
+ // Inputs with large values but cancel out to finite result. In these cases, 2.0*2.0 = 4.0 and
+ // 3.0*3.0 = 9.0 is not small enough comparing to kValue.f16.positive.max = 65504, as a result
+ // kValue.f16.positive.max + 9.0 = 65513 is exactly representable in f32 and f64. So, if the
+ // positive and negative large number don't cancel each other first, the computation will
+ // overflow f16 and result in unbounded bounds.
+ // https://github.com/gpuweb/cts/issues/2155
+ { input: [[kValue.f16.positive.max, 1.0, 2.0, 3.0], [-1.0, kValue.f16.positive.max, -2.0, -3.0]], expected: kUnboundedBounds },
+ { input: [[kValue.f16.positive.max, 1.0, 2.0, 3.0], [1.0, kValue.f16.negative.min, 2.0, 3.0]], expected: kUnboundedBounds },
+ ] as VectorPairToIntervalCase[],
+} as const;
+
+g.test('dotInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<VectorPairToIntervalCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // vec2
+ { input: [[1.0, 0.0], [1.0, 0.0]], expected: 1.0 },
+ { input: [[0.0, 1.0], [0.0, 1.0]], expected: 1.0 },
+ { input: [[1.0, 1.0], [1.0, 1.0]], expected: 2.0 },
+ { input: [[-1.0, -1.0], [-1.0, -1.0]], expected: 2.0 },
+ { input: [[-1.0, 1.0], [1.0, -1.0]], expected: -2.0 },
+ { input: [[0.1, 0.0], [1.0, 0.0]], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1']}, // correclt rounded of 0.1
+
+ // vec3
+ { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: 1.0 },
+ { input: [[0.0, 1.0, 0.0], [0.0, 1.0, 0.0]], expected: 1.0 },
+ { input: [[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]], expected: 1.0 },
+ { input: [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]], expected: 3.0 },
+ { input: [[-1.0, -1.0, -1.0], [-1.0, -1.0, -1.0]], expected: 3.0 },
+ { input: [[1.0, -1.0, -1.0], [-1.0, 1.0, -1.0]], expected: -1.0 },
+ { input: [[0.1, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1']}, // correclt rounded of 0.1
+
+ // vec4
+ { input: [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: 1.0 },
+ { input: [[0.0, 1.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]], expected: 1.0 },
+ { input: [[0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 1.0, 0.0]], expected: 1.0 },
+ { input: [[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 1.0]], expected: 1.0 },
+ { input: [[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]], expected: 4.0 },
+ { input: [[-1.0, -1.0, -1.0, -1.0], [-1.0, -1.0, -1.0, -1.0]], expected: 4.0 },
+ { input: [[-1.0, 1.0, -1.0, 1.0], [1.0, -1.0, 1.0, -1.0]], expected: -4.0 },
+ { input: [[0.1, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: kConstantCorrectlyRoundedExpectation[p.trait]['0.1']}, // correclt rounded of 0.1
+
+ ...kDotIntervalCases[p.trait],
+
+ // Test that going out of bounds in the intermediate calculations is caught correctly.
+ { input: [[constants.positive.nearest_max, constants.positive.max, constants.negative.min], [1.0, 1.0, 1.0]], expected: kUnboundedBounds },
+ { input: [[constants.positive.nearest_max, constants.negative.min, constants.positive.max], [1.0, 1.0, 1.0]], expected: kUnboundedBounds },
+ { input: [[constants.positive.max, constants.positive.nearest_max, constants.negative.min], [1.0, 1.0, 1.0]], expected: kUnboundedBounds },
+ { input: [[constants.negative.min, constants.positive.nearest_max, constants.positive.max], [1.0, 1.0, 1.0]], expected: kUnboundedBounds },
+ { input: [[constants.positive.max, constants.negative.min, constants.positive.nearest_max], [1.0, 1.0, 1.0]], expected: kUnboundedBounds },
+ { input: [[constants.negative.min, constants.positive.max, constants.positive.nearest_max], [1.0, 1.0, 1.0]], expected: kUnboundedBounds },
+ ];
+ })
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.dotInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.dotInterval([${x}], [${y}]) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface VectorToVectorCase {
+ input: number[];
+ expected: (number | IntervalBounds)[];
+}
+
+// prettier-ignore
+const kNormalizeIntervalCases = {
+ f32: [
+ // vec2
+ {input: [1.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0]
+ {input: [0.0, 1.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)]] }, // [ ~0.0, ~1.0]
+ {input: [-1.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_0000_b000_0000n), reinterpretU64AsF64(0xbfef_fffe_7000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0]
+ {input: [1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fe6_a09d_5000_0000n), reinterpretU64AsF64(0x3fe6_a09f_9000_0000n)], [reinterpretU64AsF64(0x3fe6_a09d_5000_0000n), reinterpretU64AsF64(0x3fe6_a09f_9000_0000n)]] }, // [ ~1/√2, ~1/√2]
+
+ // vec3
+ {input: [1.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0]
+ {input: [0.0, 1.0, 0.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~0.0, ~1.0, ~0.0]
+ {input: [0.0, 0.0, 1.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)]] }, // [ ~0.0, ~0.0, ~1.0]
+ {input: [-1.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_0000_b000_0000n), reinterpretU64AsF64(0xbfef_fffe_7000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0]
+ {input: [1.0, 1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fe2_79a6_5000_0000n), reinterpretU64AsF64(0x3fe2_79a8_5000_0000n)], [reinterpretU64AsF64(0x3fe2_79a6_5000_0000n), reinterpretU64AsF64(0x3fe2_79a8_5000_0000n)], [reinterpretU64AsF64(0x3fe2_79a6_5000_0000n), reinterpretU64AsF64(0x3fe2_79a8_5000_0000n)]] }, // [ ~1/√3, ~1/√3, ~1/√3]
+
+ // vec4
+ {input: [1.0, 0.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0, ~0.0]
+ {input: [0.0, 1.0, 0.0, 0.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~0.0, ~1.0, ~0.0, ~0.0]
+ {input: [0.0, 0.0, 1.0, 0.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~0.0, ~0.0, ~1.0, ~0.0]
+ {input: [0.0, 0.0, 0.0, 1.0], expected: [[reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU64AsF64(0x3fef_fffe_7000_0000n), reinterpretU64AsF64(0x3ff0_0000_b000_0000n)]] }, // [ ~0.0, ~0.0, ~0.0, ~1.0]
+ {input: [-1.0, 0.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_0000_b000_0000n), reinterpretU64AsF64(0xbfef_fffe_7000_0000n)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)], [reinterpretU32AsF32(0x81200000), reinterpretU32AsF32(0x01200000)]] }, // [ ~1.0, ~0.0, ~0.0, ~0.0]
+ {input: [1.0, 1.0, 1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fdf_fffe_7000_0000n), reinterpretU64AsF64(0x3fe0_0000_b000_0000n)], [reinterpretU64AsF64(0x3fdf_fffe_7000_0000n), reinterpretU64AsF64(0x3fe0_0000_b000_0000n)], [reinterpretU64AsF64(0x3fdf_fffe_7000_0000n), reinterpretU64AsF64(0x3fe0_0000_b000_0000n)], [reinterpretU64AsF64(0x3fdf_fffe_7000_0000n), reinterpretU64AsF64(0x3fe0_0000_b000_0000n)]] }, // [ ~1/√4, ~1/√4, ~1/√4]
+ ] as VectorToVectorCase[],
+ f16: [
+ // vec2
+ {input: [1.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0]
+ {input: [0.0, 1.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)]] }, // [ ~0.0, ~1.0]
+ {input: [-1.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_1600_0000_0000n), reinterpretU64AsF64(0xbfef_ce00_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0]
+ {input: [1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fe6_7e00_0000_0000n), reinterpretU64AsF64(0x3fe6_c600_0000_0000n)], [reinterpretU64AsF64(0x3fe6_7e00_0000_0000n), reinterpretU64AsF64(0x3fe6_c600_0000_0000n)]] }, // [ ~1/√2, ~1/√2]
+
+ // vec3
+ {input: [1.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0, ~0.0]
+ {input: [0.0, 1.0, 0.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~0.0, ~1.0, ~0.0]
+ {input: [0.0, 0.0, 1.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)]] }, // [ ~0.0, ~0.0, ~1.0]
+ {input: [-1.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_1600_0000_0000n), reinterpretU64AsF64(0xbfef_ce00_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0, ~0.0]
+ {input: [1.0, 1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fe2_5a00_0000_0000n), reinterpretU64AsF64(0x3fe2_9a00_0000_0000n)], [reinterpretU64AsF64(0x3fe2_5a00_0000_0000n), reinterpretU64AsF64(0x3fe2_9a00_0000_0000n)], [reinterpretU64AsF64(0x3fe2_5a00_0000_0000n), reinterpretU64AsF64(0x3fe2_9a00_0000_0000n)]] }, // [ ~1/√3, ~1/√3, ~1/√3]
+
+ // vec4
+ {input: [1.0, 0.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0, ~0.0, ~0.0]
+ {input: [0.0, 1.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~0.0, ~1.0, ~0.0, ~0.0]
+ {input: [0.0, 0.0, 1.0, 0.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~0.0, ~0.0, ~1.0, ~0.0]
+ {input: [0.0, 0.0, 0.0, 1.0], expected: [[reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0x3fef_ce00_0000_0000n), reinterpretU64AsF64(0x3ff0_1600_0000_0000n)]] }, // [ ~0.0, ~0.0, ~0.0, ~1.0]
+ {input: [-1.0, 0.0, 0.0, 0.0], expected: [[reinterpretU64AsF64(0xbff0_1600_0000_0000n), reinterpretU64AsF64(0xbfef_ce00_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)], [reinterpretU64AsF64(0xbf24_0000_0000_0000n), reinterpretU64AsF64(0x3f24_0000_0000_0000n)]] }, // [ ~1.0, ~0.0, ~0.0, ~0.0]
+ {input: [1.0, 1.0, 1.0, 1.0], expected: [[reinterpretU64AsF64(0x3fdf_ce00_0000_0000n), reinterpretU64AsF64(0x3fe0_1600_0000_0000n)], [reinterpretU64AsF64(0x3fdf_ce00_0000_0000n), reinterpretU64AsF64(0x3fe0_1600_0000_0000n)], [reinterpretU64AsF64(0x3fdf_ce00_0000_0000n), reinterpretU64AsF64(0x3fe0_1600_0000_0000n)], [reinterpretU64AsF64(0x3fdf_ce00_0000_0000n), reinterpretU64AsF64(0x3fe0_1600_0000_0000n)]] }, // [ ~1/√4, ~1/√4, ~1/√4]
+ ] as VectorToVectorCase[],
+} as const;
+
+g.test('normalizeInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<VectorToVectorCase>(p => kNormalizeIntervalCases[p.trait])
+ )
+ .fn(t => {
+ const x = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toVector(t.params.expected);
+ const got = trait.normalizeInterval(x);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.normalizeInterval([${x}]) returned ${got}. Expected ${expected}`
+ );
+ });
+
+interface VectorPairToVectorCase {
+ input: [number[], number[]];
+ expected: (number | IntervalBounds)[];
+}
+
+// prettier-ignore
+const kCrossIntervalCases = {
+ f32: [
+ { input: [
+ [kValue.f32.positive.subnormal.max, kValue.f32.negative.subnormal.max, kValue.f32.negative.subnormal.min],
+ [kValue.f32.negative.subnormal.min, kValue.f32.positive.subnormal.min, kValue.f32.negative.subnormal.max]
+ ],
+ expected: [
+ [0.0, reinterpretU32AsF32(0x00000002)], // ~0
+ [0.0, reinterpretU32AsF32(0x00000002)], // ~0
+ [kValue.f32.negative.subnormal.max, kValue.f32.positive.subnormal.min] // ~0
+ ]
+ },
+ { input: [
+ [0.1, -0.1, -0.1],
+ [-0.1, 0.1, -0.1]
+ ],
+ expected: [
+ [reinterpretU32AsF32(0x3ca3d708), reinterpretU32AsF32(0x3ca3d70b)], // ~0.02
+ [reinterpretU32AsF32(0x3ca3d708), reinterpretU32AsF32(0x3ca3d70b)], // ~0.02
+ [reinterpretU32AsF32(0xb1400000), reinterpretU32AsF32(0x31400000)], // ~0
+ ]
+ },
+ ] as VectorPairToVectorCase[],
+ f16: [
+ { input: [
+ [kValue.f16.positive.subnormal.max, kValue.f16.negative.subnormal.max, kValue.f16.negative.subnormal.min],
+ [kValue.f16.negative.subnormal.min, kValue.f16.positive.subnormal.min, kValue.f16.negative.subnormal.max]
+ ],
+ expected: [
+ [0.0, reinterpretU16AsF16(0x0002)], // ~0
+ [0.0, reinterpretU16AsF16(0x0002)], // ~0
+ [kValue.f16.negative.subnormal.max, kValue.f16.positive.subnormal.min] // ~0
+ ]
+ },
+ { input: [
+ [0.1, -0.1, -0.1],
+ [-0.1, 0.1, -0.1]
+ ],
+ expected: [
+ [reinterpretU16AsF16(0x251e), reinterpretU16AsF16(0x2520)], // ~0.02
+ [reinterpretU16AsF16(0x251e), reinterpretU16AsF16(0x2520)], // ~0.02
+ [reinterpretU16AsF16(0x8100), reinterpretU16AsF16(0x0100)] // ~0
+ ]
+ },
+ ] as VectorPairToVectorCase[],
+ abstract: [
+ { input: [
+ [kValue.f64.positive.subnormal.max, kValue.f64.negative.subnormal.max, kValue.f64.negative.subnormal.min],
+ [kValue.f64.negative.subnormal.min, kValue.f64.positive.subnormal.min, kValue.f64.negative.subnormal.max]
+ ],
+ expected: [0.0, 0.0, 0.0]
+ },
+ { input: [
+ [0.1, -0.1, -0.1],
+ [-0.1, 0.1, -0.1]
+ ],
+ expected: [
+ reinterpretU64AsF64(0x3f94_7ae1_47ae_147cn), // ~0.02
+ reinterpretU64AsF64(0x3f94_7ae1_47ae_147cn), // ~0.02
+ 0.0
+ ]
+ },
+ ] as VectorPairToVectorCase[],
+} as const;
+
+g.test('crossInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<VectorPairToVectorCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // parallel vectors, AXB == 0
+ { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [0.0, 0.0, 0.0] },
+ { input: [[0.0, 1.0, 0.0], [0.0, 1.0, 0.0]], expected: [0.0, 0.0, 0.0] },
+ { input: [[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]], expected: [0.0, 0.0, 0.0] },
+ { input: [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]], expected: [0.0, 0.0, 0.0] },
+ { input: [[-1.0, -1.0, -1.0], [-1.0, -1.0, -1.0]], expected: [0.0, 0.0, 0.0] },
+ { input: [[0.1, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [0.0, 0.0, 0.0] },
+ { input: [[constants.positive.subnormal.max, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [0.0, 0.0, 0.0] },
+
+ // non-parallel vectors, AXB != 0
+ { input: [[1.0, -1.0, -1.0], [-1.0, 1.0, -1.0]], expected: [2.0, 2.0, 0.0] },
+ { input: [[1.0, 2, 3], [1.0, 5.0, 7.0]], expected: [-1, -4, 3] },
+ ...kCrossIntervalCases[p.trait],
+ ];
+ })
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toVector(t.params.expected);
+ const got = trait.crossInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.crossInterval([${x}], [${y}]) returned ${got}. Expected ${expected}`
+ );
+ });
+
+// prettier-ignore
+const kReflectIntervalCases = {
+ f32: [
+ // vec2s
+ { input: [[0.1, 0.1], [1.0, 1.0]], expected: [[reinterpretU32AsF32(0xbe99999a), reinterpretU32AsF32(0xbe999998)], [reinterpretU32AsF32(0xbe99999a), reinterpretU32AsF32(0xbe999998)]] }, // [~-0.3, ~-0.3]
+ { input: [[kValue.f32.positive.subnormal.max, kValue.f32.negative.subnormal.max], [1.0, 1.0]], expected: [[reinterpretU32AsF32(0x80fffffe), reinterpretU32AsF32(0x00800001)], [reinterpretU32AsF32(0x80ffffff), reinterpretU32AsF32(0x00000002)]] }, // [~0.0, ~0.0]
+ // vec3s
+ { input: [[0.1, 0.1, 0.1], [1.0, 1.0, 1.0]], expected: [[reinterpretU32AsF32(0xbf000001), reinterpretU32AsF32(0xbefffffe)], [reinterpretU32AsF32(0xbf000001), reinterpretU32AsF32(0xbefffffe)], [reinterpretU32AsF32(0xbf000001), reinterpretU32AsF32(0xbefffffe)]] }, // [~-0.5, ~-0.5, ~-0.5]
+ { input: [[kValue.f32.positive.subnormal.max, kValue.f32.negative.subnormal.max, 0.0], [1.0, 1.0, 1.0]], expected: [[reinterpretU32AsF32(0x80fffffe), reinterpretU32AsF32(0x00800001)], [reinterpretU32AsF32(0x80ffffff), reinterpretU32AsF32(0x00000002)], [reinterpretU32AsF32(0x80fffffe), reinterpretU32AsF32(0x00000002)]] }, // [~0.0, ~0.0, ~0.0]
+ // vec4s
+ { input: [[0.1, 0.1, 0.1, 0.1], [1.0, 1.0, 1.0, 1.0]], expected: [[reinterpretU32AsF32(0xbf333335), reinterpretU32AsF32(0xbf333332)], [reinterpretU32AsF32(0xbf333335), reinterpretU32AsF32(0xbf333332)], [reinterpretU32AsF32(0xbf333335), reinterpretU32AsF32(0xbf333332)], [reinterpretU32AsF32(0xbf333335), reinterpretU32AsF32(0xbf333332)]] }, // [~-0.7, ~-0.7, ~-0.7, ~-0.7]
+ { input: [[kValue.f32.positive.subnormal.max, kValue.f32.negative.subnormal.max, 0.0, 0.0], [1.0, 1.0, 1.0, 1.0]], expected: [[reinterpretU32AsF32(0x80fffffe), reinterpretU32AsF32(0x00800001)], [reinterpretU32AsF32(0x80ffffff), reinterpretU32AsF32(0x00000002)], [reinterpretU32AsF32(0x80fffffe), reinterpretU32AsF32(0x00000002)], [reinterpretU32AsF32(0x80fffffe), reinterpretU32AsF32(0x00000002)]] }, // [~0.0, ~0.0, ~0.0, ~0.0]
+ ] as VectorPairToVectorCase[],
+ f16: [
+ // vec2s
+ { input: [[0.1, 0.1], [1.0, 1.0]], expected: [[reinterpretU16AsF16(0xb4ce), reinterpretU16AsF16(0xb4cc)], [reinterpretU16AsF16(0xb4ce), reinterpretU16AsF16(0xb4cc)]] }, // [~-0.3, ~-0.3]
+ { input: [[kValue.f16.positive.subnormal.max, kValue.f16.negative.subnormal.max], [1.0, 1.0]], expected: [[reinterpretU16AsF16(0x87fe), reinterpretU16AsF16(0x0401)], [reinterpretU16AsF16(0x87ff), reinterpretU16AsF16(0x0002)]] }, // [~0.0, ~0.0]
+ // vec3s
+ { input: [[0.1, 0.1, 0.1], [1.0, 1.0, 1.0]], expected: [[reinterpretU16AsF16(0xb802), reinterpretU16AsF16(0xb7fe)], [reinterpretU16AsF16(0xb802), reinterpretU16AsF16(0xb7fe)], [reinterpretU16AsF16(0xb802), reinterpretU16AsF16(0xb7fe)]] }, // [~-0.5, ~-0.5, ~-0.5]
+ { input: [[kValue.f16.positive.subnormal.max, kValue.f16.negative.subnormal.max, 0.0], [1.0, 1.0, 1.0]], expected: [[reinterpretU16AsF16(0x87fe), reinterpretU16AsF16(0x0401)], [reinterpretU16AsF16(0x87ff), reinterpretU16AsF16(0x0002)], [reinterpretU16AsF16(0x87fe), reinterpretU16AsF16(0x0002)]] }, // [~0.0, ~0.0, ~0.0]
+ // vec4s
+ { input: [[0.1, 0.1, 0.1, 0.1], [1.0, 1.0, 1.0, 1.0]], expected: [[reinterpretU16AsF16(0xb99c), reinterpretU16AsF16(0xb998)], [reinterpretU16AsF16(0xb99c), reinterpretU16AsF16(0xb998)], [reinterpretU16AsF16(0xb99c), reinterpretU16AsF16(0xb998)], [reinterpretU16AsF16(0xb99c), reinterpretU16AsF16(0xb998)]] }, // [~-0.7, ~-0.7, ~-0.7, ~-0.7]
+ { input: [[kValue.f16.positive.subnormal.max, kValue.f16.negative.subnormal.max, 0.0, 0.0], [1.0, 1.0, 1.0, 1.0]], expected: [[reinterpretU16AsF16(0x87fe), reinterpretU16AsF16(0x0401)], [reinterpretU16AsF16(0x87ff), reinterpretU16AsF16(0x0002)], [reinterpretU16AsF16(0x87fe), reinterpretU16AsF16(0x0002)], [reinterpretU16AsF16(0x87fe), reinterpretU16AsF16(0x0002)]] }, // [~0.0, ~0.0, ~0.0, ~0.0]
+ ] as VectorPairToVectorCase[],
+} as const;
+
+g.test('reflectInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<VectorPairToVectorCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ ...kReflectIntervalCases[p.trait],
+
+ // vec2s
+ { input: [[1.0, 0.0], [1.0, 0.0]], expected: [-1.0, 0.0] },
+ { input: [[1.0, 0.0], [0.0, 1.0]], expected: [1.0, 0.0] },
+ { input: [[0.0, 1.0], [0.0, 1.0]], expected: [0.0, -1.0] },
+ { input: [[0.0, 1.0], [1.0, 0.0]], expected: [0.0, 1.0] },
+ { input: [[1.0, 1.0], [1.0, 1.0]], expected: [-3.0, -3.0] },
+ { input: [[-1.0, -1.0], [1.0, 1.0]], expected: [3.0, 3.0] },
+ // vec3s
+ { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [-1.0, 0.0, 0.0] },
+ { input: [[0.0, 1.0, 0.0], [1.0, 0.0, 0.0]], expected: [0.0, 1.0, 0.0] },
+ { input: [[0.0, 0.0, 1.0], [1.0, 0.0, 0.0]], expected: [0.0, 0.0, 1.0] },
+ { input: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], expected: [1.0, 0.0, 0.0] },
+ { input: [[1.0, 0.0, 0.0], [0.0, 0.0, 1.0]], expected: [1.0, 0.0, 0.0] },
+ { input: [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]], expected: [-5.0, -5.0, -5.0] },
+ { input: [[-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]], expected: [5.0, 5.0, 5.0] },
+ // vec4s
+ { input: [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [-1.0, 0.0, 0.0, 0.0] },
+ { input: [[0.0, 1.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [0.0, 1.0, 0.0, 0.0] },
+ { input: [[0.0, 0.0, 1.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [0.0, 0.0, 1.0, 0.0] },
+ { input: [[0.0, 0.0, 0.0, 1.0], [1.0, 0.0, 0.0, 0.0]], expected: [0.0, 0.0, 0.0, 1.0] },
+ { input: [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]], expected: [1.0, 0.0, 0.0, 0.0] },
+ { input: [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]], expected: [1.0, 0.0, 0.0, 0.0] },
+ { input: [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]], expected: [1.0, 0.0, 0.0, 0.0] },
+ { input: [[-1.0, -1.0, -1.0, -1.0], [1.0, 1.0, 1.0, 1.0]], expected: [7.0, 7.0, 7.0, 7.0] },
+ // Test that dot going OOB bounds in the intermediate calculations propagates
+ { input: [[constants.positive.nearest_max, constants.positive.max, constants.negative.min], [1.0, 1.0, 1.0]], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] },
+ { input: [[constants.positive.nearest_max, constants.negative.min, constants.positive.max], [1.0, 1.0, 1.0]], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] },
+ { input: [[constants.positive.max, constants.positive.nearest_max, constants.negative.min], [1.0, 1.0, 1.0]], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] },
+ { input: [[constants.negative.min, constants.positive.nearest_max, constants.positive.max], [1.0, 1.0, 1.0]], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] },
+ { input: [[constants.positive.max, constants.negative.min, constants.positive.nearest_max], [1.0, 1.0, 1.0]], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] },
+ { input: [[constants.negative.min, constants.positive.max, constants.positive.nearest_max], [1.0, 1.0, 1.0]], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] },
+
+ // Test that post-dot going OOB propagates
+ { input: [[constants.positive.max, 1.0, 2.0, 3.0], [-1.0, constants.positive.max, -2.0, -3.0]], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] },
+ ];
+ })
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toVector(t.params.expected);
+ const got = trait.reflectInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.reflectInterval([${x}], [${y}]) returned ${JSON.stringify(
+ got
+ )}. Expected ${JSON.stringify(expected)}`
+ );
+ });
+
+interface MatrixToScalarCase {
+ input: number[][];
+ expected: number | IntervalBounds;
+}
+
+g.test('determinantInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .combineWithParams<MatrixToScalarCase>([
+ // Extreme values, i.e. subnormals, very large magnitudes, and those lead to
+ // non-precise products, are intentionally not tested, since the accuracy of
+ // determinant is restricted to well behaving inputs. Handling all cases
+ // requires ~23! options to be calculated in the 4x4 case, so is not
+ // feasible.
+ {
+ input: [
+ [1, 2],
+ [3, 4],
+ ],
+ expected: -2,
+ },
+ {
+ input: [
+ [-1, 2],
+ [-3, 4],
+ ],
+ expected: 2,
+ },
+ {
+ input: [
+ [11, 22],
+ [33, 44],
+ ],
+ expected: -242,
+ },
+ {
+ input: [
+ [5, 6],
+ [8, 9],
+ ],
+ expected: -3,
+ },
+ {
+ input: [
+ [4, 6],
+ [7, 9],
+ ],
+ expected: -6,
+ },
+ {
+ input: [
+ [4, 5],
+ [7, 8],
+ ],
+ expected: -3,
+ },
+ {
+ input: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ ],
+ expected: 0,
+ },
+ {
+ input: [
+ [-1, 2, 3],
+ [-4, 5, 6],
+ [-7, 8, 9],
+ ],
+ expected: 0,
+ },
+ {
+ input: [
+ [4, 1, -1],
+ [-3, 0, 5],
+ [5, 3, 2],
+ ],
+ expected: -20,
+ },
+ {
+ input: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ [13, 14, 15, 16],
+ ],
+ expected: 0,
+ },
+ {
+ input: [
+ [4, 0, 0, 0],
+ [3, 1, -1, 3],
+ [2, -3, 3, 1],
+ [2, 3, 3, 1],
+ ],
+ expected: -240,
+ },
+ ])
+ )
+ .fn(t => {
+ const input = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toInterval(t.params.expected);
+ const got = trait.determinantInterval(input);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.determinantInterval([${JSON.stringify(
+ input
+ )}]) returned '${got}. Expected '${expected}'`
+ );
+ });
+
+interface MatrixToMatrixCase {
+ input: number[][];
+ expected: (number | IntervalBounds)[][];
+}
+
+g.test('transposeInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<MatrixToMatrixCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ return [
+ {
+ input: [
+ [1, 2],
+ [3, 4],
+ ],
+ expected: [
+ [1, 3],
+ [2, 4],
+ ],
+ },
+ {
+ input: [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ ],
+ expected: [
+ [1, 3, 5],
+ [2, 4, 6],
+ ],
+ },
+ {
+ input: [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ [7, 8],
+ ],
+ expected: [
+ [1, 3, 5, 7],
+ [2, 4, 6, 8],
+ ],
+ },
+ {
+ input: [
+ [1, 2, 3],
+ [4, 5, 6],
+ ],
+ expected: [
+ [1, 4],
+ [2, 5],
+ [3, 6],
+ ],
+ },
+ {
+ input: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ ],
+ expected: [
+ [1, 4, 7],
+ [2, 5, 8],
+ [3, 6, 9],
+ ],
+ },
+ {
+ input: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ [10, 11, 12],
+ ],
+ expected: [
+ [1, 4, 7, 10],
+ [2, 5, 8, 11],
+ [3, 6, 9, 12],
+ ],
+ },
+ {
+ input: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ ],
+ expected: [
+ [1, 5],
+ [2, 6],
+ [3, 7],
+ [4, 8],
+ ],
+ },
+ {
+ input: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ ],
+ expected: [
+ [1, 5, 9],
+ [2, 6, 10],
+ [3, 7, 11],
+ [4, 8, 12],
+ ],
+ },
+ {
+ input: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ [13, 14, 15, 16],
+ ],
+ expected: [
+ [1, 5, 9, 13],
+ [2, 6, 10, 14],
+ [3, 7, 11, 15],
+ [4, 8, 12, 16],
+ ],
+ },
+ {
+ input: [
+ [constants.positive.subnormal.max, constants.positive.subnormal.min],
+ [constants.negative.subnormal.min, constants.negative.subnormal.max],
+ ],
+ expected: [
+ [
+ [0, constants.positive.subnormal.max],
+ [constants.negative.subnormal.min, 0],
+ ],
+ [
+ [0, constants.positive.subnormal.min],
+ [constants.negative.subnormal.max, 0],
+ ],
+ ],
+ },
+ ];
+ })
+ )
+ .fn(t => {
+ const input = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toMatrix(t.params.expected);
+ const got = trait.transposeInterval(input);
+ t.expect(
+ objectEquals(expected, got),
+ `FP.${t.params.trait}.transposeInterval([${JSON.stringify(
+ input
+ )}]) returned '[${JSON.stringify(got)}]'. Expected '[${JSON.stringify(expected)}]'`
+ );
+ });
+
+interface MatrixPairToMatrixCase {
+ input: [number[][], number[][]];
+ expected: (number | IntervalBounds)[][];
+}
+
+g.test('additionMatrixMatrixInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .combineWithParams<MatrixPairToMatrixCase>([
+ // Only testing that different shapes of matrices are handled correctly
+ // here, to reduce test duplication.
+ // additionMatrixMatrixInterval uses AdditionIntervalOp for calculating intervals,
+ // so the testing for additionInterval covers the actual interval
+ // calculations.
+ {
+ input: [
+ [
+ [1, 2],
+ [3, 4],
+ ],
+ [
+ [10, 20],
+ [30, 40],
+ ],
+ ],
+ expected: [
+ [11, 22],
+ [33, 44],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ ],
+ [
+ [10, 20],
+ [30, 40],
+ [50, 60],
+ ],
+ ],
+ expected: [
+ [11, 22],
+ [33, 44],
+ [55, 66],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ [7, 8],
+ ],
+ [
+ [10, 20],
+ [30, 40],
+ [50, 60],
+ [70, 80],
+ ],
+ ],
+ expected: [
+ [11, 22],
+ [33, 44],
+ [55, 66],
+ [77, 88],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3],
+ [4, 5, 6],
+ ],
+ [
+ [10, 20, 30],
+ [40, 50, 60],
+ ],
+ ],
+ expected: [
+ [11, 22, 33],
+ [44, 55, 66],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ ],
+ [
+ [10, 20, 30],
+ [40, 50, 60],
+ [70, 80, 90],
+ ],
+ ],
+ expected: [
+ [11, 22, 33],
+ [44, 55, 66],
+ [77, 88, 99],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ [10, 11, 12],
+ ],
+ [
+ [10, 20, 30],
+ [40, 50, 60],
+ [70, 80, 90],
+ [1000, 1100, 1200],
+ ],
+ ],
+ expected: [
+ [11, 22, 33],
+ [44, 55, 66],
+ [77, 88, 99],
+ [1010, 1111, 1212],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ ],
+ [
+ [10, 20, 30, 40],
+ [50, 60, 70, 80],
+ ],
+ ],
+ expected: [
+ [11, 22, 33, 44],
+ [55, 66, 77, 88],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ ],
+ [
+ [10, 20, 30, 40],
+ [50, 60, 70, 80],
+ [90, 1000, 1100, 1200],
+ ],
+ ],
+ expected: [
+ [11, 22, 33, 44],
+ [55, 66, 77, 88],
+ [99, 1010, 1111, 1212],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ [13, 14, 15, 16],
+ ],
+ [
+ [10, 20, 30, 40],
+ [50, 60, 70, 80],
+ [90, 1000, 1100, 1200],
+ [1300, 1400, 1500, 1600],
+ ],
+ ],
+ expected: [
+ [11, 22, 33, 44],
+ [55, 66, 77, 88],
+ [99, 1010, 1111, 1212],
+ [1313, 1414, 1515, 1616],
+ ],
+ },
+ ])
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toMatrix(t.params.expected);
+ const got = trait.additionMatrixMatrixInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.additionMatrixMatrixInterval([${JSON.stringify(x)}], [${JSON.stringify(
+ y
+ )}]) returned '[${JSON.stringify(got)}]'. Expected '[${JSON.stringify(expected)}]'`
+ );
+ });
+
+g.test('subtractionMatrixMatrixInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .combineWithParams<MatrixPairToMatrixCase>([
+ // Only testing that different shapes of matrices are handled correctly
+ // here, to reduce test duplication.
+ // subtractionMatrixMatrixInterval uses AdditionIntervalOp for calculating intervals,
+ // so the testing for subtractionInterval covers the actual interval
+ // calculations.
+ {
+ input: [
+ [
+ [1, 2],
+ [3, 4],
+ ],
+ [
+ [-10, -20],
+ [-30, -40],
+ ],
+ ],
+ expected: [
+ [11, 22],
+ [33, 44],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ ],
+ [
+ [-10, -20],
+ [-30, -40],
+ [-50, -60],
+ ],
+ ],
+ expected: [
+ [11, 22],
+ [33, 44],
+ [55, 66],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ [7, 8],
+ ],
+ [
+ [-10, -20],
+ [-30, -40],
+ [-50, -60],
+ [-70, -80],
+ ],
+ ],
+ expected: [
+ [11, 22],
+ [33, 44],
+ [55, 66],
+ [77, 88],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3],
+ [4, 5, 6],
+ ],
+ [
+ [-10, -20, -30],
+ [-40, -50, -60],
+ ],
+ ],
+ expected: [
+ [11, 22, 33],
+ [44, 55, 66],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ ],
+ [
+ [-10, -20, -30],
+ [-40, -50, -60],
+ [-70, -80, -90],
+ ],
+ ],
+ expected: [
+ [11, 22, 33],
+ [44, 55, 66],
+ [77, 88, 99],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ [10, 11, 12],
+ ],
+ [
+ [-10, -20, -30],
+ [-40, -50, -60],
+ [-70, -80, -90],
+ [-1000, -1100, -1200],
+ ],
+ ],
+ expected: [
+ [11, 22, 33],
+ [44, 55, 66],
+ [77, 88, 99],
+ [1010, 1111, 1212],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ ],
+ [
+ [-10, -20, -30, -40],
+ [-50, -60, -70, -80],
+ ],
+ ],
+ expected: [
+ [11, 22, 33, 44],
+ [55, 66, 77, 88],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ ],
+ [
+ [-10, -20, -30, -40],
+ [-50, -60, -70, -80],
+ [-90, -1000, -1100, -1200],
+ ],
+ ],
+ expected: [
+ [11, 22, 33, 44],
+ [55, 66, 77, 88],
+ [99, 1010, 1111, 1212],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ [13, 14, 15, 16],
+ ],
+ [
+ [-10, -20, -30, -40],
+ [-50, -60, -70, -80],
+ [-90, -1000, -1100, -1200],
+ [-1300, -1400, -1500, -1600],
+ ],
+ ],
+ expected: [
+ [11, 22, 33, 44],
+ [55, 66, 77, 88],
+ [99, 1010, 1111, 1212],
+ [1313, 1414, 1515, 1616],
+ ],
+ },
+ ])
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toMatrix(t.params.expected);
+ const got = trait.subtractionMatrixMatrixInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.subtractionMatrixMatrixInterval([${JSON.stringify(x)}], [${JSON.stringify(
+ y
+ )}]) returned '[${JSON.stringify(got)}]'. Expected '[${JSON.stringify(expected)}]'`
+ );
+ });
+
+g.test('multiplicationMatrixMatrixInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .combineWithParams<MatrixPairToMatrixCase>([
+ // Only testing that different shapes of matrices are handled correctly
+ // here, to reduce test duplication.
+ // multiplicationMatrixMatrixInterval uses and transposeInterval &
+ // dotInterval for calculating intervals, so the testing for those functions
+ // will cover the actual interval calculations.
+ // Keep all expected result integer no larger than 2047 to ensure that all result is exactly
+ // represeantable in both f32 and f16.
+ {
+ input: [
+ [
+ [1, 2],
+ [3, 4],
+ ],
+ [
+ [11, 22],
+ [33, 44],
+ ],
+ ],
+ expected: [
+ [77, 110],
+ [165, 242],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2],
+ [3, 4],
+ ],
+ [
+ [11, 22],
+ [33, 44],
+ [55, 66],
+ ],
+ ],
+ expected: [
+ [77, 110],
+ [165, 242],
+ [253, 374],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2],
+ [3, 4],
+ ],
+ [
+ [11, 22],
+ [33, 44],
+ [55, 66],
+ [77, 88],
+ ],
+ ],
+ expected: [
+ [77, 110],
+ [165, 242],
+ [253, 374],
+ [341, 506],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3],
+ [4, 5, 6],
+ ],
+ [
+ [11, 22],
+ [33, 44],
+ ],
+ ],
+ expected: [
+ [99, 132, 165],
+ [209, 286, 363],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3],
+ [4, 5, 6],
+ ],
+ [
+ [11, 22],
+ [33, 44],
+ [55, 66],
+ ],
+ ],
+ expected: [
+ [99, 132, 165],
+ [209, 286, 363],
+ [319, 440, 561],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3],
+ [4, 5, 6],
+ ],
+ [
+ [11, 22],
+ [33, 44],
+ [55, 66],
+ [77, 88],
+ ],
+ ],
+ expected: [
+ [99, 132, 165],
+ [209, 286, 363],
+ [319, 440, 561],
+ [429, 594, 759],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ ],
+ [
+ [11, 22],
+ [33, 44],
+ ],
+ ],
+ expected: [
+ [121, 154, 187, 220],
+ [253, 330, 407, 484],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ ],
+ [
+ [11, 22],
+ [33, 44],
+ [55, 66],
+ ],
+ ],
+ expected: [
+ [121, 154, 187, 220],
+ [253, 330, 407, 484],
+ [385, 506, 627, 748],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ ],
+ [
+ [11, 22],
+ [33, 44],
+ [55, 66],
+ [77, 88],
+ ],
+ ],
+ expected: [
+ [121, 154, 187, 220],
+ [253, 330, 407, 484],
+ [385, 506, 627, 748],
+ [517, 682, 847, 1012],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ ],
+ [
+ [11, 22, 33],
+ [44, 55, 66],
+ ],
+ ],
+ expected: [
+ [242, 308],
+ [539, 704],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ ],
+ [
+ [11, 22, 33],
+ [44, 55, 66],
+ [77, 88, 99],
+ ],
+ ],
+ expected: [
+ [242, 308],
+ [539, 704],
+ [836, 1100],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ ],
+ [
+ [11, 22, 33],
+ [44, 55, 66],
+ [77, 88, 99],
+ [10, 11, 12],
+ ],
+ ],
+ expected: [
+ [242, 308],
+ [539, 704],
+ [836, 1100],
+ [103, 136],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ ],
+ [
+ [11, 22, 33],
+ [44, 55, 66],
+ ],
+ ],
+ expected: [
+ [330, 396, 462],
+ [726, 891, 1056],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ ],
+ [
+ [11, 22, 33],
+ [44, 55, 66],
+ [77, 88, 99],
+ ],
+ ],
+ expected: [
+ [330, 396, 462],
+ [726, 891, 1056],
+ [1122, 1386, 1650],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ ],
+ [
+ [11, 22, 33],
+ [44, 55, 66],
+ [77, 88, 99],
+ [10, 11, 12],
+ ],
+ ],
+ expected: [
+ [330, 396, 462],
+ [726, 891, 1056],
+ [1122, 1386, 1650],
+ [138, 171, 204],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ ],
+ [
+ [11, 12, 13],
+ [21, 22, 23],
+ ],
+ ],
+ expected: [
+ [188, 224, 260, 296],
+ [338, 404, 470, 536],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ ],
+ [
+ [11, 12, 13],
+ [21, 22, 23],
+ [31, 32, 33],
+ ],
+ ],
+ expected: [
+ [188, 224, 260, 296],
+ [338, 404, 470, 536],
+ [488, 584, 680, 776],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ ],
+ [
+ [11, 12, 13],
+ [21, 22, 23],
+ [31, 32, 33],
+ [41, 42, 43],
+ ],
+ ],
+ expected: [
+ [188, 224, 260, 296],
+ [338, 404, 470, 536],
+ [488, 584, 680, 776],
+ [638, 764, 890, 1016],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ [7, 8],
+ ],
+ [
+ [11, 22, 33, 44],
+ [55, 66, 77, 88],
+ ],
+ ],
+ expected: [
+ [550, 660],
+ [1254, 1540],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ [7, 8],
+ ],
+ [
+ [11, 12, 13, 14],
+ [21, 22, 23, 24],
+ [31, 32, 33, 34],
+ ],
+ ],
+ expected: [
+ [210, 260],
+ [370, 460],
+ [530, 660],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ [7, 8],
+ ],
+ [
+ [11, 12, 13, 14],
+ [21, 22, 23, 24],
+ [31, 32, 33, 34],
+ [41, 42, 43, 44],
+ ],
+ ],
+ expected: [
+ [210, 260],
+ [370, 460],
+ [530, 660],
+ [690, 860],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ [10, 11, 12],
+ ],
+ [
+ [11, 12, 13, 14],
+ [21, 22, 23, 24],
+ ],
+ ],
+ expected: [
+ [290, 340, 390],
+ [510, 600, 690],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ [10, 11, 12],
+ ],
+ [
+ [11, 12, 13, 14],
+ [21, 22, 23, 24],
+ [31, 32, 33, 34],
+ ],
+ ],
+ expected: [
+ [290, 340, 390],
+ [510, 600, 690],
+ [730, 860, 990],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ [10, 11, 12],
+ ],
+ [
+ [11, 12, 13, 14],
+ [21, 22, 23, 24],
+ [31, 32, 33, 34],
+ [41, 42, 43, 44],
+ ],
+ ],
+ expected: [
+ [290, 340, 390],
+ [510, 600, 690],
+ [730, 860, 990],
+ [950, 1120, 1290],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ [13, 14, 15, 16],
+ ],
+ [
+ [11, 12, 13, 14],
+ [21, 22, 23, 24],
+ ],
+ ],
+ expected: [
+ [370, 420, 470, 520],
+ [650, 740, 830, 920],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ [13, 14, 15, 16],
+ ],
+ [
+ [11, 12, 13, 14],
+ [21, 22, 23, 24],
+ [31, 32, 33, 34],
+ ],
+ ],
+ expected: [
+ [370, 420, 470, 520],
+ [650, 740, 830, 920],
+ [930, 1060, 1190, 1320],
+ ],
+ },
+ {
+ input: [
+ [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ [13, 14, 15, 16],
+ ],
+ [
+ [11, 12, 13, 14],
+ [21, 22, 23, 24],
+ [31, 32, 33, 34],
+ [41, 42, 43, 44],
+ ],
+ ],
+ expected: [
+ [370, 420, 470, 520],
+ [650, 740, 830, 920],
+ [930, 1060, 1190, 1320],
+ [1210, 1380, 1550, 1720],
+ ],
+ },
+ ])
+ )
+ .fn(t => {
+ const [x, y] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toMatrix(t.params.expected);
+ const got = trait.multiplicationMatrixMatrixInterval(x, y);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.multiplicationMatrixMatrixInterval([${JSON.stringify(
+ x
+ )}], [${JSON.stringify(y)}]) returned '[${JSON.stringify(got)}]'. Expected '[${JSON.stringify(
+ expected
+ )}]'`
+ );
+ });
+
+interface MatrixScalarToMatrixCase {
+ matrix: number[][];
+ scalar: number;
+ expected: (number | IntervalBounds)[][];
+}
+
+const kMultiplicationMatrixScalarIntervalCases = {
+ f32: [
+ // From https://github.com/gpuweb/cts/issues/3044
+ {
+ matrix: [
+ [kValue.f32.negative.min, 0],
+ [0, 0],
+ ],
+ scalar: kValue.f32.negative.subnormal.min,
+ expected: [
+ [[0, reinterpretU32AsF32(0x407ffffe)], 0], // [[0, 3.9999995...], 0],
+ [0, 0],
+ ],
+ },
+ ] as MatrixScalarToMatrixCase[],
+ f16: [
+ // From https://github.com/gpuweb/cts/issues/3044
+ {
+ matrix: [
+ [kValue.f16.negative.min, 0],
+ [0, 0],
+ ],
+ scalar: kValue.f16.negative.subnormal.min,
+ expected: [
+ [[0, reinterpretU16AsF16(0x43fe)], 0], // [[0, 3.99609375], 0]
+ [0, 0],
+ ],
+ },
+ ] as MatrixScalarToMatrixCase[],
+} as const;
+
+g.test('multiplicationMatrixScalarInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<MatrixScalarToMatrixCase>(p => {
+ // Primarily testing that different shapes of matrices are handled correctly
+ // here, to reduce test duplication. Additional testing for edge case
+ // discovered in https://github.com/gpuweb/cts/issues/3044.
+ //
+ // multiplicationMatrixScalarInterval uses for calculating intervals,
+ // so the testing for multiplicationInterval covers the actual interval
+ // calculations.
+ return [
+ {
+ matrix: [
+ [1, 2],
+ [3, 4],
+ ],
+ scalar: 10,
+ expected: [
+ [10, 20],
+ [30, 40],
+ ],
+ },
+ {
+ matrix: [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ ],
+ scalar: 10,
+ expected: [
+ [10, 20],
+ [30, 40],
+ [50, 60],
+ ],
+ },
+ {
+ matrix: [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ [7, 8],
+ ],
+ scalar: 10,
+ expected: [
+ [10, 20],
+ [30, 40],
+ [50, 60],
+ [70, 80],
+ ],
+ },
+ {
+ matrix: [
+ [1, 2, 3],
+ [4, 5, 6],
+ ],
+ scalar: 10,
+ expected: [
+ [10, 20, 30],
+ [40, 50, 60],
+ ],
+ },
+ {
+ matrix: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ ],
+ scalar: 10,
+ expected: [
+ [10, 20, 30],
+ [40, 50, 60],
+ [70, 80, 90],
+ ],
+ },
+ {
+ matrix: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ [10, 11, 12],
+ ],
+ scalar: 10,
+ expected: [
+ [10, 20, 30],
+ [40, 50, 60],
+ [70, 80, 90],
+ [100, 110, 120],
+ ],
+ },
+ {
+ matrix: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ ],
+ scalar: 10,
+ expected: [
+ [10, 20, 30, 40],
+ [50, 60, 70, 80],
+ ],
+ },
+ {
+ matrix: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ ],
+ scalar: 10,
+ expected: [
+ [10, 20, 30, 40],
+ [50, 60, 70, 80],
+ [90, 100, 110, 120],
+ ],
+ },
+ {
+ matrix: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ [13, 14, 15, 16],
+ ],
+ scalar: 10,
+ expected: [
+ [10, 20, 30, 40],
+ [50, 60, 70, 80],
+ [90, 100, 110, 120],
+ [130, 140, 150, 160],
+ ],
+ },
+ ...kMultiplicationMatrixScalarIntervalCases[p.trait],
+ ];
+ })
+ )
+ .fn(t => {
+ const matrix = t.params.matrix;
+ const scalar = t.params.scalar;
+ const trait = FP[t.params.trait];
+ const expected = trait.toMatrix(t.params.expected);
+ const got = trait.multiplicationMatrixScalarInterval(matrix, scalar);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.multiplicationMatrixScalarInterval([${JSON.stringify(
+ matrix
+ )}], ${scalar}) returned '[${JSON.stringify(got)}]'. Expected '[${JSON.stringify(expected)}]'`
+ );
+ });
+
+// There are no explicit tests for multiplicationScalarMatrixInterval, since it
+// is just a pass-through to multiplicationMatrixScalarInterval
+
+interface MatrixVectorToVectorCase {
+ matrix: number[][];
+ vector: number[];
+ expected: (number | IntervalBounds)[];
+}
+
+g.test('multiplicationMatrixVectorInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .combineWithParams<MatrixVectorToVectorCase>([
+ // Only testing that different shapes of matrices are handled correctly
+ // here, to reduce test duplication.
+ // multiplicationMatrixVectorInterval uses DotIntervalOp &
+ // TransposeIntervalOp for calculating intervals, so the testing for
+ // dotInterval & transposeInterval covers the actual interval
+ // calculations.
+ {
+ matrix: [
+ [1, 2],
+ [3, 4],
+ ],
+ vector: [11, 22],
+ expected: [77, 110],
+ },
+ {
+ matrix: [
+ [1, 2, 3],
+ [4, 5, 6],
+ ],
+ vector: [11, 22],
+ expected: [99, 132, 165],
+ },
+ {
+ matrix: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ ],
+ vector: [11, 22],
+ expected: [121, 154, 187, 220],
+ },
+ {
+ matrix: [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ ],
+ vector: [11, 22, 33],
+ expected: [242, 308],
+ },
+ {
+ matrix: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ ],
+ vector: [11, 22, 33],
+ expected: [330, 396, 462],
+ },
+ {
+ matrix: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ ],
+ vector: [11, 22, 33],
+ expected: [418, 484, 550, 616],
+ },
+ {
+ matrix: [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ [7, 8],
+ ],
+ vector: [11, 22, 33, 44],
+ expected: [550, 660],
+ },
+ {
+ matrix: [
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9],
+ [10, 11, 12],
+ ],
+ vector: [11, 22, 33, 44],
+ expected: [770, 880, 990],
+ },
+ {
+ matrix: [
+ [1, 2, 3, 4],
+ [5, 6, 7, 8],
+ [9, 10, 11, 12],
+ [13, 14, 15, 16],
+ ],
+ vector: [11, 22, 33, 44],
+ expected: [990, 1100, 1210, 1320],
+ },
+ ])
+ )
+ .fn(t => {
+ const matrix = t.params.matrix;
+ const vector = t.params.vector;
+ const trait = FP[t.params.trait];
+ const expected = trait.toVector(t.params.expected);
+ const got = trait.multiplicationMatrixVectorInterval(matrix, vector);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.multiplicationMatrixVectorInterval([${JSON.stringify(
+ matrix
+ )}], [${JSON.stringify(vector)}]) returned '[${JSON.stringify(
+ got
+ )}]'. Expected '[${JSON.stringify(expected)}]'`
+ );
+ });
+
+interface VectorMatrixToVectorCase {
+ vector: number[];
+ matrix: number[][];
+ expected: (number | IntervalBounds)[];
+}
+
+g.test('multiplicationVectorMatrixInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .combineWithParams<VectorMatrixToVectorCase>([
+ // Only testing that different shapes of matrices are handled correctly
+ // here, to reduce test duplication.
+ // multiplicationVectorMatrixInterval uses DotIntervalOp for calculating
+ // intervals, so the testing for dotInterval covers the actual interval
+ // calculations.
+ // Keep all expected result integer no larger than 2047 to ensure that all result is exactly
+ // represeantable in both f32 and f16.
+ {
+ vector: [1, 2],
+ matrix: [
+ [11, 22],
+ [33, 44],
+ ],
+ expected: [55, 121],
+ },
+ {
+ vector: [1, 2],
+ matrix: [
+ [11, 22],
+ [33, 44],
+ [55, 66],
+ ],
+ expected: [55, 121, 187],
+ },
+ {
+ vector: [1, 2],
+ matrix: [
+ [11, 22],
+ [33, 44],
+ [55, 66],
+ [77, 88],
+ ],
+ expected: [55, 121, 187, 253],
+ },
+ {
+ vector: [1, 2, 3],
+ matrix: [
+ [11, 12, 13],
+ [21, 22, 23],
+ ],
+ expected: [74, 134],
+ },
+ {
+ vector: [1, 2, 3],
+ matrix: [
+ [11, 12, 13],
+ [21, 22, 23],
+ [31, 32, 33],
+ ],
+ expected: [74, 134, 194],
+ },
+ {
+ vector: [1, 2, 3],
+ matrix: [
+ [11, 12, 13],
+ [21, 22, 23],
+ [31, 32, 33],
+ [41, 42, 43],
+ ],
+ expected: [74, 134, 194, 254],
+ },
+ {
+ vector: [1, 2, 3, 4],
+ matrix: [
+ [11, 12, 13, 14],
+ [21, 22, 23, 24],
+ ],
+ expected: [130, 230],
+ },
+ {
+ vector: [1, 2, 3, 4],
+ matrix: [
+ [11, 12, 13, 14],
+ [21, 22, 23, 24],
+ [31, 32, 33, 34],
+ ],
+ expected: [130, 230, 330],
+ },
+ {
+ vector: [1, 2, 3, 4],
+ matrix: [
+ [11, 12, 13, 14],
+ [21, 22, 23, 24],
+ [31, 32, 33, 34],
+ [41, 42, 43, 44],
+ ],
+ expected: [130, 230, 330, 430],
+ },
+ ])
+ )
+ .fn(t => {
+ const vector = t.params.vector;
+ const matrix = t.params.matrix;
+ const trait = FP[t.params.trait];
+ const expected = trait.toVector(t.params.expected);
+ const got = trait.multiplicationVectorMatrixInterval(vector, matrix);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.multiplicationVectorMatrixInterval([${JSON.stringify(
+ vector
+ )}], [${JSON.stringify(matrix)}]) returned '[${JSON.stringify(
+ got
+ )}]'. Expected '[${JSON.stringify(expected)}]'`
+ );
+ });
+
+// API - Acceptance Intervals w/ bespoke implementations
+
+interface FaceForwardCase {
+ input: [number[], number[], number[]];
+ expected: ((number | IntervalBounds)[] | undefined)[];
+}
+
+g.test('faceForwardIntervals')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<FaceForwardCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ // vec2
+ { input: [[1.0, 0.0], [1.0, 0.0], [1.0, 0.0]], expected: [[-1.0, 0.0]] },
+ { input: [[-1.0, 0.0], [1.0, 0.0], [1.0, 0.0]], expected: [[1.0, 0.0]] },
+ { input: [[1.0, 0.0], [-1.0, 1.0], [1.0, -1.0]], expected: [[1.0, 0.0]] },
+ { input: [[-1.0, 0.0], [-1.0, 1.0], [1.0, -1.0]], expected: [[-1.0, 0.0]] },
+ { input: [[10.0, 0.0], [10.0, 0.0], [10.0, 0.0]], expected: [[-10.0, 0.0]] },
+ { input: [[-10.0, 0.0], [10.0, 0.0], [10.0, 0.0]], expected: [[10.0, 0.0]] },
+ { input: [[10.0, 0.0], [-10.0, 10.0], [10.0, -10.0]], expected: [[10.0, 0.0]] },
+ { input: [[-10.0, 0.0], [-10.0, 10.0], [10.0, -10.0]], expected: [[-10.0, 0.0]] },
+ { input: [[0.1, 0.0], [0.1, 0.0], [0.1, 0.0]], expected: [[kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'], 0.0]] },
+ { input: [[-0.1, 0.0], [0.1, 0.0], [0.1, 0.0]], expected: [[kConstantCorrectlyRoundedExpectation[p.trait]['0.1'], 0.0]] },
+ { input: [[0.1, 0.0], [-0.1, 0.1], [0.1, -0.1]], expected: [[kConstantCorrectlyRoundedExpectation[p.trait]['0.1'], 0.0]] },
+ { input: [[-0.1, 0.0], [-0.1, 0.1], [0.1, -0.1]], expected: [[kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'], 0.0]] },
+
+ // vec3
+ { input: [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [[-1.0, 0.0, 0.0]] },
+ { input: [[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], expected: [[1.0, 0.0, 0.0]] },
+ { input: [[1.0, 0.0, 0.0], [-1.0, 1.0, 0.0], [1.0, -1.0, 0.0]], expected: [[1.0, 0.0, 0.0]] },
+ { input: [[-1.0, 0.0, 0.0], [-1.0, 1.0, 0.0], [1.0, -1.0, 0.0]], expected: [[-1.0, 0.0, 0.0]] },
+ { input: [[10.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 0.0, 0.0]], expected: [[-10.0, 0.0, 0.0]] },
+ { input: [[-10.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 0.0, 0.0]], expected: [[10.0, 0.0, 0.0]] },
+ { input: [[10.0, 0.0, 0.0], [-10.0, 10.0, 0.0], [10.0, -10.0, 0.0]], expected: [[10.0, 0.0, 0.0]] },
+ { input: [[-10.0, 0.0, 0.0], [-10.0, 10.0, 0.0], [10.0, -10.0, 0.0]], expected: [[-10.0, 0.0, 0.0]] },
+ { input: [[0.1, 0.0, 0.0], [0.1, 0.0, 0.0], [0.1, 0.0, 0.0]], expected: [[kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'], 0.0, 0.0]] },
+ { input: [[-0.1, 0.0, 0.0], [0.1, 0.0, 0.0], [0.1, 0.0, 0.0]], expected: [[kConstantCorrectlyRoundedExpectation[p.trait]['0.1'], 0.0, 0.0]] },
+ { input: [[0.1, 0.0, 0.0], [-0.1, 0.0, 0.0], [0.1, -0.0, 0.0]], expected: [[kConstantCorrectlyRoundedExpectation[p.trait]['0.1'], 0.0, 0.0]] },
+ { input: [[-0.1, 0.0, 0.0], [-0.1, 0.0, 0.0], [0.1, -0.0, 0.0]], expected: [[kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'], 0.0, 0.0]] },
+
+ // vec4
+ { input: [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [[-1.0, 0.0, 0.0, 0.0]] },
+ { input: [[-1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]], expected: [[1.0, 0.0, 0.0, 0.0]] },
+ { input: [[1.0, 0.0, 0.0, 0.0], [-1.0, 1.0, 0.0, 0.0], [1.0, -1.0, 0.0, 0.0]], expected: [[1.0, 0.0, 0.0, 0.0]] },
+ { input: [[-1.0, 0.0, 0.0, 0.0], [-1.0, 1.0, 0.0, 0.0], [1.0, -1.0, 0.0, 0.0]], expected: [[-1.0, 0.0, 0.0, 0.0]] },
+ { input: [[10.0, 0.0, 0.0, 0.0], [10.0, 0.0, 0.0, 0.0], [10.0, 0.0, 0.0, 0.0]], expected: [[-10.0, 0.0, 0.0, 0.0]] },
+ { input: [[-10.0, 0.0, 0.0, 0.0], [10.0, 0.0, 0.0, 0.0], [10.0, 0.0, 0.0, 0.0]], expected: [[10.0, 0.0, 0.0, 0.0]] },
+ { input: [[10.0, 0.0, 0.0, 0.0], [-10.0, 10.0, 0.0, 0.0], [10.0, -10.0, 0.0, 0.0]], expected: [[10.0, 0.0, 0.0, 0.0]] },
+ { input: [[-10.0, 0.0, 0.0, 0.0], [-10.0, 10.0, 0.0, 0.0], [10.0, -10.0, 0.0, 0.0]], expected: [[-10.0, 0.0, 0.0, 0.0]] },
+ { input: [[0.1, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0]], expected: [[kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'], 0.0, 0.0, 0.0]] },
+ { input: [[-0.1, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0], [0.1, 0.0, 0.0, 0.0]], expected: [[kConstantCorrectlyRoundedExpectation[p.trait]['0.1'], 0.0, 0.0, 0.0]] },
+ { input: [[0.1, 0.0, 0.0, 0.0], [-0.1, 0.0, 0.0, 0.0], [0.1, -0.0, 0.0, 0.0]], expected: [[kConstantCorrectlyRoundedExpectation[p.trait]['0.1'], 0.0, 0.0, 0.0]] },
+ { input: [[-0.1, 0.0, 0.0, 0.0], [-0.1, 0.0, 0.0, 0.0], [0.1, -0.0, 0.0, 0.0]], expected: [[kConstantCorrectlyRoundedExpectation[p.trait]['-0.1'], 0.0, 0.0, 0.0]] },
+
+ // dot(y, z) === 0
+ { input: [[1.0, 1.0], [1.0, 0.0], [0.0, 1.0]], expected: [[-1.0, -1.0]] },
+
+ // subnormals, also dot(y, z) spans 0
+ { input: [[constants.positive.subnormal.max, 0.0], [constants.positive.subnormal.min, 0.0], [constants.negative.subnormal.min, 0.0]], expected: [[[0.0, constants.positive.subnormal.max], 0.0], [[constants.negative.subnormal.min, 0], 0.0]] },
+
+ // dot going OOB returns [undefined, x, -x]
+ { input: [[1.0, 1.0], [constants.positive.max, constants.positive.max], [constants.positive.max, constants.positive.max]], expected: [undefined, [1, 1], [-1, -1]] },
+ ];
+ })
+ )
+ .fn(t => {
+ const [x, y, z] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = t.params.expected.map(e => (e !== undefined ? trait.toVector(e) : undefined));
+ const got = trait.faceForwardIntervals(x, y, z);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.faceForwardInterval([${x}], [${y}], [${z}]) returned [${got}]. Expected [${expected}]`
+ );
+ });
+
+interface ModfCase {
+ input: number;
+ fract: number | IntervalBounds;
+ whole: number | IntervalBounds;
+}
+
+g.test('modfInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'abstract'] as const)
+ .beginSubcases()
+ .expandWithParams<ModfCase>(p => {
+ const constants = FP[p.trait].constants();
+ // prettier-ignore
+ return [
+ // Normals
+ { input: 0, fract: 0, whole: 0 },
+ { input: 1, fract: 0, whole: 1 },
+ { input: -1, fract: 0, whole: -1 },
+ { input: 0.5, fract: 0.5, whole: 0 },
+ { input: -0.5, fract: -0.5, whole: 0 },
+ { input: 2.5, fract: 0.5, whole: 2 },
+ { input: -2.5, fract: -0.5, whole: -2 },
+ { input: 10.0, fract: 0, whole: 10 },
+ { input: -10.0, fract: 0, whole: -10 },
+
+ // Subnormals
+ { input: constants.positive.subnormal.min, fract: [0, constants.positive.subnormal.min], whole: 0 },
+ { input: constants.positive.subnormal.max, fract: [0, constants.positive.subnormal.max], whole: 0 },
+ { input: constants.negative.subnormal.min, fract: [constants.negative.subnormal.min, 0], whole: 0 },
+ { input: constants.negative.subnormal.max, fract: [constants.negative.subnormal.max, 0], whole: 0 },
+
+ // Boundaries
+ { input: constants.negative.min, fract: 0, whole: constants.negative.min },
+ { input: constants.negative.max, fract: constants.negative.max, whole: 0 },
+ { input: constants.positive.min, fract: constants.positive.min, whole: 0 },
+ { input: constants.positive.max, fract: 0, whole: constants.positive.max },
+ ];
+ })
+ )
+ .fn(t => {
+ const trait = FP[t.params.trait];
+ const expected = {
+ fract: trait.toInterval(t.params.fract),
+ whole: trait.toInterval(t.params.whole),
+ };
+
+ const got = trait.modfInterval(t.params.input);
+ t.expect(
+ objectEquals(expected, got),
+ `${trait}.modfInterval([${t.params.input}) returned { fract: [${got.fract}], whole: [${got.whole}] }. Expected { fract: [${expected.fract}], whole: [${expected.whole}] }`
+ );
+ });
+
+interface RefractCase {
+ input: [number[], number[], number];
+ expected: (number | IntervalBounds)[];
+}
+
+// Scope for refractInterval tests so that they can have constants for magic
+// numbers that don't pollute the global namespace or have unwieldy long names.
+{
+ const kNegativeOneBounds = {
+ f32: [
+ reinterpretU64AsF64(0xbff0_0000_c000_0000n),
+ reinterpretU64AsF64(0xbfef_ffff_4000_0000n),
+ ] as IntervalBounds,
+ f16: [reinterpretU16AsF16(0xbc06), reinterpretU16AsF16(0xbbfa)] as IntervalBounds,
+ } as const;
+
+ // prettier-ignore
+ const kRefractIntervalCases = {
+ f32: [
+ // k > 0
+ // vec2
+ { input: [[1, -2], [3, 4], 5], expected: [[reinterpretU32AsF32(0x40ce87a4), reinterpretU32AsF32(0x40ce8840)], // ~6.454...
+ [reinterpretU32AsF32(0xc100fae8), reinterpretU32AsF32(0xc100fa80)]] }, // ~-8.061...
+ // vec3
+ { input: [[1, -2, 3], [-4, 5, -6], 7], expected: [[reinterpretU32AsF32(0x40d24480), reinterpretU32AsF32(0x40d24c00)], // ~6.571...
+ [reinterpretU32AsF32(0xc1576f80), reinterpretU32AsF32(0xc1576ad0)], // ~-13.464...
+ [reinterpretU32AsF32(0x41a2d9b0), reinterpretU32AsF32(0x41a2dc80)]] }, // ~20.356...
+ // vec4
+ { input: [[1, -2, 3, -4], [-5, 6, -7, 8], 9], expected: [[reinterpretU32AsF32(0x410ae480), reinterpretU32AsF32(0x410af240)], // ~8.680...
+ [reinterpretU32AsF32(0xc18cf7c0), reinterpretU32AsF32(0xc18cef80)], // ~-17.620...
+ [reinterpretU32AsF32(0x41d46cc0), reinterpretU32AsF32(0x41d47660)], // ~26.553...
+ [reinterpretU32AsF32(0xc20dfa80), reinterpretU32AsF32(0xc20df500)]] }, // ~-35.494...
+ ] as RefractCase[],
+ f16: [
+ // k > 0
+ // vec2
+ { input: [[1, -2], [3, 4], 5], expected: [[reinterpretU16AsF16(0x4620), reinterpretU16AsF16(0x46bc)], // ~6.454...
+ [reinterpretU16AsF16(0xc840), reinterpretU16AsF16(0xc7b0)]] }, // ~-8.061...
+ // vec3
+ { input: [[1, -2, 3], [-4, 5, -6], 7], expected: [[reinterpretU16AsF16(0x4100), reinterpretU16AsF16(0x4940)], // ~6.571...
+ [reinterpretU16AsF16(0xcc98), reinterpretU16AsF16(0xc830)], // ~-13.464...
+ [reinterpretU16AsF16(0x4b20), reinterpretU16AsF16(0x4e90)]] }, // ~20.356...
+ // vec4
+ // x = [1, -2, 3, -4], y = [-5, 6, -7, 8], z = 9,
+ // dot(y, x) = -71, k = 1.0 - 9 * 9 * (1.0 - 71 * 71) = 408241 overflow f16.
+ { input: [[1, -2, 3, -4], [-5, 6, -7, 8], 9], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] },
+ // x = [1, -2, 3, -4], y = [-5, 4, -3, 2], z = 2.5,
+ // dot(y, x) = -30, k = 1.0 - 2.5 * 2.5 * (1.0 - 30 * 30) = 5619.75.
+ // a = z * dot(y, x) + sqrt(k) = ~-0.035, result is about z * x - a * y = [~2.325, ~-4.86, ~7.4025, ~-9.93]
+ { input: [[1, -2, 3, -4], [-5, 4, -3, 2], 2.5], expected: [[reinterpretU16AsF16(0x3900), reinterpretU16AsF16(0x4410)], // ~2.325
+ [reinterpretU16AsF16(0xc640), reinterpretU16AsF16(0xc300)], // ~-4.86
+ [reinterpretU16AsF16(0x4660), reinterpretU16AsF16(0x4838)], // ~7.4025
+ [reinterpretU16AsF16(0xc950), reinterpretU16AsF16(0xc8a0)]] }, // ~-9.93
+ ] as RefractCase[],
+ } as const;
+
+ g.test('refractInterval')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16'] as const)
+ .beginSubcases()
+ .expandWithParams<RefractCase>(p => {
+ const trait = FP[p.trait];
+ const constants = trait.constants();
+ // prettier-ignore
+ return [
+ ...kRefractIntervalCases[p.trait],
+
+ // k < 0
+ { input: [[1, 1], [0.1, 0], 10], expected: [0, 0] },
+
+ // k contains 0
+ { input: [[1, 1], [0.1, 0], 1.005038], expected: [kUnboundedBounds, kUnboundedBounds] },
+
+ // k > 0
+ // vec2
+ { input: [[1, 1], [1, 0], 1], expected: [kNegativeOneBounds[p.trait], 1] },
+ // vec3
+ { input: [[1, 1, 1], [1, 0, 0], 1], expected: [kNegativeOneBounds[p.trait], 1, 1] },
+ // vec4
+ { input: [[1, 1, 1, 1], [1, 0, 0, 0], 1], expected: [kNegativeOneBounds[p.trait], 1, 1, 1] },
+
+ // Test that dot going OOB bounds in the intermediate calculations propagates
+ { input: [[constants.positive.nearest_max, constants.positive.max, constants.negative.min], [1.0, 1.0, 1.0], 1], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] },
+ { input: [[constants.positive.nearest_max, constants.negative.min, constants.positive.max], [1.0, 1.0, 1.0], 1], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] },
+ { input: [[constants.positive.max, constants.positive.nearest_max, constants.negative.min], [1.0, 1.0, 1.0], 1], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] },
+ { input: [[constants.negative.min, constants.positive.nearest_max, constants.positive.max], [1.0, 1.0, 1.0], 1], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] },
+ { input: [[constants.positive.max, constants.negative.min, constants.positive.nearest_max], [1.0, 1.0, 1.0], 1], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] },
+ { input: [[constants.negative.min, constants.positive.max, constants.positive.nearest_max], [1.0, 1.0, 1.0], 1], expected: [kUnboundedBounds, kUnboundedBounds, kUnboundedBounds] },
+ ];
+ })
+ )
+ .fn(t => {
+ const [i, s, r] = t.params.input;
+ const trait = FP[t.params.trait];
+ const expected = trait.toVector(t.params.expected);
+ const got = trait.refractInterval(i, s, r);
+ t.expect(
+ objectEquals(expected, got),
+ `${t.params.trait}.refractIntervals([${i}], [${s}], ${r}) returned [${got}]. Expected [${expected}]`
+ );
+ });
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/getStackTrace.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/getStackTrace.spec.ts
new file mode 100644
index 0000000000..5090fe3f9d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/getStackTrace.spec.ts
@@ -0,0 +1,138 @@
+export const description = `
+Tests for getStackTrace.
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { extractImportantStackTrace } from '../common/internal/stack.js';
+
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+g.test('stacks')
+ .paramsSimple([
+ {
+ case: 'node_fail',
+ _expectedLines: 3,
+ _stack: `Error:
+ at CaseRecorder.fail (/Users/kainino/src/cts/src/common/framework/logger.ts:99:30)
+ at RunCaseSpecific.exports.g.test.t [as fn] (/Users/kainino/src/cts/src/unittests/logger.spec.ts:80:7)
+ at RunCaseSpecific.run (/Users/kainino/src/cts/src/common/framework/test_group.ts:121:18)
+ at processTicksAndRejections (internal/process/task_queues.js:86:5)`,
+ },
+ {
+ // MAINTENANCE_TODO: make sure this test case actually matches what happens on windows
+ case: 'node_fail_backslash',
+ _expectedLines: 3,
+ _stack: `Error:
+ at CaseRecorder.fail (C:\\Users\\kainino\\src\\cts\\src\\common\\framework\\logger.ts:99:30)
+ at RunCaseSpecific.exports.g.test.t [as fn] (C:\\Users\\kainino\\src\\cts\\src\\unittests\\logger.spec.ts:80:7)
+ at RunCaseSpecific.run (C:\\Users\\kainino\\src\\cts\\src\\common\\framework\\test_group.ts:121:18)
+ at processTicksAndRejections (internal\\process\\task_queues.js:86:5)`,
+ },
+ {
+ case: 'node_fail_processTicksAndRejections',
+ _expectedLines: 5,
+ _stack: `Error: expectation had no effect: suite1:foo:
+ at Object.generateMinimalQueryList (/Users/kainino/src/cts/src/common/framework/generate_minimal_query_list.ts:72:24)
+ at testGenerateMinimalQueryList (/Users/kainino/src/cts/src/unittests/loading.spec.ts:289:25)
+ at processTicksAndRejections (internal/process/task_queues.js:93:5)
+ at RunCaseSpecific.fn (/Users/kainino/src/cts/src/unittests/loading.spec.ts:300:3)
+ at RunCaseSpecific.run (/Users/kainino/src/cts/src/common/framework/test_group.ts:144:9)
+ at /Users/kainino/src/cts/src/common/runtime/cmdline.ts:62:25
+ at async Promise.all (index 29)
+ at /Users/kainino/src/cts/src/common/runtime/cmdline.ts:78:5`,
+ },
+ {
+ case: 'node_throw',
+ _expectedLines: 2,
+ _stack: `Error: hello
+ at RunCaseSpecific.g.test.t [as fn] (/Users/kainino/src/cts/src/unittests/test_group.spec.ts:51:11)
+ at RunCaseSpecific.run (/Users/kainino/src/cts/src/common/framework/test_group.ts:121:18)
+ at processTicksAndRejections (internal/process/task_queues.js:86:5)`,
+ },
+ {
+ case: 'firefox_fail',
+ _expectedLines: 3,
+ _stack: `fail@http://localhost:8080/out/common/framework/logger.js:104:30
+expect@http://localhost:8080/out/common/framework/default_fixture.js:59:16
+@http://localhost:8080/out/unittests/util.spec.js:35:5
+run@http://localhost:8080/out/common/framework/test_group.js:119:18`,
+ },
+ {
+ case: 'firefox_throw',
+ _expectedLines: 1,
+ _stack: `@http://localhost:8080/out/unittests/test_group.spec.js:48:11
+run@http://localhost:8080/out/common/framework/test_group.js:119:18`,
+ },
+ {
+ case: 'safari_fail',
+ _expectedLines: 3,
+ _stack: `fail@http://localhost:8080/out/common/framework/logger.js:104:39
+expect@http://localhost:8080/out/common/framework/default_fixture.js:59:20
+http://localhost:8080/out/unittests/util.spec.js:35:11
+http://localhost:8080/out/common/framework/test_group.js:119:20
+asyncFunctionResume@[native code]
+[native code]
+promiseReactionJob@[native code]`,
+ },
+ {
+ case: 'safari_throw',
+ _expectedLines: 1,
+ _stack: `http://localhost:8080/out/unittests/test_group.spec.js:48:20
+http://localhost:8080/out/common/framework/test_group.js:119:20
+asyncFunctionResume@[native code]
+[native code]
+promiseReactionJob@[native code]`,
+ },
+ {
+ case: 'chrome_fail',
+ _expectedLines: 4,
+ _stack: `Error
+ at CaseRecorder.fail (http://localhost:8080/out/common/framework/logger.js:104:30)
+ at DefaultFixture.expect (http://localhost:8080/out/common/framework/default_fixture.js:59:16)
+ at RunCaseSpecific.fn (http://localhost:8080/out/unittests/util.spec.js:35:5)
+ at RunCaseSpecific.run (http://localhost:8080/out/common/framework/test_group.js:119:18)
+ at async runCase (http://localhost:8080/out/common/runtime/standalone.js:37:17)
+ at async http://localhost:8080/out/common/runtime/standalone.js:102:7`,
+ },
+ {
+ case: 'chrome_throw',
+ _expectedLines: 6,
+ _stack: `Error: hello
+ at RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:48:11)
+ at RunCaseSpecific.run (http://localhost:8080/out/common/framework/test_group.js:119:18)"
+ at async Promise.all (index 0)
+ at async TestGroupTest.run (http://localhost:8080/out/unittests/test_group_test.js:6:5)
+ at async RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:53:15)
+ at async RunCaseSpecific.run (http://localhost:8080/out/common/framework/test_group.js:119:7)
+ at async runCase (http://localhost:8080/out/common/runtime/standalone.js:37:17)
+ at async http://localhost:8080/out/common/runtime/standalone.js:102:7`,
+ },
+ {
+ case: 'multiple_lines',
+ _expectedLines: 8,
+ _stack: `Error: hello
+ at RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:48:11)
+ at RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:48:11)
+ at RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:48:11)
+ at RunCaseSpecific.run (http://localhost:8080/out/common/framework/test_group.js:119:18)"
+ at async Promise.all (index 0)
+ at async TestGroupTest.run (http://localhost:8080/out/unittests/test_group_test.js:6:5)
+ at async RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:53:15)
+ at async RunCaseSpecific.run (http://localhost:8080/out/common/framework/test_group.js:119:7)
+ at async runCase (http://localhost:8080/out/common/runtime/standalone.js:37:17)
+ at async http://localhost:8080/out/common/runtime/standalone.js:102:7`,
+ },
+ ])
+ .fn(t => {
+ const ex = new Error();
+ ex.stack = t.params._stack;
+ t.expect(ex.stack === t.params._stack);
+ const stringified = extractImportantStackTrace(ex);
+ const parts = stringified.split('\n');
+
+ t.expect(parts.length === t.params._expectedLines);
+ const last = parts[parts.length - 1];
+ t.expect(last.indexOf('/unittests/') !== -1 || last.indexOf('\\unittests\\') !== -1);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/listing.ts b/dom/webgpu/tests/cts/checkout/src/unittests/listing.ts
new file mode 100644
index 0000000000..823639c692
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/listing.ts
@@ -0,0 +1,5 @@
+/* eslint-disable import/no-restricted-paths */
+import { TestSuiteListing } from '../common/internal/test_suite_listing.js';
+import { makeListing } from '../common/tools/crawl.js';
+
+export const listing: Promise<TestSuiteListing> = makeListing(__filename);
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/loaders_and_trees.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/loaders_and_trees.spec.ts
new file mode 100644
index 0000000000..a22c06e669
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/loaders_and_trees.spec.ts
@@ -0,0 +1,978 @@
+export const description = `
+Tests for queries/filtering, loading, and running.
+`;
+
+import { Fixture } from '../common/framework/fixture.js';
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { TestFileLoader, SpecFile } from '../common/internal/file_loader.js';
+import { Logger } from '../common/internal/logging/logger.js';
+import { Status } from '../common/internal/logging/result.js';
+import { parseQuery } from '../common/internal/query/parseQuery.js';
+import {
+ TestQuery,
+ TestQuerySingleCase,
+ TestQueryMultiCase,
+ TestQueryMultiTest,
+ TestQueryMultiFile,
+ TestQueryWithExpectation,
+} from '../common/internal/query/query.js';
+import { makeTestGroupForUnitTesting } from '../common/internal/test_group.js';
+import { TestSuiteListing, TestSuiteListingEntry } from '../common/internal/test_suite_listing.js';
+import { ExpandThroughLevel, TestTreeLeaf } from '../common/internal/tree.js';
+import { assert, objectEquals } from '../common/util/util.js';
+
+import { UnitTest } from './unit_test.js';
+
+const listingData: { [k: string]: TestSuiteListingEntry[] } = {
+ suite1: [
+ { file: [], readme: 'desc 1a' },
+ { file: ['foo'] },
+ { file: ['bar'], readme: 'desc 1h' },
+ { file: ['bar', 'biz'] },
+ { file: ['bar', 'buzz', 'buzz'] },
+ { file: ['baz'] },
+ { file: ['empty'], readme: 'desc 1z' }, // directory with no files
+ ],
+ suite2: [{ file: [], readme: 'desc 2a' }, { file: ['foof'] }],
+};
+
+const specsData: { [k: string]: SpecFile } = {
+ 'suite1/foo.spec.js': {
+ description: 'desc 1b',
+ g: (() => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('hello').fn(() => {});
+ g.test('bonjour').fn(() => {});
+ g.test('hola')
+ .desc('TODO TODO')
+ .fn(() => {});
+ return g;
+ })(),
+ },
+ 'suite1/bar/biz.spec.js': {
+ description: 'desc 1f TODO TODO',
+ g: makeTestGroupForUnitTesting(UnitTest), // file with no tests
+ },
+ 'suite1/bar/buzz/buzz.spec.js': {
+ description: 'desc 1d TODO',
+ g: (() => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('zap').fn(() => {});
+ return g;
+ })(),
+ },
+ 'suite1/baz.spec.js': {
+ description: 'desc 1e',
+ g: (() => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('wye')
+ .paramsSimple([{}, { x: 1 }])
+ .fn(() => {});
+ g.test('zed')
+ .paramsSimple([
+ { a: 1, b: 2, _c: 0 },
+ { b: 3, a: 1, _c: 0 },
+ ])
+ .fn(() => {});
+ g.test('batched')
+ // creates two cases: one for subcases 1,2 and one for subcase 3
+ .paramsSubcasesOnly(u => u.combine('x', [1, 2, 3]))
+ .batch(2)
+ .fn(() => {});
+ return g;
+ })(),
+ },
+ 'suite2/foof.spec.js': {
+ description: 'desc 2b',
+ g: (() => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('blah').fn(t => {
+ t.debug('OK');
+ });
+ g.test('bleh')
+ .paramsSimple([{ a: 1 }])
+ .fn(t => {
+ t.debug('OK');
+ t.debug('OK');
+ });
+ g.test('bluh,a').fn(t => {
+ t.fail('goodbye');
+ });
+ return g;
+ })(),
+ },
+};
+
+class FakeTestFileLoader extends TestFileLoader {
+ listing(suite: string): Promise<TestSuiteListing> {
+ return Promise.resolve(listingData[suite]);
+ }
+
+ import(path: string): Promise<SpecFile> {
+ assert(path in specsData, '[test] mock file ' + path + ' does not exist');
+ return Promise.resolve(specsData[path]);
+ }
+}
+
+class LoadingTest extends UnitTest {
+ loader: FakeTestFileLoader = new FakeTestFileLoader();
+ events: (string | null)[] = [];
+ private isListenersAdded = false;
+
+ collectEvents(): void {
+ this.events = [];
+ if (!this.isListenersAdded) {
+ this.isListenersAdded = true;
+ this.loader.addEventListener('import', ev => this.events.push(ev.data.url));
+ this.loader.addEventListener('finish', _ev => this.events.push(null));
+ }
+ }
+
+ async load(query: string): Promise<TestTreeLeaf[]> {
+ return Array.from(await this.loader.loadCases(parseQuery(query)));
+ }
+
+ async loadNames(query: string): Promise<string[]> {
+ return (await this.load(query)).map(c => c.query.toString());
+ }
+}
+
+export const g = makeTestGroup(LoadingTest);
+
+g.test('suite').fn(t => {
+ t.shouldReject('Error', t.load('suite1'));
+ t.shouldReject('Error', t.load('suite1:'));
+});
+
+g.test('group').fn(async t => {
+ t.collectEvents();
+ t.expect((await t.load('suite1:*')).length === 10);
+ t.expect(
+ objectEquals(t.events, [
+ 'suite1/foo.spec.js',
+ 'suite1/bar/biz.spec.js',
+ 'suite1/bar/buzz/buzz.spec.js',
+ 'suite1/baz.spec.js',
+ null,
+ ])
+ );
+
+ t.collectEvents();
+ t.expect((await t.load('suite1:foo,*')).length === 3); // x:foo,* matches x:foo:
+ t.expect(objectEquals(t.events, ['suite1/foo.spec.js', null]));
+
+ t.collectEvents();
+ t.expect((await t.load('suite1:bar,*')).length === 1);
+ t.expect(
+ objectEquals(t.events, ['suite1/bar/biz.spec.js', 'suite1/bar/buzz/buzz.spec.js', null])
+ );
+
+ t.collectEvents();
+ t.expect((await t.load('suite1:bar,buzz,buzz,*')).length === 1);
+ t.expect(objectEquals(t.events, ['suite1/bar/buzz/buzz.spec.js', null]));
+
+ t.shouldReject('Error', t.load('suite1:f*'));
+
+ {
+ const s = new TestQueryMultiFile('suite1', ['bar', 'buzz']).toString();
+ t.collectEvents();
+ t.expect((await t.load(s)).length === 1);
+ t.expect(objectEquals(t.events, ['suite1/bar/buzz/buzz.spec.js', null]));
+ }
+});
+
+g.test('test').fn(async t => {
+ t.shouldReject('Error', t.load('suite1::'));
+ t.shouldReject('Error', t.load('suite1:bar:'));
+ t.shouldReject('Error', t.load('suite1:bar,:'));
+
+ t.shouldReject('Error', t.load('suite1::*'));
+ t.shouldReject('Error', t.load('suite1:bar,:*'));
+ t.shouldReject('Error', t.load('suite1:bar:*'));
+
+ t.expect((await t.load('suite1:foo:*')).length === 3);
+ t.expect((await t.load('suite1:bar,buzz,buzz:*')).length === 1);
+ t.expect((await t.load('suite1:baz:*')).length === 6);
+
+ t.expect((await t.load('suite2:foof:bluh,*')).length === 1);
+ t.expect((await t.load('suite2:foof:bluh,a,*')).length === 1);
+
+ {
+ const s = new TestQueryMultiTest('suite2', ['foof'], ['bluh']).toString();
+ t.expect((await t.load(s)).length === 1);
+ }
+});
+
+g.test('case').fn(async t => {
+ t.shouldReject('Error', t.load('suite1:foo::'));
+ t.shouldReject('Error', t.load('suite1:bar:zed,:'));
+
+ t.shouldReject('Error', t.load('suite1:foo:h*'));
+
+ t.shouldReject('Error', t.load('suite1:foo::*'));
+ t.shouldReject('Error', t.load('suite1:baz::*'));
+ t.shouldReject('Error', t.load('suite1:baz:zed,:*'));
+
+ t.shouldReject('Error', t.load('suite1:baz:zed:'));
+ t.shouldReject('Error', t.load('suite1:baz:zed:a=1'));
+ t.shouldReject('Error', t.load('suite1:baz:zed:a=1;b=2*'));
+ t.shouldReject('Error', t.load('suite1:baz:zed:a=1;b=2;'));
+ t.shouldReject('SyntaxError', t.load('suite1:baz:zed:a=1;b=2,')); // tries to parse '2,' as JSON
+ t.shouldReject('Error', t.load('suite1:baz:zed:a=1,b=2')); // '=' not allowed in value '1,b=2'
+ t.shouldReject('Error', t.load('suite1:baz:zed:b=2*'));
+ t.shouldReject('Error', t.load('suite1:baz:zed:b=2;a=1;_c=0'));
+ t.shouldReject('Error', t.load('suite1:baz:zed:a=1,*'));
+
+ t.expect((await t.load('suite1:baz:zed:*')).length === 2);
+ t.expect((await t.load('suite1:baz:zed:a=1;*')).length === 2);
+ t.expect((await t.load('suite1:baz:zed:a=1;b=2')).length === 1);
+ t.expect((await t.load('suite1:baz:zed:a=1;b=2;*')).length === 1);
+ t.expect((await t.load('suite1:baz:zed:b=2;*')).length === 1);
+ t.expect((await t.load('suite1:baz:zed:b=2;a=1')).length === 1);
+ t.expect((await t.load('suite1:baz:zed:b=2;a=1;*')).length === 1);
+ t.expect((await t.load('suite1:baz:zed:b=3;a=1')).length === 1);
+ t.expect((await t.load('suite1:baz:zed:a=1;b=3')).length === 1);
+ t.expect((await t.load('suite1:foo:hello:')).length === 1);
+
+ {
+ const s = new TestQueryMultiCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }).toString();
+ t.expect((await t.load(s)).length === 1);
+ }
+ {
+ const s = new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }).toString();
+ t.expect((await t.load(s)).length === 1);
+ }
+});
+
+g.test('batching').fn(async t => {
+ t.expect((await t.load('suite1:baz:batched,*')).length === 2);
+ t.expect((await t.load('suite1:baz:batched:*')).length === 2);
+ t.expect((await t.load('suite1:baz:batched:batch__=1;*')).length === 1);
+ t.expect((await t.load('suite1:baz:batched:batch__=1')).length === 1);
+});
+
+async function runTestcase(
+ t: Fixture,
+ log: Logger,
+ testcases: TestTreeLeaf[],
+ i: number,
+ query: TestQuery,
+ expectations: TestQueryWithExpectation[],
+ status: Status,
+ logs: (s: string[]) => boolean
+) {
+ t.expect(objectEquals(testcases[i].query, query));
+ const name = testcases[i].query.toString();
+ const [rec, res] = log.record(name);
+ await testcases[i].run(rec, expectations);
+
+ t.expect(log.results.get(name) === res);
+ t.expect(res.status === status);
+ t.expect(res.timems >= 0);
+ assert(res.logs !== undefined); // only undefined while pending
+ t.expect(logs(res.logs.map(l => JSON.stringify(l))));
+}
+
+g.test('end2end').fn(async t => {
+ const l = await t.load('suite2:foof:*');
+ assert(l.length === 3, 'listing length');
+
+ const log = new Logger({ overrideDebugMode: true });
+
+ await runTestcase(
+ t,
+ log,
+ l,
+ 0,
+ new TestQuerySingleCase('suite2', ['foof'], ['blah'], {}),
+ [],
+ 'pass',
+ logs => objectEquals(logs, ['"DEBUG: OK"'])
+ );
+ await runTestcase(
+ t,
+ log,
+ l,
+ 1,
+ new TestQuerySingleCase('suite2', ['foof'], ['bleh'], { a: 1 }),
+ [],
+ 'pass',
+ logs => objectEquals(logs, ['"DEBUG: OK"', '"DEBUG: OK"'])
+ );
+ await runTestcase(
+ t,
+ log,
+ l,
+ 2,
+ new TestQuerySingleCase('suite2', ['foof'], ['bluh', 'a'], {}),
+ [],
+ 'fail',
+ logs =>
+ logs.length === 1 &&
+ logs[0].startsWith('"EXPECTATION FAILED: goodbye\\n') &&
+ logs[0].indexOf('loaders_and_trees.spec.') !== -1
+ );
+});
+
+g.test('expectations,single_case').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const zedCases = await t.load('suite1:baz:zed:*');
+
+ // Single-case. Covers one case.
+ const zedExpectationsSkipA1B2 = [
+ {
+ query: new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 0,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }),
+ zedExpectationsSkipA1B2,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 1,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 3 }),
+ zedExpectationsSkipA1B2,
+ 'pass',
+ logs => logs.length === 0
+ );
+});
+
+g.test('expectations,single_case,none').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const zedCases = await t.load('suite1:baz:zed:*');
+ // Single-case. Doesn't cover any cases.
+ const zedExpectationsSkipA1B0 = [
+ {
+ query: new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 0 }),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 0,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }),
+ zedExpectationsSkipA1B0,
+ 'pass',
+ logs => logs.length === 0
+ );
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 1,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 3 }),
+ zedExpectationsSkipA1B0,
+ 'pass',
+ logs => logs.length === 0
+ );
+});
+
+g.test('expectations,multi_case').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const zedCases = await t.load('suite1:baz:zed:*');
+ // Multi-case, not all cases covered.
+ const zedExpectationsSkipB3 = [
+ {
+ query: new TestQueryMultiCase('suite1', ['baz'], ['zed'], { b: 3 }),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 0,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }),
+ zedExpectationsSkipB3,
+ 'pass',
+ logs => logs.length === 0
+ );
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 1,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 3 }),
+ zedExpectationsSkipB3,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+});
+
+g.test('expectations,multi_case_all').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const zedCases = await t.load('suite1:baz:zed:*');
+ // Multi-case, all cases covered.
+ const zedExpectationsSkipA1 = [
+ {
+ query: new TestQueryMultiCase('suite1', ['baz'], ['zed'], { a: 1 }),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 0,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }),
+ zedExpectationsSkipA1,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 1,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 3 }),
+ zedExpectationsSkipA1,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+});
+
+g.test('expectations,multi_case_none').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const zedCases = await t.load('suite1:baz:zed:*');
+ // Multi-case, no params, all cases covered.
+ const zedExpectationsSkipZed = [
+ {
+ query: new TestQueryMultiCase('suite1', ['baz'], ['zed'], {}),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 0,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }),
+ zedExpectationsSkipZed,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+
+ await runTestcase(
+ t,
+ log,
+ zedCases,
+ 1,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 3 }),
+ zedExpectationsSkipZed,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+});
+
+g.test('expectations,multi_test').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const suite1Cases = await t.load('suite1:*');
+
+ // Multi-test, all cases covered.
+ const expectationsSkipAllInBaz = [
+ {
+ query: new TestQueryMultiTest('suite1', ['baz'], []),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ suite1Cases,
+ 4,
+ new TestQuerySingleCase('suite1', ['baz'], ['wye'], {}),
+ expectationsSkipAllInBaz,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+
+ await runTestcase(
+ t,
+ log,
+ suite1Cases,
+ 6,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }),
+ expectationsSkipAllInBaz,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+});
+
+g.test('expectations,multi_test,none').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const suite1Cases = await t.load('suite1:*');
+
+ // Multi-test, no cases covered.
+ const expectationsSkipAllInFoo = [
+ {
+ query: new TestQueryMultiTest('suite1', ['foo'], []),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ suite1Cases,
+ 4,
+ new TestQuerySingleCase('suite1', ['baz'], ['wye'], {}),
+ expectationsSkipAllInFoo,
+ 'pass',
+ logs => logs.length === 0
+ );
+
+ await runTestcase(
+ t,
+ log,
+ suite1Cases,
+ 6,
+ new TestQuerySingleCase('suite1', ['baz'], ['zed'], { a: 1, b: 2 }),
+ expectationsSkipAllInFoo,
+ 'pass',
+ logs => logs.length === 0
+ );
+});
+
+g.test('expectations,multi_file').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const suite1Cases = await t.load('suite1:*');
+
+ // Multi-file
+ const expectationsSkipAll = [
+ {
+ query: new TestQueryMultiFile('suite1', []),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ suite1Cases,
+ 0,
+ new TestQuerySingleCase('suite1', ['foo'], ['hello'], {}),
+ expectationsSkipAll,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+
+ await runTestcase(
+ t,
+ log,
+ suite1Cases,
+ 3,
+ new TestQuerySingleCase('suite1', ['bar', 'buzz', 'buzz'], ['zap'], {}),
+ expectationsSkipAll,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+});
+
+g.test('expectations,catches_failure').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const suite2Cases = await t.load('suite2:*');
+
+ // Catches failure
+ const expectedFailures = [
+ {
+ query: new TestQueryMultiCase('suite2', ['foof'], ['bluh', 'a'], {}),
+ expectation: 'fail' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ suite2Cases,
+ 0,
+ new TestQuerySingleCase('suite2', ['foof'], ['blah'], {}),
+ expectedFailures,
+ 'pass',
+ logs => objectEquals(logs, ['"DEBUG: OK"'])
+ );
+
+ // Status is passed, but failure is logged.
+ await runTestcase(
+ t,
+ log,
+ suite2Cases,
+ 2,
+ new TestQuerySingleCase('suite2', ['foof'], ['bluh', 'a'], {}),
+ expectedFailures,
+ 'pass',
+ logs => logs.length === 1 && logs[0].startsWith('"EXPECTATION FAILED: goodbye\\n')
+ );
+});
+
+g.test('expectations,skip_dominates_failure').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const suite2Cases = await t.load('suite2:*');
+
+ const expectedFailures = [
+ {
+ query: new TestQueryMultiCase('suite2', ['foof'], ['bluh', 'a'], {}),
+ expectation: 'fail' as const,
+ },
+ {
+ query: new TestQueryMultiCase('suite2', ['foof'], ['bluh', 'a'], {}),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ suite2Cases,
+ 2,
+ new TestQuerySingleCase('suite2', ['foof'], ['bluh', 'a'], {}),
+ expectedFailures,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+});
+
+g.test('expectations,skip_inside_failure').fn(async t => {
+ const log = new Logger({ overrideDebugMode: true });
+ const suite2Cases = await t.load('suite2:*');
+
+ const expectedFailures = [
+ {
+ query: new TestQueryMultiFile('suite2', []),
+ expectation: 'fail' as const,
+ },
+ {
+ query: new TestQueryMultiCase('suite2', ['foof'], ['blah'], {}),
+ expectation: 'skip' as const,
+ },
+ ];
+
+ await runTestcase(
+ t,
+ log,
+ suite2Cases,
+ 0,
+ new TestQuerySingleCase('suite2', ['foof'], ['blah'], {}),
+ expectedFailures,
+ 'skip',
+ logs => logs.length === 1 && logs[0].startsWith('"SKIP: Skipped by expectations"')
+ );
+
+ await runTestcase(
+ t,
+ log,
+ suite2Cases,
+ 2,
+ new TestQuerySingleCase('suite2', ['foof'], ['bluh', 'a'], {}),
+ expectedFailures,
+ 'pass',
+ logs => logs.length === 1 && logs[0].startsWith('"EXPECTATION FAILED: goodbye\\n')
+ );
+});
+
+async function testIterateCollapsed(
+ t: LoadingTest,
+ alwaysExpandThroughLevel: ExpandThroughLevel,
+ expectations: string[],
+ expectedResult: 'throws' | string[] | [string, number | undefined][],
+ includeEmptySubtrees = false
+) {
+ t.debug(`expandThrough=${alwaysExpandThroughLevel} expectations=${expectations}`);
+ const treePromise = t.loader.loadTree(new TestQueryMultiFile('suite1', []), {
+ subqueriesToExpand: expectations,
+ });
+ if (expectedResult === 'throws') {
+ t.shouldReject('Error', treePromise, {
+ // Some errors here use StacklessError to print nicer command line outputs.
+ allowMissingStack: true,
+ });
+ return;
+ }
+ const tree = await treePromise;
+ const actualIter = tree.iterateCollapsedNodes({
+ includeEmptySubtrees,
+ alwaysExpandThroughLevel,
+ });
+ const testingTODOs = expectedResult.length > 0 && expectedResult[0] instanceof Array;
+ const actual = Array.from(actualIter, ({ query, subtreeCounts }) =>
+ testingTODOs ? [query.toString(), subtreeCounts?.nodesWithTODO] : query.toString()
+ );
+ if (!objectEquals(actual, expectedResult)) {
+ t.fail(
+ `iterateCollapsed failed:
+ got ${JSON.stringify(actual)}
+ exp ${JSON.stringify(expectedResult)}
+${tree.toString()}`
+ );
+ }
+}
+
+g.test('print').fn(async t => {
+ const tree = await t.loader.loadTree(new TestQueryMultiFile('suite1', []));
+ tree.toString();
+});
+
+g.test('iterateCollapsed').fn(async t => {
+ await testIterateCollapsed(
+ t,
+ 1,
+ [],
+ [
+ ['suite1:foo:*', 1], // to-do propagated up from foo:hola
+ ['suite1:bar,buzz,buzz:*', 1], // to-do in file description
+ ['suite1:baz:*', 0],
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 2,
+ [],
+ [
+ ['suite1:foo:hello:*', 0],
+ ['suite1:foo:bonjour:*', 0],
+ ['suite1:foo:hola:*', 1], // to-do in test description
+ ['suite1:bar,buzz,buzz:zap:*', 0],
+ ['suite1:baz:wye:*', 0],
+ ['suite1:baz:zed:*', 0],
+ ['suite1:baz:batched:*', 0],
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 3,
+ [],
+ [
+ ['suite1:foo:hello:', undefined],
+ ['suite1:foo:bonjour:', undefined],
+ ['suite1:foo:hola:', undefined],
+ ['suite1:bar,buzz,buzz:zap:', undefined],
+ ['suite1:baz:wye:', undefined],
+ ['suite1:baz:wye:x=1', undefined],
+ ['suite1:baz:zed:a=1;b=2', undefined],
+ ['suite1:baz:zed:b=3;a=1', undefined],
+ ['suite1:baz:batched:batch__=0', undefined],
+ ['suite1:baz:batched:batch__=1', undefined],
+ ]
+ );
+
+ // Expectations lists that have no effect
+ await testIterateCollapsed(
+ t,
+ 1,
+ ['suite1:foo:*'],
+ ['suite1:foo:*', 'suite1:bar,buzz,buzz:*', 'suite1:baz:*']
+ );
+ await testIterateCollapsed(
+ t,
+ 1,
+ ['suite1:bar,buzz,buzz:*'],
+ ['suite1:foo:*', 'suite1:bar,buzz,buzz:*', 'suite1:baz:*']
+ );
+ await testIterateCollapsed(
+ t,
+ 2,
+ ['suite1:baz:wye:*'],
+ [
+ 'suite1:foo:hello:*',
+ 'suite1:foo:bonjour:*',
+ 'suite1:foo:hola:*',
+ 'suite1:bar,buzz,buzz:zap:*',
+ 'suite1:baz:wye:*',
+ 'suite1:baz:zed:*',
+ 'suite1:baz:batched:*',
+ ]
+ );
+ // Test with includeEmptySubtrees=true
+ await testIterateCollapsed(
+ t,
+ 1,
+ [],
+ [
+ 'suite1:foo:*',
+ 'suite1:bar,biz:*',
+ 'suite1:bar,buzz,buzz:*',
+ 'suite1:baz:*',
+ 'suite1:empty,*',
+ ],
+ true
+ );
+ await testIterateCollapsed(
+ t,
+ 2,
+ [],
+ [
+ 'suite1:foo:hello:*',
+ 'suite1:foo:bonjour:*',
+ 'suite1:foo:hola:*',
+ 'suite1:bar,biz:*',
+ 'suite1:bar,buzz,buzz:zap:*',
+ 'suite1:baz:wye:*',
+ 'suite1:baz:zed:*',
+ 'suite1:baz:batched:*',
+ 'suite1:empty,*',
+ ],
+ true
+ );
+
+ // Expectations lists that have some effect
+ await testIterateCollapsed(
+ t,
+ 1,
+ ['suite1:baz:wye:*'],
+ [
+ 'suite1:foo:*',
+ 'suite1:bar,buzz,buzz:*',
+ 'suite1:baz:wye:*',
+ 'suite1:baz:zed,*',
+ 'suite1:baz:batched,*',
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 1,
+ ['suite1:baz:zed:*'],
+ [
+ 'suite1:foo:*',
+ 'suite1:bar,buzz,buzz:*',
+ 'suite1:baz:wye,*',
+ 'suite1:baz:zed:*',
+ 'suite1:baz:batched,*',
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 1,
+ ['suite1:baz:wye:*', 'suite1:baz:zed:*'],
+ [
+ 'suite1:foo:*',
+ 'suite1:bar,buzz,buzz:*',
+ 'suite1:baz:wye:*',
+ 'suite1:baz:zed:*',
+ 'suite1:baz:batched,*',
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 1,
+ ['suite1:baz:wye:'],
+ [
+ 'suite1:foo:*',
+ 'suite1:bar,buzz,buzz:*',
+ 'suite1:baz:wye:',
+ 'suite1:baz:wye:x=1;*',
+ 'suite1:baz:zed,*',
+ 'suite1:baz:batched,*',
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 1,
+ ['suite1:baz:wye:x=1'],
+ [
+ 'suite1:foo:*',
+ 'suite1:bar,buzz,buzz:*',
+ 'suite1:baz:wye:',
+ 'suite1:baz:wye:x=1',
+ 'suite1:baz:zed,*',
+ 'suite1:baz:batched,*',
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 1,
+ ['suite1:foo:*', 'suite1:baz:wye:'],
+ [
+ 'suite1:foo:*',
+ 'suite1:bar,buzz,buzz:*',
+ 'suite1:baz:wye:',
+ 'suite1:baz:wye:x=1;*',
+ 'suite1:baz:zed,*',
+ 'suite1:baz:batched,*',
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 2,
+ ['suite1:baz:wye:'],
+ [
+ 'suite1:foo:hello:*',
+ 'suite1:foo:bonjour:*',
+ 'suite1:foo:hola:*',
+ 'suite1:bar,buzz,buzz:zap:*',
+ 'suite1:baz:wye:',
+ 'suite1:baz:wye:x=1;*',
+ 'suite1:baz:zed:*',
+ 'suite1:baz:batched:*',
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 2,
+ ['suite1:baz:wye:x=1'],
+ [
+ 'suite1:foo:hello:*',
+ 'suite1:foo:bonjour:*',
+ 'suite1:foo:hola:*',
+ 'suite1:bar,buzz,buzz:zap:*',
+ 'suite1:baz:wye:',
+ 'suite1:baz:wye:x=1',
+ 'suite1:baz:zed:*',
+ 'suite1:baz:batched:*',
+ ]
+ );
+ await testIterateCollapsed(
+ t,
+ 2,
+ ['suite1:foo:hello:*', 'suite1:baz:wye:'],
+ [
+ 'suite1:foo:hello:*',
+ 'suite1:foo:bonjour:*',
+ 'suite1:foo:hola:*',
+ 'suite1:bar,buzz,buzz:zap:*',
+ 'suite1:baz:wye:',
+ 'suite1:baz:wye:x=1;*',
+ 'suite1:baz:zed:*',
+ 'suite1:baz:batched:*',
+ ]
+ );
+
+ // Invalid expectation queries
+ await testIterateCollapsed(t, 1, ['*'], 'throws');
+ await testIterateCollapsed(t, 1, ['garbage'], 'throws');
+ await testIterateCollapsed(t, 1, ['garbage*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite1*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite1:foo*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite1:foo:he*'], 'throws');
+
+ // Valid expectation queries but they don't match anything
+ await testIterateCollapsed(t, 1, ['garbage:*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite1:doesntexist:*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite2:foo:*'], 'throws');
+ // Can't expand subqueries bigger than one file.
+ await testIterateCollapsed(t, 1, ['suite1:*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite1:bar,*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite1:*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite1:bar:hello,*'], 'throws');
+ await testIterateCollapsed(t, 1, ['suite1:baz,*'], 'throws');
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/logger.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/logger.spec.ts
new file mode 100644
index 0000000000..abc27e2876
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/logger.spec.ts
@@ -0,0 +1,173 @@
+export const description = `
+Unit tests for namespaced logging system.
+
+Also serves as a larger test of async test functions, and of the logging system.
+`;
+
+import { SkipTestCase } from '../common/framework/fixture.js';
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { Logger } from '../common/internal/logging/logger.js';
+import { assert } from '../common/util/util.js';
+
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+g.test('construct').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [, res1] = mylog.record('one');
+ const [, res2] = mylog.record('two');
+
+ t.expect(mylog.results.get('one') === res1);
+ t.expect(mylog.results.get('two') === res2);
+ t.expect(res1.logs === undefined);
+ t.expect(res1.status === 'running');
+ t.expect(res1.timems < 0);
+ t.expect(res2.logs === undefined);
+ t.expect(res2.status === 'running');
+ t.expect(res2.timems < 0);
+});
+
+g.test('empty').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ t.expect(res.status === 'running');
+ rec.finish();
+
+ t.expect(res.status === 'notrun');
+ t.expect(res.timems >= 0);
+});
+
+g.test('passed').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ rec.passed();
+ rec.finish();
+
+ t.expect(res.status === 'pass');
+ t.expect(res.timems >= 0);
+});
+
+g.test('pass').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ rec.debug(new Error('hello'));
+ t.expect(res.status === 'running');
+ rec.finish();
+
+ t.expect(res.status === 'pass');
+ t.expect(res.timems >= 0);
+});
+
+g.test('skip').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ rec.skipped(new SkipTestCase());
+ rec.finish();
+
+ t.expect(res.status === 'skip');
+ t.expect(res.timems >= 0);
+});
+
+// Tests if there's some skips and at least one pass it's pass.
+g.test('skip_pass').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ rec.skipped(new SkipTestCase());
+ rec.debug(new Error('hello'));
+ rec.skipped(new SkipTestCase());
+ rec.finish();
+
+ t.expect(res.status === 'pass');
+ t.expect(res.timems >= 0);
+});
+
+g.test('warn').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ rec.warn(new Error('hello'));
+ rec.skipped(new SkipTestCase());
+ rec.finish();
+
+ t.expect(res.status === 'warn');
+ t.expect(res.timems >= 0);
+});
+
+g.test('fail,expectationFailed').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ rec.expectationFailed(new Error('bye'));
+ rec.warn(new Error());
+ rec.skipped(new SkipTestCase());
+ rec.finish();
+
+ t.expect(res.status === 'fail');
+ t.expect(res.timems >= 0);
+});
+
+g.test('fail,validationFailed').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ rec.validationFailed(new Error('bye'));
+ rec.warn(new Error());
+ rec.skipped(new SkipTestCase());
+ rec.finish();
+
+ t.expect(res.status === 'fail');
+ t.expect(res.timems >= 0);
+});
+
+g.test('fail,threw').fn(t => {
+ const mylog = new Logger({ overrideDebugMode: true });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ rec.threw(new Error('bye'));
+ rec.warn(new Error());
+ rec.skipped(new SkipTestCase());
+ rec.finish();
+
+ t.expect(res.status === 'fail');
+ t.expect(res.timems >= 0);
+});
+
+g.test('debug')
+ .paramsSimple([
+ { debug: true, _logsCount: 5 }, //
+ { debug: false, _logsCount: 3 },
+ ])
+ .fn(t => {
+ const { debug, _logsCount } = t.params;
+
+ const mylog = new Logger({ overrideDebugMode: debug });
+ const [rec, res] = mylog.record('one');
+
+ rec.start();
+ rec.debug(new Error('hello'));
+ rec.expectationFailed(new Error('bye'));
+ rec.warn(new Error());
+ rec.skipped(new SkipTestCase());
+ rec.debug(new Error('foo'));
+ rec.finish();
+
+ t.expect(res.status === 'fail');
+ t.expect(res.timems >= 0);
+ assert(res.logs !== undefined);
+ t.expect(res.logs.length === _logsCount);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/maths.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/maths.spec.ts
new file mode 100644
index 0000000000..357c574281
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/maths.spec.ts
@@ -0,0 +1,1924 @@
+export const description = `
+Util math unit tests.
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { objectEquals } from '../common/util/util.js';
+import { kBit, kValue } from '../webgpu/util/constants.js';
+import {
+ f16,
+ f32,
+ f64,
+ float16ToUint16,
+ float32ToUint32,
+ uint16ToFloat16,
+ uint32ToFloat32,
+} from '../webgpu/util/conversion.js';
+import {
+ biasedRange,
+ calculatePermutations,
+ cartesianProduct,
+ correctlyRoundedF16,
+ correctlyRoundedF32,
+ FlushMode,
+ frexp,
+ fullF16Range,
+ fullF32Range,
+ fullI32Range,
+ lerp,
+ linearRange,
+ nextAfterF16,
+ nextAfterF32,
+ nextAfterF64,
+ NextDirection,
+ oneULPF16,
+ oneULPF32,
+ oneULPF64,
+ lerpBigInt,
+ linearRangeBigInt,
+} from '../webgpu/util/math.js';
+import {
+ reinterpretU16AsF16,
+ reinterpretU32AsF32,
+ reinterpretU64AsF64,
+} from '../webgpu/util/reinterpret.js';
+
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+/**
+ * Utility wrapper around oneULP to test if a value is within 1 ULP(x)
+ *
+ * @param got number to test
+ * @param expected number to be within 1 ULP of
+ * @param mode should oneULP FTZ
+ * @returns if got is within 1 ULP of expected
+ */
+function withinOneULPF32(got: number, expected: number, mode: FlushMode): boolean {
+ const ulp = oneULPF32(expected, mode);
+ return got >= expected - ulp && got <= expected + ulp;
+}
+
+/**
+ * @returns true if arrays are equal within 1ULP, doing element-wise comparison
+ * as needed, and considering NaNs to be equal.
+ *
+ * Depends on the correctness of oneULP, which is tested in this file.
+ **
+ * @param got array of numbers to compare for equality
+ * @param expect array of numbers to compare against
+ * @param mode should different subnormals be considered the same, i.e. should
+ * FTZ occur during comparison
+ **/
+function compareArrayOfNumbersF32(
+ got: readonly number[],
+ expect: readonly number[],
+ mode: FlushMode = 'flush'
+): boolean {
+ return (
+ got.length === expect.length &&
+ got.every((value, index) => {
+ const expected = expect[index];
+ return (
+ (Number.isNaN(value) && Number.isNaN(expected)) || withinOneULPF32(value, expected, mode)
+ );
+ })
+ );
+}
+
+/** @returns the hex value representation of a f64, from is numeric representation */
+function float64ToUint64(value: number): bigint {
+ return new BigUint64Array(new Float64Array([value]).buffer)[0];
+}
+
+/** @returns the numeric representation of a f64, from its hex value representation */
+function uint64ToFloat64(bits: bigint): number {
+ return new Float64Array(new BigUint64Array([bits]).buffer)[0];
+}
+
+interface nextAfterCase {
+ val: number;
+ dir: NextDirection;
+ result: number;
+}
+
+g.test('nextAfterF64FlushToZero')
+ .paramsSubcasesOnly<nextAfterCase>(
+ // prettier-ignore
+ [
+ // Edge Cases
+ { val: Number.NaN, dir: 'positive', result: Number.NaN },
+ { val: Number.NaN, dir: 'negative', result: Number.NaN },
+ { val: Number.POSITIVE_INFINITY, dir: 'positive', result: kValue.f64.positive.infinity },
+ { val: Number.POSITIVE_INFINITY, dir: 'negative', result: kValue.f64.positive.infinity },
+ { val: Number.NEGATIVE_INFINITY, dir: 'positive', result: kValue.f64.negative.infinity },
+ { val: Number.NEGATIVE_INFINITY, dir: 'negative', result: kValue.f64.negative.infinity },
+
+ // Zeroes
+ { val: +0, dir: 'positive', result: kValue.f64.positive.min },
+ { val: +0, dir: 'negative', result: kValue.f64.negative.max },
+ { val: -0, dir: 'positive', result: kValue.f64.positive.min },
+ { val: -0, dir: 'negative', result: kValue.f64.negative.max },
+
+ // Subnormals
+ { val: kValue.f64.positive.subnormal.min, dir: 'positive', result: kValue.f64.positive.min },
+ { val: kValue.f64.positive.subnormal.min, dir: 'negative', result: kValue.f64.negative.max },
+ { val: kValue.f64.positive.subnormal.max, dir: 'positive', result: kValue.f64.positive.min },
+ { val: kValue.f64.positive.subnormal.max, dir: 'negative', result: kValue.f64.negative.max },
+ { val: kValue.f64.negative.subnormal.min, dir: 'positive', result: kValue.f64.positive.min },
+ { val: kValue.f64.negative.subnormal.min, dir: 'negative', result: kValue.f64.negative.max },
+ { val: kValue.f64.negative.subnormal.max, dir: 'positive', result: kValue.f64.positive.min },
+ { val: kValue.f64.negative.subnormal.max, dir: 'negative', result: kValue.f64.negative.max },
+
+ // Normals
+ { val: kValue.f64.positive.max, dir: 'positive', result: kValue.f64.positive.infinity },
+ { val: kValue.f64.positive.max, dir: 'negative', result: kValue.f64.positive.nearest_max },
+ { val: kValue.f64.positive.min, dir: 'positive', result: reinterpretU64AsF64(0x0010_0000_0000_0001n ) },
+ { val: kValue.f64.positive.min, dir: 'negative', result: 0 },
+ { val: kValue.f64.negative.max, dir: 'positive', result: 0 },
+ { val: kValue.f64.negative.max, dir: 'negative', result: reinterpretU64AsF64(0x8010_0000_0000_0001n) },
+ { val: kValue.f64.negative.min, dir: 'positive', result: kValue.f64.negative.nearest_min },
+ { val: kValue.f64.negative.min, dir: 'negative', result: kValue.f64.negative.infinity },
+ { val: reinterpretU64AsF64(0x0380_0000_0000_0000n), dir: 'positive', result: reinterpretU64AsF64(0x0380_0000_0000_0001n) },
+ { val: reinterpretU64AsF64(0x0380_0000_0000_0000n), dir: 'negative', result: reinterpretU64AsF64(0x037f_ffff_ffff_ffffn) },
+ { val: reinterpretU64AsF64(0x8380_0000_0000_0000n), dir: 'positive', result: reinterpretU64AsF64(0x837f_ffff_ffff_ffffn) },
+ { val: reinterpretU64AsF64(0x8380_0000_0000_0000n), dir: 'negative', result: reinterpretU64AsF64(0x8380_0000_0000_0001n) },
+ ]
+ )
+ .fn(t => {
+ const val = t.params.val;
+ const dir = t.params.dir;
+ const expect = t.params.result;
+ const got = nextAfterF64(val, dir, 'flush');
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `nextAfterF64(${f64(val)}, '${dir}', 'flush') returned ${f64(got)}. Expected ${f64(expect)}`
+ );
+ });
+
+g.test('nextAfterF64NoFlush')
+ .paramsSubcasesOnly<nextAfterCase>(
+ // prettier-ignore
+ [
+ // Edge Cases
+ { val: Number.NaN, dir: 'positive', result: Number.NaN },
+ { val: Number.NaN, dir: 'negative', result: Number.NaN },
+ { val: Number.POSITIVE_INFINITY, dir: 'positive', result: kValue.f64.positive.infinity },
+ { val: Number.POSITIVE_INFINITY, dir: 'negative', result: kValue.f64.positive.infinity },
+ { val: Number.NEGATIVE_INFINITY, dir: 'positive', result: kValue.f64.negative.infinity },
+ { val: Number.NEGATIVE_INFINITY, dir: 'negative', result: kValue.f64.negative.infinity },
+
+ // Zeroes
+ { val: +0, dir: 'positive', result: kValue.f64.positive.subnormal.min },
+ { val: +0, dir: 'negative', result: kValue.f64.negative.subnormal.max },
+ { val: -0, dir: 'positive', result: kValue.f64.positive.subnormal.min },
+ { val: -0, dir: 'negative', result: kValue.f64.negative.subnormal.max },
+
+ // Subnormals
+ { val: kValue.f64.positive.subnormal.min, dir: 'positive', result: reinterpretU64AsF64(0x0000_0000_0000_0002n) },
+ { val: kValue.f64.positive.subnormal.min, dir: 'negative', result: 0 },
+ { val: kValue.f64.positive.subnormal.max, dir: 'positive', result: kValue.f64.positive.min },
+ { val: kValue.f64.positive.subnormal.max, dir: 'negative', result: reinterpretU64AsF64(0x000f_ffff_ffff_fffen) },
+ { val: kValue.f64.negative.subnormal.min, dir: 'positive', result: reinterpretU64AsF64(0x800f_ffff_ffff_fffen) },
+ { val: kValue.f64.negative.subnormal.min, dir: 'negative', result: kValue.f64.negative.max },
+ { val: kValue.f64.negative.subnormal.max, dir: 'positive', result: 0 },
+ { val: kValue.f64.negative.subnormal.max, dir: 'negative', result: reinterpretU64AsF64(0x8000_0000_0000_0002n) },
+
+ // Normals
+ { val: kValue.f64.positive.max, dir: 'positive', result: kValue.f64.positive.infinity },
+ { val: kValue.f64.positive.max, dir: 'negative', result: kValue.f64.positive.nearest_max },
+ { val: kValue.f64.positive.min, dir: 'positive', result: reinterpretU64AsF64(0x0010_0000_0000_0001n ) },
+ { val: kValue.f64.positive.min, dir: 'negative', result: reinterpretU64AsF64(0x000f_ffff_ffff_ffffn) },
+ { val: kValue.f64.negative.max, dir: 'positive', result: reinterpretU64AsF64(0x800f_ffff_ffff_ffffn) },
+ { val: kValue.f64.negative.max, dir: 'negative', result: reinterpretU64AsF64(0x8010_0000_0000_0001n) },
+ { val: kValue.f64.negative.min, dir: 'positive', result: kValue.f64.negative.nearest_min },
+ { val: kValue.f64.negative.min, dir: 'negative', result: kValue.f64.negative.infinity },
+ { val: reinterpretU64AsF64(0x0380_0000_0000_0000n), dir: 'positive', result: reinterpretU64AsF64(0x0380_0000_0000_0001n) },
+ { val: reinterpretU64AsF64(0x0380_0000_0000_0000n), dir: 'negative', result: reinterpretU64AsF64(0x037f_ffff_ffff_ffffn) },
+ { val: reinterpretU64AsF64(0x8380_0000_0000_0000n), dir: 'positive', result: reinterpretU64AsF64(0x837f_ffff_ffff_ffffn) },
+ { val: reinterpretU64AsF64(0x8380_0000_0000_0000n), dir: 'negative', result: reinterpretU64AsF64(0x8380_0000_0000_0001n) },
+ ]
+ )
+ .fn(t => {
+ const val = t.params.val;
+ const dir = t.params.dir;
+ const expect = t.params.result;
+ const got = nextAfterF64(val, dir, 'no-flush');
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `nextAfterF64(${f64(val)}, '${dir}', 'no-flush') returned ${f64(got)}. Expected ${f64(
+ expect
+ )}`
+ );
+ });
+
+g.test('nextAfterF32FlushToZero')
+ .paramsSubcasesOnly<nextAfterCase>(
+ // prettier-ignore
+ [
+ // Edge Cases
+ { val: Number.NaN, dir: 'positive', result: Number.NaN },
+ { val: Number.NaN, dir: 'negative', result: Number.NaN },
+ { val: Number.POSITIVE_INFINITY, dir: 'positive', result: kValue.f32.positive.infinity },
+ { val: Number.POSITIVE_INFINITY, dir: 'negative', result: kValue.f32.positive.infinity },
+ { val: Number.NEGATIVE_INFINITY, dir: 'positive', result: kValue.f32.negative.infinity },
+ { val: Number.NEGATIVE_INFINITY, dir: 'negative', result: kValue.f32.negative.infinity },
+
+ // Zeroes
+ { val: +0, dir: 'positive', result: kValue.f32.positive.min },
+ { val: +0, dir: 'negative', result: kValue.f32.negative.max },
+ { val: -0, dir: 'positive', result: kValue.f32.positive.min },
+ { val: -0, dir: 'negative', result: kValue.f32.negative.max },
+
+ // Subnormals
+ { val: kValue.f32.positive.subnormal.min, dir: 'positive', result: kValue.f32.positive.min },
+ { val: kValue.f32.positive.subnormal.min, dir: 'negative', result: kValue.f32.negative.max },
+ { val: kValue.f32.positive.subnormal.max, dir: 'positive', result: kValue.f32.positive.min },
+ { val: kValue.f32.positive.subnormal.max, dir: 'negative', result: kValue.f32.negative.max },
+ { val: kValue.f32.negative.subnormal.min, dir: 'positive', result: kValue.f32.positive.min },
+ { val: kValue.f32.negative.subnormal.min, dir: 'negative', result: kValue.f32.negative.max },
+ { val: kValue.f32.negative.subnormal.max, dir: 'positive', result: kValue.f32.positive.min },
+ { val: kValue.f32.negative.subnormal.max, dir: 'negative', result: kValue.f32.negative.max },
+
+ // Normals
+ { val: kValue.f32.positive.max, dir: 'positive', result: kValue.f32.positive.infinity },
+ { val: kValue.f32.positive.max, dir: 'negative', result: kValue.f32.positive.nearest_max },
+ { val: kValue.f32.positive.min, dir: 'positive', result: reinterpretU32AsF32(0x00800001) },
+ { val: kValue.f32.positive.min, dir: 'negative', result: 0 },
+ { val: kValue.f32.negative.max, dir: 'positive', result: 0 },
+ { val: kValue.f32.negative.max, dir: 'negative', result: reinterpretU32AsF32(0x80800001) },
+ { val: kValue.f32.negative.min, dir: 'positive', result: reinterpretU32AsF32(0xff7ffffe) },
+ { val: kValue.f32.negative.min, dir: 'negative', result: kValue.f32.negative.infinity },
+ { val: reinterpretU32AsF32(0x03800000), dir: 'positive', result: reinterpretU32AsF32(0x03800001) },
+ { val: reinterpretU32AsF32(0x03800000), dir: 'negative', result: reinterpretU32AsF32(0x037fffff) },
+ { val: reinterpretU32AsF32(0x83800000), dir: 'positive', result: reinterpretU32AsF32(0x837fffff) },
+ { val: reinterpretU32AsF32(0x83800000), dir: 'negative', result: reinterpretU32AsF32(0x83800001) },
+
+ // Not precisely expressible as f32
+ { val: 0.001, dir: 'positive', result: reinterpretU32AsF32(0x3a83126f) }, // positive normal
+ { val: 0.001, dir: 'negative', result: reinterpretU32AsF32(0x3a83126e) }, // positive normal
+ { val: -0.001, dir: 'positive', result: reinterpretU32AsF32(0xba83126e) }, // negative normal
+ { val: -0.001, dir: 'negative', result: reinterpretU32AsF32(0xba83126f) }, // negative normal
+ { val: 2.82E-40, dir: 'positive', result: kValue.f32.positive.min }, // positive subnormal
+ { val: 2.82E-40, dir: 'negative', result: kValue.f32.negative.max }, // positive subnormal
+ { val: -2.82E-40, dir: 'positive', result: kValue.f32.positive.min }, // negative subnormal
+ { val: -2.82E-40, dir: 'negative', result: kValue.f32.negative.max }, // negative subnormal
+ ]
+ )
+ .fn(t => {
+ const val = t.params.val;
+ const dir = t.params.dir;
+ const expect = t.params.result;
+ const got = nextAfterF32(val, dir, 'flush');
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `nextAfterF32(${f64(val)}, '${dir}', 'flush') returned ${f32(got)}. Expected ${f32(expect)}`
+ );
+ });
+
+g.test('nextAfterF32NoFlush')
+ .paramsSubcasesOnly<nextAfterCase>(
+ // prettier-ignore
+ [
+ // Edge Cases
+ { val: Number.NaN, dir: 'positive', result: Number.NaN },
+ { val: Number.NaN, dir: 'negative', result: Number.NaN },
+ { val: Number.POSITIVE_INFINITY, dir: 'positive', result: kValue.f32.positive.infinity },
+ { val: Number.POSITIVE_INFINITY, dir: 'negative', result: kValue.f32.positive.infinity },
+ { val: Number.NEGATIVE_INFINITY, dir: 'positive', result: kValue.f32.negative.infinity },
+ { val: Number.NEGATIVE_INFINITY, dir: 'negative', result: kValue.f32.negative.infinity },
+
+ // Zeroes
+ { val: +0, dir: 'positive', result: kValue.f32.positive.subnormal.min },
+ { val: +0, dir: 'negative', result: kValue.f32.negative.subnormal.max },
+ { val: -0, dir: 'positive', result: kValue.f32.positive.subnormal.min },
+ { val: -0, dir: 'negative', result: kValue.f32.negative.subnormal.max },
+
+ // Subnormals
+ { val:kValue.f32.positive.subnormal.min, dir: 'positive', result: reinterpretU32AsF32(0x00000002) },
+ { val:kValue.f32.positive.subnormal.min, dir: 'negative', result: 0 },
+ { val:kValue.f32.positive.subnormal.max, dir: 'positive', result: kValue.f32.positive.min },
+ { val:kValue.f32.positive.subnormal.max, dir: 'negative', result: reinterpretU32AsF32(0x007ffffe) },
+ { val:kValue.f32.negative.subnormal.min, dir: 'positive', result: reinterpretU32AsF32(0x807ffffe) },
+ { val:kValue.f32.negative.subnormal.min, dir: 'negative', result: kValue.f32.negative.max },
+ { val:kValue.f32.negative.subnormal.max, dir: 'positive', result: 0 },
+ { val:kValue.f32.negative.subnormal.max, dir: 'negative', result: reinterpretU32AsF32(0x80000002) },
+
+ // Normals
+ { val: kValue.f32.positive.max, dir: 'positive', result: kValue.f32.positive.infinity },
+ { val: kValue.f32.positive.max, dir: 'negative', result: kValue.f32.positive.nearest_max },
+ { val: kValue.f32.positive.min, dir: 'positive', result: reinterpretU32AsF32(0x00800001) },
+ { val: kValue.f32.positive.min, dir: 'negative', result: kValue.f32.positive.subnormal.max },
+ { val: kValue.f32.negative.max, dir: 'positive', result: kValue.f32.negative.subnormal.min },
+ { val: kValue.f32.negative.max, dir: 'negative', result: reinterpretU32AsF32(0x80800001) },
+ { val: kValue.f32.negative.min, dir: 'positive', result: kValue.f32.negative.nearest_min },
+ { val: kValue.f32.negative.min, dir: 'negative', result: kValue.f32.negative.infinity },
+ { val: reinterpretU32AsF32(0x03800000), dir: 'positive', result: reinterpretU32AsF32(0x03800001) },
+ { val: reinterpretU32AsF32(0x03800000), dir: 'negative', result: reinterpretU32AsF32(0x037fffff) },
+ { val: reinterpretU32AsF32(0x83800000), dir: 'positive', result: reinterpretU32AsF32(0x837fffff) },
+ { val: reinterpretU32AsF32(0x83800000), dir: 'negative', result: reinterpretU32AsF32(0x83800001) },
+
+ // Not precisely expressible as f32
+ { val: 0.001, dir: 'positive', result: reinterpretU32AsF32(0x3a83126f) }, // positive normal
+ { val: 0.001, dir: 'negative', result: reinterpretU32AsF32(0x3a83126e) }, // positive normal
+ { val: -0.001, dir: 'positive', result: reinterpretU32AsF32(0xba83126e) }, // negative normal
+ { val: -0.001, dir: 'negative', result: reinterpretU32AsF32(0xba83126f) }, // negative normal
+ { val: 2.82E-40, dir: 'positive', result: reinterpretU32AsF32(0x0003121a) }, // positive subnormal
+ { val: 2.82E-40, dir: 'negative', result: reinterpretU32AsF32(0x00031219) }, // positive subnormal
+ { val: -2.82E-40, dir: 'positive', result: reinterpretU32AsF32(0x80031219) }, // negative subnormal
+ { val: -2.82E-40, dir: 'negative', result: reinterpretU32AsF32(0x8003121a) }, // negative subnormal
+ ]
+ )
+ .fn(t => {
+ const val = t.params.val;
+ const dir = t.params.dir;
+ const expect = t.params.result;
+ const got = nextAfterF32(val, dir, 'no-flush');
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `nextAfterF32(${f64(val)}, '${dir}', 'no-flush') returned ${f32(got)}. Expected ${f32(
+ expect
+ )}`
+ );
+ });
+
+g.test('nextAfterF16FlushToZero')
+ .paramsSubcasesOnly<nextAfterCase>(
+ // prettier-ignore
+ [
+ // Edge Cases
+ { val: Number.NaN, dir: 'positive', result: Number.NaN },
+ { val: Number.NaN, dir: 'negative', result: Number.NaN },
+ { val: Number.POSITIVE_INFINITY, dir: 'positive', result: kValue.f16.positive.infinity },
+ { val: Number.POSITIVE_INFINITY, dir: 'negative', result: kValue.f16.positive.infinity },
+ { val: Number.NEGATIVE_INFINITY, dir: 'positive', result: kValue.f16.negative.infinity },
+ { val: Number.NEGATIVE_INFINITY, dir: 'negative', result: kValue.f16.negative.infinity },
+
+ // Zeroes
+ { val: +0, dir: 'positive', result: kValue.f16.positive.min },
+ { val: +0, dir: 'negative', result: kValue.f16.negative.max },
+ { val: -0, dir: 'positive', result: kValue.f16.positive.min },
+ { val: -0, dir: 'negative', result: kValue.f16.negative.max },
+
+ // Subnormals
+ { val: kValue.f16.positive.subnormal.min, dir: 'positive', result: kValue.f16.positive.min },
+ { val: kValue.f16.positive.subnormal.min, dir: 'negative', result: kValue.f16.negative.max },
+ { val: kValue.f16.positive.subnormal.max, dir: 'positive', result: kValue.f16.positive.min },
+ { val: kValue.f16.positive.subnormal.max, dir: 'negative', result: kValue.f16.negative.max },
+ { val: kValue.f16.negative.subnormal.min, dir: 'positive', result: kValue.f16.positive.min },
+ { val: kValue.f16.negative.subnormal.min, dir: 'negative', result: kValue.f16.negative.max },
+ { val: kValue.f16.negative.subnormal.max, dir: 'positive', result: kValue.f16.positive.min },
+ { val: kValue.f16.negative.subnormal.max, dir: 'negative', result: kValue.f16.negative.max },
+
+ // Normals
+ { val: kValue.f16.positive.max, dir: 'positive', result: kValue.f16.positive.infinity },
+ { val: kValue.f16.positive.max, dir: 'negative', result: reinterpretU16AsF16(0x7bfe) },
+ { val: kValue.f16.positive.min, dir: 'positive', result: reinterpretU16AsF16(0x0401) },
+ { val: kValue.f16.positive.min, dir: 'negative', result: 0 },
+ { val: kValue.f16.negative.max, dir: 'positive', result: 0 },
+ { val: kValue.f16.negative.max, dir: 'negative', result: reinterpretU16AsF16(0x8401) },
+ { val: kValue.f16.negative.min, dir: 'positive', result: reinterpretU16AsF16(0xfbfe) },
+ { val: kValue.f16.negative.min, dir: 'negative', result: kValue.f16.negative.infinity },
+ { val: reinterpretU16AsF16(0x1380), dir: 'positive', result: reinterpretU16AsF16(0x1381) },
+ { val: reinterpretU16AsF16(0x1380), dir: 'negative', result: reinterpretU16AsF16(0x137f) },
+ { val: reinterpretU16AsF16(0x9380), dir: 'positive', result: reinterpretU16AsF16(0x937f) },
+ { val: reinterpretU16AsF16(0x9380), dir: 'negative', result: reinterpretU16AsF16(0x9381) },
+
+ // Not precisely expressible as f16
+ { val: 0.01, dir: 'positive', result: reinterpretU16AsF16(0x211f) }, // positive normal
+ { val: 0.01, dir: 'negative', result: reinterpretU16AsF16(0x211e) }, // positive normal
+ { val: -0.01, dir: 'positive', result: reinterpretU16AsF16(0xa11e) }, // negative normal
+ { val: -0.01, dir: 'negative', result: reinterpretU16AsF16(0xa11f) }, // negative normal
+ { val: 2.82E-40, dir: 'positive', result: kValue.f16.positive.min }, // positive subnormal
+ { val: 2.82E-40, dir: 'negative', result: kValue.f16.negative.max }, // positive subnormal
+ { val: -2.82E-40, dir: 'positive', result: kValue.f16.positive.min }, // negative subnormal
+ { val: -2.82E-40, dir: 'negative', result: kValue.f16.negative.max }, // negative subnormal
+ ]
+ )
+ .fn(t => {
+ const val = t.params.val;
+ const dir = t.params.dir;
+ const expect = t.params.result;
+ const got = nextAfterF16(val, dir, 'flush');
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `nextAfterF16(${f64(val)}, '${dir}', 'flush') returned ${f16(got)}. Expected ${f16(expect)}`
+ );
+ });
+
+g.test('nextAfterF16NoFlush')
+ .paramsSubcasesOnly<nextAfterCase>(
+ // prettier-ignore
+ [
+ // Edge Cases
+ { val: Number.NaN, dir: 'positive', result: Number.NaN },
+ { val: Number.NaN, dir: 'negative', result: Number.NaN },
+ { val: Number.POSITIVE_INFINITY, dir: 'positive', result: kValue.f16.positive.infinity },
+ { val: Number.POSITIVE_INFINITY, dir: 'negative', result: kValue.f16.positive.infinity },
+ { val: Number.NEGATIVE_INFINITY, dir: 'positive', result: kValue.f16.negative.infinity },
+ { val: Number.NEGATIVE_INFINITY, dir: 'negative', result: kValue.f16.negative.infinity },
+
+ // Zeroes
+ { val: +0, dir: 'positive', result: kValue.f16.positive.subnormal.min },
+ { val: +0, dir: 'negative', result: kValue.f16.negative.subnormal.max },
+ { val: -0, dir: 'positive', result: kValue.f16.positive.subnormal.min },
+ { val: -0, dir: 'negative', result: kValue.f16.negative.subnormal.max },
+
+ // Subnormals
+ { val: kValue.f16.positive.subnormal.min, dir: 'positive', result: reinterpretU16AsF16(0x0002) },
+ { val: kValue.f16.positive.subnormal.min, dir: 'negative', result: 0 },
+ { val: kValue.f16.positive.subnormal.max, dir: 'positive', result: kValue.f16.positive.min },
+ { val: kValue.f16.positive.subnormal.max, dir: 'negative', result: reinterpretU16AsF16(0x03fe) },
+ { val: kValue.f16.negative.subnormal.min, dir: 'positive', result: reinterpretU16AsF16(0x83fe) },
+ { val: kValue.f16.negative.subnormal.min, dir: 'negative', result: kValue.f16.negative.max },
+ { val: kValue.f16.negative.subnormal.max, dir: 'positive', result: 0 },
+ { val: kValue.f16.negative.subnormal.max, dir: 'negative', result: reinterpretU16AsF16(0x8002) },
+
+ // Normals
+ { val: kValue.f16.positive.max, dir: 'positive', result: kValue.f16.positive.infinity },
+ { val: kValue.f16.positive.max, dir: 'negative', result: reinterpretU16AsF16(0x7bfe) },
+ { val: kValue.f16.positive.min, dir: 'positive', result: reinterpretU16AsF16(0x0401) },
+ { val: kValue.f16.positive.min, dir: 'negative', result: kValue.f16.positive.subnormal.max },
+ { val: kValue.f16.negative.max, dir: 'positive', result: kValue.f16.negative.subnormal.min },
+ { val: kValue.f16.negative.max, dir: 'negative', result: reinterpretU16AsF16(0x8401) },
+ { val: kValue.f16.negative.min, dir: 'positive', result: reinterpretU16AsF16(0xfbfe) },
+ { val: kValue.f16.negative.min, dir: 'negative', result: kValue.f16.negative.infinity },
+ { val: reinterpretU16AsF16(0x1380), dir: 'positive', result: reinterpretU16AsF16(0x1381) },
+ { val: reinterpretU16AsF16(0x1380), dir: 'negative', result: reinterpretU16AsF16(0x137f) },
+ { val: reinterpretU16AsF16(0x9380), dir: 'positive', result: reinterpretU16AsF16(0x937f) },
+ { val: reinterpretU16AsF16(0x9380), dir: 'negative', result: reinterpretU16AsF16(0x9381) },
+
+ // Not precisely expressible as f16
+ { val: 0.01, dir: 'positive', result: reinterpretU16AsF16(0x211f) }, // positive normal
+ { val: 0.01, dir: 'negative', result: reinterpretU16AsF16(0x211e) }, // positive normal
+ { val: -0.01, dir: 'positive', result: reinterpretU16AsF16(0xa11e) }, // negative normal
+ { val: -0.01, dir: 'negative', result: reinterpretU16AsF16(0xa11f) }, // negative normal
+ { val: 2.82E-40, dir: 'positive', result: kValue.f16.positive.subnormal.min }, // positive subnormal
+ { val: 2.82E-40, dir: 'negative', result: 0 }, // positive subnormal
+ { val: -2.82E-40, dir: 'positive', result: 0 }, // negative subnormal
+ { val: -2.82E-40, dir: 'negative', result: kValue.f16.negative.subnormal.max }, // negative subnormal
+ ]
+ )
+ .fn(t => {
+ const val = t.params.val;
+ const dir = t.params.dir;
+ const expect = t.params.result;
+ const got = nextAfterF16(val, dir, 'no-flush');
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `nextAfterF16(${f64(val)}, '${dir}', 'no-flush') returned ${f16(got)}. Expected ${f16(
+ expect
+ )}`
+ );
+ });
+
+interface OneULPCase {
+ target: number;
+ expect: number;
+}
+
+g.test('oneULPF64FlushToZero')
+ .paramsSimple<OneULPCase>([
+ // Edge Cases
+ { target: Number.NaN, expect: Number.NaN },
+ { target: Number.POSITIVE_INFINITY, expect: reinterpretU64AsF64(0x7ca0_0000_0000_0000n) },
+ { target: Number.NEGATIVE_INFINITY, expect: reinterpretU64AsF64(0x7ca0_0000_0000_0000n) },
+
+ // Zeroes
+ { target: +0, expect: reinterpretU64AsF64(0x0010_0000_0000_0000n) },
+ { target: -0, expect: reinterpretU64AsF64(0x0010_0000_0000_0000n) },
+
+ // Subnormals
+ {
+ target: kValue.f64.positive.subnormal.min,
+ expect: reinterpretU64AsF64(0x0010_0000_0000_0000n),
+ },
+ {
+ target: kValue.f64.positive.subnormal.max,
+ expect: reinterpretU64AsF64(0x0010_0000_0000_0000n),
+ },
+ {
+ target: kValue.f64.negative.subnormal.min,
+ expect: reinterpretU64AsF64(0x0010_0000_0000_0000n),
+ },
+ {
+ target: kValue.f64.negative.subnormal.max,
+ expect: reinterpretU64AsF64(0x0010_0000_0000_0000n),
+ },
+
+ // Normals
+ { target: kValue.f64.positive.min, expect: reinterpretU64AsF64(0x0000_0000_0000_0001n) },
+ { target: 1, expect: reinterpretU64AsF64(0x3ca0_0000_0000_0000n) },
+ { target: 2, expect: reinterpretU64AsF64(0x3cb0_0000_0000_0000n) },
+ { target: 4, expect: reinterpretU64AsF64(0x3cc0_0000_0000_0000n) },
+ { target: 1000000, expect: reinterpretU64AsF64(0x3de0_0000_0000_0000n) },
+ { target: kValue.f64.positive.max, expect: reinterpretU64AsF64(0x7ca0_0000_0000_0000n) },
+ { target: kValue.f64.negative.max, expect: reinterpretU64AsF64(0x0000_0000_0000_0001n) },
+ { target: -1, expect: reinterpretU64AsF64(0x3ca0_0000_0000_0000n) },
+ { target: -2, expect: reinterpretU64AsF64(0x3cb0_0000_0000_0000n) },
+ { target: -4, expect: reinterpretU64AsF64(0x3cc0_0000_0000_0000n) },
+ { target: -1000000, expect: reinterpretU64AsF64(0x3de0_0000_0000_0000n) },
+ { target: kValue.f64.negative.min, expect: reinterpretU64AsF64(0x7ca0_0000_0000_0000n) },
+ ])
+ .fn(t => {
+ const target = t.params.target;
+ const got = oneULPF64(target, 'flush');
+ const expect = t.params.expect;
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `oneULPF64(${f64(target)}, 'flush') returned ${f64(got)}. Expected ${f64(expect)}`
+ );
+ });
+
+g.test('oneULPF64NoFlush')
+ .paramsSimple<OneULPCase>([
+ // Edge Cases
+ { target: Number.NaN, expect: Number.NaN },
+ { target: Number.POSITIVE_INFINITY, expect: reinterpretU64AsF64(0x7ca0_0000_0000_0000n) },
+ { target: Number.NEGATIVE_INFINITY, expect: reinterpretU64AsF64(0x7ca0_0000_0000_0000n) },
+
+ // Zeroes
+ { target: +0, expect: reinterpretU64AsF64(0x0000_0000_0000_0001n) },
+ { target: -0, expect: reinterpretU64AsF64(0x0000_0000_0000_0001n) },
+
+ // Subnormals
+ {
+ target: kValue.f64.positive.subnormal.min,
+ expect: reinterpretU64AsF64(0x0000_0000_0000_0001n),
+ },
+ {
+ target: kValue.f64.positive.subnormal.max,
+ expect: reinterpretU64AsF64(0x0000_0000_0000_0001n),
+ },
+ {
+ target: kValue.f64.negative.subnormal.min,
+ expect: reinterpretU64AsF64(0x0000_0000_0000_0001n),
+ },
+ {
+ target: kValue.f64.negative.subnormal.max,
+ expect: reinterpretU64AsF64(0x0000_0000_0000_0001n),
+ },
+
+ // Normals
+ { target: kValue.f64.positive.min, expect: reinterpretU64AsF64(0x0000_0000_0000_0001n) },
+ { target: 1, expect: reinterpretU64AsF64(0x3ca0_0000_0000_0000n) },
+ { target: 2, expect: reinterpretU64AsF64(0x3cb0_0000_0000_0000n) },
+ { target: 4, expect: reinterpretU64AsF64(0x3cc0_0000_0000_0000n) },
+ { target: 1000000, expect: reinterpretU64AsF64(0x3de0_0000_0000_0000n) },
+ { target: kValue.f64.positive.max, expect: reinterpretU64AsF64(0x7ca0_0000_0000_0000n) },
+ { target: kValue.f64.negative.max, expect: reinterpretU64AsF64(0x0000_0000_0000_0001n) },
+ { target: -1, expect: reinterpretU64AsF64(0x3ca0_0000_0000_0000n) },
+ { target: -2, expect: reinterpretU64AsF64(0x3cb0_0000_0000_0000n) },
+ { target: -4, expect: reinterpretU64AsF64(0x3cc0_0000_0000_0000n) },
+ { target: -1000000, expect: reinterpretU64AsF64(0x3de0_0000_0000_0000n) },
+ { target: kValue.f64.negative.min, expect: reinterpretU64AsF64(0x7ca0_0000_0000_0000n) },
+ ])
+ .fn(t => {
+ const target = t.params.target;
+ const got = oneULPF64(target, 'no-flush');
+ const expect = t.params.expect;
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `oneULPF64(${f64(target)}, 'no-flush') returned ${f64(got)}. Expected ${f64(expect)}`
+ );
+ });
+
+g.test('oneULPF64')
+ .paramsSimple<OneULPCase>([
+ // Edge Cases
+ { target: Number.NaN, expect: Number.NaN },
+ { target: Number.POSITIVE_INFINITY, expect: reinterpretU64AsF64(0x7ca0_0000_0000_0000n) },
+ { target: Number.NEGATIVE_INFINITY, expect: reinterpretU64AsF64(0x7ca0_0000_0000_0000n) },
+
+ // Zeroes
+ { target: +0, expect: reinterpretU64AsF64(0x0010_0000_0000_0000n) },
+ { target: -0, expect: reinterpretU64AsF64(0x0010_0000_0000_0000n) },
+
+ // Subnormals
+ {
+ target: kValue.f64.positive.subnormal.min,
+ expect: reinterpretU64AsF64(0x0010_0000_0000_0000n),
+ },
+ {
+ target: kValue.f64.positive.subnormal.max,
+ expect: reinterpretU64AsF64(0x0010_0000_0000_0000n),
+ },
+ {
+ target: kValue.f64.negative.subnormal.min,
+ expect: reinterpretU64AsF64(0x0010_0000_0000_0000n),
+ },
+ {
+ target: kValue.f64.negative.subnormal.max,
+ expect: reinterpretU64AsF64(0x0010_0000_0000_0000n),
+ },
+
+ // Normals
+ { target: kValue.f64.positive.min, expect: reinterpretU64AsF64(0x0000_0000_0000_0001n) },
+ { target: 1, expect: reinterpretU64AsF64(0x3ca0_0000_0000_0000n) },
+ { target: 2, expect: reinterpretU64AsF64(0x3cb0_0000_0000_0000n) },
+ { target: 4, expect: reinterpretU64AsF64(0x3cc0_0000_0000_0000n) },
+ { target: 1000000, expect: reinterpretU64AsF64(0x3de0_0000_0000_0000n) },
+ { target: kValue.f64.positive.max, expect: reinterpretU64AsF64(0x7ca0_0000_0000_0000n) },
+ { target: kValue.f64.negative.max, expect: reinterpretU64AsF64(0x0000_0000_0000_0001n) },
+ { target: -1, expect: reinterpretU64AsF64(0x3ca0_0000_0000_0000n) },
+ { target: -2, expect: reinterpretU64AsF64(0x3cb0_0000_0000_0000n) },
+ { target: -4, expect: reinterpretU64AsF64(0x3cc0_0000_0000_0000n) },
+ { target: -1000000, expect: reinterpretU64AsF64(0x3de0_0000_0000_0000n) },
+ { target: kValue.f64.negative.min, expect: reinterpretU64AsF64(0x7ca0_0000_0000_0000n) },
+ ])
+ .fn(t => {
+ const target = t.params.target;
+ const got = oneULPF64(target);
+ const expect = t.params.expect;
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `oneULPF64(${f64(target)}) returned ${f64(got)}. Expected ${f64(expect)}`
+ );
+ });
+
+g.test('oneULPF32FlushToZero')
+ .paramsSimple<OneULPCase>([
+ // Edge Cases
+ { target: Number.NaN, expect: Number.NaN },
+ { target: Number.POSITIVE_INFINITY, expect: reinterpretU32AsF32(0x73800000) },
+ { target: Number.NEGATIVE_INFINITY, expect: reinterpretU32AsF32(0x73800000) },
+
+ // Zeroes
+ { target: +0, expect: reinterpretU32AsF32(0x00800000) },
+ { target: -0, expect: reinterpretU32AsF32(0x00800000) },
+
+ // Subnormals
+ { target: kValue.f32.positive.subnormal.min, expect: reinterpretU32AsF32(0x00800000) },
+ { target: 2.82e-40, expect: reinterpretU32AsF32(0x00800000) }, // positive subnormal
+ { target: kValue.f32.positive.subnormal.max, expect: reinterpretU32AsF32(0x00800000) },
+ { target: kValue.f32.negative.subnormal.min, expect: reinterpretU32AsF32(0x00800000) },
+ { target: -2.82e-40, expect: reinterpretU32AsF32(0x00800000) }, // negative subnormal
+ { target: kValue.f32.negative.subnormal.max, expect: reinterpretU32AsF32(0x00800000) },
+
+ // Normals
+ { target: kValue.f32.positive.min, expect: reinterpretU32AsF32(0x00000001) },
+ { target: 1, expect: reinterpretU32AsF32(0x33800000) },
+ { target: 2, expect: reinterpretU32AsF32(0x34000000) },
+ { target: 4, expect: reinterpretU32AsF32(0x34800000) },
+ { target: 1000000, expect: reinterpretU32AsF32(0x3d800000) },
+ { target: kValue.f32.positive.max, expect: reinterpretU32AsF32(0x73800000) },
+ { target: kValue.f32.negative.max, expect: reinterpretU32AsF32(0x00000001) },
+ { target: -1, expect: reinterpretU32AsF32(0x33800000) },
+ { target: -2, expect: reinterpretU32AsF32(0x34000000) },
+ { target: -4, expect: reinterpretU32AsF32(0x34800000) },
+ { target: -1000000, expect: reinterpretU32AsF32(0x3d800000) },
+ { target: kValue.f32.negative.min, expect: reinterpretU32AsF32(0x73800000) },
+
+ // No precise f32 value
+ { target: 0.001, expect: reinterpretU32AsF32(0x2f000000) }, // positive normal
+ { target: -0.001, expect: reinterpretU32AsF32(0x2f000000) }, // negative normal
+ { target: 1e40, expect: reinterpretU32AsF32(0x73800000) }, // positive out of range
+ { target: -1e40, expect: reinterpretU32AsF32(0x73800000) }, // negative out of range
+ ])
+ .fn(t => {
+ const target = t.params.target;
+ const got = oneULPF32(target, 'flush');
+ const expect = t.params.expect;
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `oneULPF32(${target}, 'flush') returned ${got}. Expected ${expect}`
+ );
+ });
+
+g.test('oneULPF32NoFlush')
+ .paramsSimple<OneULPCase>([
+ // Edge Cases
+ { target: Number.NaN, expect: Number.NaN },
+ { target: Number.POSITIVE_INFINITY, expect: reinterpretU32AsF32(0x73800000) },
+ { target: Number.NEGATIVE_INFINITY, expect: reinterpretU32AsF32(0x73800000) },
+
+ // Zeroes
+ { target: +0, expect: reinterpretU32AsF32(0x00000001) },
+ { target: -0, expect: reinterpretU32AsF32(0x00000001) },
+
+ // Subnormals
+ { target: kValue.f32.positive.subnormal.min, expect: reinterpretU32AsF32(0x00000001) },
+ { target: -2.82e-40, expect: reinterpretU32AsF32(0x00000001) }, // negative subnormal
+ { target: kValue.f32.positive.subnormal.max, expect: reinterpretU32AsF32(0x00000001) },
+ { target: kValue.f32.negative.subnormal.min, expect: reinterpretU32AsF32(0x00000001) },
+ { target: 2.82e-40, expect: reinterpretU32AsF32(0x00000001) }, // positive subnormal
+ { target: kValue.f32.negative.subnormal.max, expect: reinterpretU32AsF32(0x00000001) },
+
+ // Normals
+ { target: kValue.f32.positive.min, expect: reinterpretU32AsF32(0x00000001) },
+ { target: 1, expect: reinterpretU32AsF32(0x33800000) },
+ { target: 2, expect: reinterpretU32AsF32(0x34000000) },
+ { target: 4, expect: reinterpretU32AsF32(0x34800000) },
+ { target: 1000000, expect: reinterpretU32AsF32(0x3d800000) },
+ { target: kValue.f32.positive.max, expect: reinterpretU32AsF32(0x73800000) },
+ { target: kValue.f32.negative.max, expect: reinterpretU32AsF32(0x00000001) },
+ { target: -1, expect: reinterpretU32AsF32(0x33800000) },
+ { target: -2, expect: reinterpretU32AsF32(0x34000000) },
+ { target: -4, expect: reinterpretU32AsF32(0x34800000) },
+ { target: -1000000, expect: reinterpretU32AsF32(0x3d800000) },
+ { target: kValue.f32.negative.min, expect: reinterpretU32AsF32(0x73800000) },
+
+ // No precise f32 value
+ { target: 0.001, expect: reinterpretU32AsF32(0x2f000000) }, // positive normal
+ { target: -0.001, expect: reinterpretU32AsF32(0x2f000000) }, // negative normal
+ { target: 1e40, expect: reinterpretU32AsF32(0x73800000) }, // positive out of range
+ { target: -1e40, expect: reinterpretU32AsF32(0x73800000) }, // negative out of range
+ ])
+ .fn(t => {
+ const target = t.params.target;
+ const got = oneULPF32(target, 'no-flush');
+ const expect = t.params.expect;
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `oneULPF32(${target}, no-flush) returned ${got}. Expected ${expect}`
+ );
+ });
+
+g.test('oneULPF32')
+ .paramsSimple<OneULPCase>([
+ // Edge Cases
+ { target: Number.NaN, expect: Number.NaN },
+ { target: Number.NEGATIVE_INFINITY, expect: reinterpretU32AsF32(0x73800000) },
+ { target: Number.POSITIVE_INFINITY, expect: reinterpretU32AsF32(0x73800000) },
+
+ // Zeroes
+ { target: +0, expect: reinterpretU32AsF32(0x00800000) },
+ { target: -0, expect: reinterpretU32AsF32(0x00800000) },
+
+ // Subnormals
+ { target: kValue.f32.negative.subnormal.max, expect: reinterpretU32AsF32(0x00800000) },
+ { target: -2.82e-40, expect: reinterpretU32AsF32(0x00800000) },
+ { target: kValue.f32.negative.subnormal.min, expect: reinterpretU32AsF32(0x00800000) },
+ { target: kValue.f32.positive.subnormal.max, expect: reinterpretU32AsF32(0x00800000) },
+ { target: 2.82e-40, expect: reinterpretU32AsF32(0x00800000) },
+ { target: kValue.f32.positive.subnormal.min, expect: reinterpretU32AsF32(0x00800000) },
+
+ // Normals
+ { target: kValue.f32.positive.min, expect: reinterpretU32AsF32(0x00000001) },
+ { target: 1, expect: reinterpretU32AsF32(0x33800000) },
+ { target: 2, expect: reinterpretU32AsF32(0x34000000) },
+ { target: 4, expect: reinterpretU32AsF32(0x34800000) },
+ { target: 1000000, expect: reinterpretU32AsF32(0x3d800000) },
+ { target: kValue.f32.positive.max, expect: reinterpretU32AsF32(0x73800000) },
+ { target: kValue.f32.negative.max, expect: reinterpretU32AsF32(0x000000001) },
+ { target: -1, expect: reinterpretU32AsF32(0x33800000) },
+ { target: -2, expect: reinterpretU32AsF32(0x34000000) },
+ { target: -4, expect: reinterpretU32AsF32(0x34800000) },
+ { target: -1000000, expect: reinterpretU32AsF32(0x3d800000) },
+ { target: kValue.f32.negative.min, expect: reinterpretU32AsF32(0x73800000) },
+
+ // No precise f32 value
+ { target: -0.001, expect: reinterpretU32AsF32(0x2f000000) }, // negative normal
+ { target: -1e40, expect: reinterpretU32AsF32(0x73800000) }, // negative out of range
+ { target: 0.001, expect: reinterpretU32AsF32(0x2f000000) }, // positive normal
+ { target: 1e40, expect: reinterpretU32AsF32(0x73800000) }, // positive out of range
+ ])
+ .fn(t => {
+ const target = t.params.target;
+ const got = oneULPF32(target);
+ const expect = t.params.expect;
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `oneULPF32(${target}) returned ${got}. Expected ${expect}`
+ );
+ });
+
+g.test('oneULPF16FlushToZero')
+ .paramsSubcasesOnly<OneULPCase>([
+ // Edge Cases
+ { target: Number.NaN, expect: Number.NaN },
+ { target: Number.POSITIVE_INFINITY, expect: reinterpretU16AsF16(0x5000) },
+ { target: Number.NEGATIVE_INFINITY, expect: reinterpretU16AsF16(0x5000) },
+
+ // Zeroes, expect positive.min in flush mode
+ { target: +0, expect: reinterpretU16AsF16(0x0400) },
+ { target: -0, expect: reinterpretU16AsF16(0x0400) },
+
+ // Subnormals
+ { target: kValue.f16.positive.subnormal.min, expect: reinterpretU16AsF16(0x0400) },
+ { target: 1.91e-6, expect: reinterpretU16AsF16(0x0400) }, // positive subnormal
+ { target: kValue.f16.positive.subnormal.max, expect: reinterpretU16AsF16(0x0400) },
+ { target: kValue.f16.negative.subnormal.min, expect: reinterpretU16AsF16(0x0400) },
+ { target: -1.91e-6, expect: reinterpretU16AsF16(0x0400) }, // negative subnormal
+ { target: kValue.f16.negative.subnormal.max, expect: reinterpretU16AsF16(0x0400) },
+
+ // Normals
+ { target: kValue.f16.positive.min, expect: reinterpretU16AsF16(0x0001) },
+ { target: 1, expect: reinterpretU16AsF16(0x1000) },
+ { target: 2, expect: reinterpretU16AsF16(0x1400) },
+ { target: 4, expect: reinterpretU16AsF16(0x1800) },
+ { target: 1000, expect: reinterpretU16AsF16(0x3800) },
+ { target: kValue.f16.positive.max, expect: reinterpretU16AsF16(0x5000) },
+ { target: kValue.f16.negative.max, expect: reinterpretU16AsF16(0x0001) },
+ { target: -1, expect: reinterpretU16AsF16(0x1000) },
+ { target: -2, expect: reinterpretU16AsF16(0x1400) },
+ { target: -4, expect: reinterpretU16AsF16(0x1800) },
+ { target: -1000, expect: reinterpretU16AsF16(0x3800) },
+ { target: kValue.f16.negative.min, expect: reinterpretU16AsF16(0x5000) },
+
+ // No precise f16 value
+ { target: 0.001, expect: reinterpretU16AsF16(0x0010) }, // positive normal
+ { target: -0.001, expect: reinterpretU16AsF16(0x0010) }, // negative normal
+ { target: 1e8, expect: reinterpretU16AsF16(0x5000) }, // positive out of range
+ { target: -1e8, expect: reinterpretU16AsF16(0x5000) }, // negative out of range
+ ])
+ .fn(t => {
+ const target = t.params.target;
+ const got = oneULPF16(target, 'flush');
+ const expect = t.params.expect;
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `oneULPF16(${target}, 'flush') returned ${got}. Expected ${expect}`
+ );
+ });
+
+g.test('oneULPF16NoFlush')
+ .paramsSubcasesOnly<OneULPCase>([
+ // Edge Cases
+ { target: Number.NaN, expect: Number.NaN },
+ { target: Number.POSITIVE_INFINITY, expect: reinterpretU16AsF16(0x5000) },
+ { target: Number.NEGATIVE_INFINITY, expect: reinterpretU16AsF16(0x5000) },
+
+ // Zeroes, expect positive.min in flush mode
+ { target: +0, expect: reinterpretU16AsF16(0x0001) },
+ { target: -0, expect: reinterpretU16AsF16(0x0001) },
+
+ // Subnormals
+ { target: kValue.f16.positive.subnormal.min, expect: reinterpretU16AsF16(0x0001) },
+ { target: 1.91e-6, expect: reinterpretU16AsF16(0x0001) }, // positive subnormal
+ { target: kValue.f16.positive.subnormal.max, expect: reinterpretU16AsF16(0x0001) },
+ { target: kValue.f16.negative.subnormal.min, expect: reinterpretU16AsF16(0x0001) },
+ { target: -1.91e-6, expect: reinterpretU16AsF16(0x0001) }, // negative subnormal
+ { target: kValue.f16.negative.subnormal.max, expect: reinterpretU16AsF16(0x0001) },
+
+ // Normals
+ { target: kValue.f16.positive.min, expect: reinterpretU16AsF16(0x0001) },
+ { target: 1, expect: reinterpretU16AsF16(0x1000) },
+ { target: 2, expect: reinterpretU16AsF16(0x1400) },
+ { target: 4, expect: reinterpretU16AsF16(0x1800) },
+ { target: 1000, expect: reinterpretU16AsF16(0x3800) },
+ { target: kValue.f16.positive.max, expect: reinterpretU16AsF16(0x5000) },
+ { target: kValue.f16.negative.max, expect: reinterpretU16AsF16(0x0001) },
+ { target: -1, expect: reinterpretU16AsF16(0x1000) },
+ { target: -2, expect: reinterpretU16AsF16(0x1400) },
+ { target: -4, expect: reinterpretU16AsF16(0x1800) },
+ { target: -1000, expect: reinterpretU16AsF16(0x3800) },
+ { target: kValue.f16.negative.min, expect: reinterpretU16AsF16(0x5000) },
+
+ // No precise f16 value
+ { target: 0.001, expect: reinterpretU16AsF16(0x0010) }, // positive normal
+ { target: -0.001, expect: reinterpretU16AsF16(0x0010) }, // negative normal
+ { target: 1e8, expect: reinterpretU16AsF16(0x5000) }, // positive out of range
+ { target: -1e8, expect: reinterpretU16AsF16(0x5000) }, // negative out of range
+ ])
+ .fn(t => {
+ const target = t.params.target;
+ const got = oneULPF16(target, 'no-flush');
+ const expect = t.params.expect;
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `oneULPF16(${target}, no-flush) returned ${got}. Expected ${expect}`
+ );
+ });
+
+g.test('oneULPF16')
+ .paramsSubcasesOnly<OneULPCase>([
+ // Edge Cases
+ { target: Number.NaN, expect: Number.NaN },
+ { target: Number.POSITIVE_INFINITY, expect: reinterpretU16AsF16(0x5000) },
+ { target: Number.NEGATIVE_INFINITY, expect: reinterpretU16AsF16(0x5000) },
+
+ // Zeroes, expect positive.min in flush mode
+ { target: +0, expect: reinterpretU16AsF16(0x0400) },
+ { target: -0, expect: reinterpretU16AsF16(0x0400) },
+
+ // Subnormals
+ { target: kValue.f16.positive.subnormal.min, expect: reinterpretU16AsF16(0x0400) },
+ { target: 1.91e-6, expect: reinterpretU16AsF16(0x0400) }, // positive subnormal
+ { target: kValue.f16.positive.subnormal.max, expect: reinterpretU16AsF16(0x0400) },
+ { target: kValue.f16.negative.subnormal.min, expect: reinterpretU16AsF16(0x0400) },
+ { target: -1.91e-6, expect: reinterpretU16AsF16(0x0400) }, // negative subnormal
+ { target: kValue.f16.negative.subnormal.max, expect: reinterpretU16AsF16(0x0400) },
+
+ // Normals
+ { target: kValue.f16.positive.min, expect: reinterpretU16AsF16(0x0001) },
+ { target: 1, expect: reinterpretU16AsF16(0x1000) },
+ { target: 2, expect: reinterpretU16AsF16(0x1400) },
+ { target: 4, expect: reinterpretU16AsF16(0x1800) },
+ { target: 1000, expect: reinterpretU16AsF16(0x3800) },
+ { target: kValue.f16.positive.max, expect: reinterpretU16AsF16(0x5000) },
+ { target: kValue.f16.negative.max, expect: reinterpretU16AsF16(0x0001) },
+ { target: -1, expect: reinterpretU16AsF16(0x1000) },
+ { target: -2, expect: reinterpretU16AsF16(0x1400) },
+ { target: -4, expect: reinterpretU16AsF16(0x1800) },
+ { target: -1000, expect: reinterpretU16AsF16(0x3800) },
+ { target: kValue.f16.negative.min, expect: reinterpretU16AsF16(0x5000) },
+
+ // No precise f16 value
+ { target: 0.001, expect: reinterpretU16AsF16(0x0010) }, // positive normal
+ { target: -0.001, expect: reinterpretU16AsF16(0x0010) }, // negative normal
+ { target: 1e8, expect: reinterpretU16AsF16(0x5000) }, // positive out of range
+ { target: -1e8, expect: reinterpretU16AsF16(0x5000) }, // negative out of range
+ ])
+ .fn(t => {
+ const target = t.params.target;
+ const got = oneULPF16(target, 'flush');
+ const expect = t.params.expect;
+ t.expect(
+ got === expect || (Number.isNaN(got) && Number.isNaN(expect)),
+ `oneULPF16(${target}, 'flush') returned ${got}. Expected ${expect}`
+ );
+ });
+
+interface correctlyRoundedCase {
+ value: number;
+ expected: Array<number>;
+}
+
+g.test('correctlyRoundedF32')
+ .paramsSubcasesOnly<correctlyRoundedCase>(
+ // prettier-ignore
+ [
+ // Edge Cases
+ { value: kValue.f32.positive.max, expected: [kValue.f32.positive.max] },
+ { value: kValue.f32.negative.min, expected: [kValue.f32.negative.min] },
+ { value: kValue.f32.positive.max + oneULPF64(kValue.f32.positive.max), expected: [kValue.f32.positive.max, Number.POSITIVE_INFINITY] },
+ { value: kValue.f32.negative.min - oneULPF64(kValue.f32.negative.min), expected: [Number.NEGATIVE_INFINITY, kValue.f32.negative.min] },
+ { value: 2 ** (kValue.f32.emax + 1) - oneULPF64(kValue.f32.positive.max), expected: [kValue.f32.positive.max, Number.POSITIVE_INFINITY] },
+ { value: -(2 ** (kValue.f32.emax + 1)) + oneULPF64(kValue.f32.positive.max), expected: [Number.NEGATIVE_INFINITY, kValue.f32.negative.min] },
+ { value: 2 ** (kValue.f32.emax + 1), expected: [Number.POSITIVE_INFINITY] },
+ { value: -(2 ** (kValue.f32.emax + 1)), expected: [Number.NEGATIVE_INFINITY] },
+ { value: kValue.f32.positive.infinity, expected: [Number.POSITIVE_INFINITY] },
+ { value: kValue.f32.negative.infinity, expected: [Number.NEGATIVE_INFINITY] },
+
+ // 32-bit subnormals
+ { value: kValue.f32.positive.subnormal.min, expected: [kValue.f32.positive.subnormal.min] },
+ { value: kValue.f32.positive.subnormal.max, expected: [kValue.f32.positive.subnormal.max] },
+ { value: kValue.f32.negative.subnormal.min, expected: [kValue.f32.negative.subnormal.min] },
+ { value: kValue.f32.negative.subnormal.max, expected: [kValue.f32.negative.subnormal.max] },
+
+ // 64-bit subnormals
+ { value: reinterpretU64AsF64(0x0000_0000_0000_0001n), expected: [0, kValue.f32.positive.subnormal.min] },
+ { value: reinterpretU64AsF64(0x0000_0000_0000_0002n), expected: [0, kValue.f32.positive.subnormal.min] },
+ { value: reinterpretU64AsF64(0x800f_ffff_ffff_ffffn), expected: [kValue.f32.negative.subnormal.max, 0] },
+ { value: reinterpretU64AsF64(0x800f_ffff_ffff_fffen), expected: [kValue.f32.negative.subnormal.max, 0] },
+
+ // 32-bit normals
+ { value: 0, expected: [0] },
+ { value: kValue.f32.positive.min, expected: [kValue.f32.positive.min] },
+ { value: kValue.f32.negative.max, expected: [kValue.f32.negative.max] },
+ { value: reinterpretU32AsF32(0x03800000), expected: [reinterpretU32AsF32(0x03800000)] },
+ { value: reinterpretU32AsF32(0x03800001), expected: [reinterpretU32AsF32(0x03800001)] },
+ { value: reinterpretU32AsF32(0x83800000), expected: [reinterpretU32AsF32(0x83800000)] },
+ { value: reinterpretU32AsF32(0x83800001), expected: [reinterpretU32AsF32(0x83800001)] },
+
+ // 64-bit normals
+ { value: reinterpretU64AsF64(0x3ff0_0000_0000_0001n), expected: [reinterpretU32AsF32(0x3f800000), reinterpretU32AsF32(0x3f800001)] },
+ { value: reinterpretU64AsF64(0x3ff0_0000_0000_0002n), expected: [reinterpretU32AsF32(0x3f800000), reinterpretU32AsF32(0x3f800001)] },
+ { value: reinterpretU64AsF64(0x3ff0_0010_0000_0010n), expected: [reinterpretU32AsF32(0x3f800080), reinterpretU32AsF32(0x3f800081)] },
+ { value: reinterpretU64AsF64(0x3ff0_0020_0000_0020n), expected: [reinterpretU32AsF32(0x3f800100), reinterpretU32AsF32(0x3f800101)] },
+ { value: reinterpretU64AsF64(0xbff0_0000_0000_0001n), expected: [reinterpretU32AsF32(0xbf800001), reinterpretU32AsF32(0xbf800000)] },
+ { value: reinterpretU64AsF64(0xbff0_0000_0000_0002n), expected: [reinterpretU32AsF32(0xbf800001), reinterpretU32AsF32(0xbf800000)] },
+ { value: reinterpretU64AsF64(0xbff0_0010_0000_0010n), expected: [reinterpretU32AsF32(0xbf800081), reinterpretU32AsF32(0xbf800080)] },
+ { value: reinterpretU64AsF64(0xbff0_0020_0000_0020n), expected: [reinterpretU32AsF32(0xbf800101), reinterpretU32AsF32(0xbf800100)] },
+ ]
+ )
+ .fn(t => {
+ const value = t.params.value;
+ const expected = t.params.expected;
+
+ const got = correctlyRoundedF32(value);
+ t.expect(
+ objectEquals(expected, got),
+ `correctlyRoundedF32(${f64(value)}) returned [${got.map(f32)}]. Expected [${expected.map(
+ f32
+ )}]`
+ );
+ });
+
+g.test('correctlyRoundedF16')
+ .paramsSubcasesOnly<correctlyRoundedCase>(
+ // prettier-ignore
+ [
+ // Edge Cases
+ { value: kValue.f16.positive.max, expected: [kValue.f16.positive.max] },
+ { value: kValue.f16.negative.min, expected: [kValue.f16.negative.min] },
+ { value: kValue.f16.positive.max + oneULPF64(kValue.f16.positive.max), expected: [kValue.f16.positive.max, Number.POSITIVE_INFINITY] },
+ { value: kValue.f16.negative.min - oneULPF64(kValue.f16.negative.min), expected: [Number.NEGATIVE_INFINITY, kValue.f16.negative.min] },
+ { value: 2 ** (kValue.f16.emax + 1) - oneULPF64(kValue.f16.positive.max), expected: [kValue.f16.positive.max, Number.POSITIVE_INFINITY] },
+ { value: -(2 ** (kValue.f16.emax + 1)) + oneULPF64(kValue.f16.positive.max), expected: [Number.NEGATIVE_INFINITY, kValue.f16.negative.min] },
+ { value: 2 ** (kValue.f16.emax + 1), expected: [Number.POSITIVE_INFINITY] },
+ { value: -(2 ** (kValue.f16.emax + 1)), expected: [Number.NEGATIVE_INFINITY] },
+ { value: kValue.f16.positive.infinity, expected: [Number.POSITIVE_INFINITY] },
+ { value: kValue.f16.negative.infinity, expected: [Number.NEGATIVE_INFINITY] },
+
+ // 16-bit subnormals
+ { value: kValue.f16.positive.subnormal.min, expected: [kValue.f16.positive.subnormal.min] },
+ { value: kValue.f16.positive.subnormal.max, expected: [kValue.f16.positive.subnormal.max] },
+ { value: kValue.f16.negative.subnormal.min, expected: [kValue.f16.negative.subnormal.min] },
+ { value: kValue.f16.negative.subnormal.max, expected: [kValue.f16.negative.subnormal.max] },
+
+ // 32-bit subnormals
+ { value: kValue.f32.positive.subnormal.min, expected: [0, kValue.f16.positive.subnormal.min] },
+ { value: kValue.f32.positive.subnormal.max, expected: [0, kValue.f16.positive.subnormal.min] },
+ { value: kValue.f32.negative.subnormal.max, expected: [kValue.f16.negative.subnormal.max, 0] },
+ { value: kValue.f32.negative.subnormal.min, expected: [kValue.f16.negative.subnormal.max, 0] },
+
+ // 16-bit normals
+ { value: 0, expected: [0] },
+ { value: kValue.f16.positive.min, expected: [kValue.f16.positive.min] },
+ { value: kValue.f16.negative.max, expected: [kValue.f16.negative.max] },
+ { value: reinterpretU16AsF16(0x1380), expected: [reinterpretU16AsF16(0x1380)] },
+ { value: reinterpretU16AsF16(0x1381), expected: [reinterpretU16AsF16(0x1381)] },
+ { value: reinterpretU16AsF16(0x9380), expected: [reinterpretU16AsF16(0x9380)] },
+ { value: reinterpretU16AsF16(0x9381), expected: [reinterpretU16AsF16(0x9381)] },
+
+ // 32-bit normals
+ { value: reinterpretU32AsF32(0x3a700001), expected: [reinterpretU16AsF16(0x1380), reinterpretU16AsF16(0x1381)] },
+ { value: reinterpretU32AsF32(0x3a700002), expected: [reinterpretU16AsF16(0x1380), reinterpretU16AsF16(0x1381)] },
+ { value: reinterpretU32AsF32(0xba700001), expected: [reinterpretU16AsF16(0x9381), reinterpretU16AsF16(0x9380)] },
+ { value: reinterpretU32AsF32(0xba700002), expected: [reinterpretU16AsF16(0x9381), reinterpretU16AsF16(0x9380)] },
+ ]
+ )
+ .fn(t => {
+ const value = t.params.value;
+ const expected = t.params.expected;
+
+ const got = correctlyRoundedF16(value);
+ t.expect(
+ objectEquals(expected, got),
+ `correctlyRoundedF16(${f64(value)}) returned [${got.map(f16)}]. Expected [${expected.map(
+ f16
+ )}]`
+ );
+ });
+
+interface frexpCase {
+ input: number;
+ fract: number;
+ exp: number;
+}
+
+// prettier-ignore
+const kFrexpCases = {
+ f32: [
+ { input: kValue.f32.positive.max, fract: 0.9999999403953552, exp: 128 },
+ { input: kValue.f32.positive.min, fract: 0.5, exp: -125 },
+ { input: kValue.f32.negative.max, fract: -0.5, exp: -125 },
+ { input: kValue.f32.negative.min, fract: -0.9999999403953552, exp: 128 },
+ { input: kValue.f32.positive.subnormal.max, fract: 0.9999998807907104, exp: -126 },
+ { input: kValue.f32.positive.subnormal.min, fract: 0.5, exp: -148 },
+ { input: kValue.f32.negative.subnormal.max, fract: -0.5, exp: -148 },
+ { input: kValue.f32.negative.subnormal.min, fract: -0.9999998807907104, exp: -126 },
+ ] as frexpCase[],
+ f16: [
+ { input: kValue.f16.positive.max, fract: 0.99951171875, exp: 16 },
+ { input: kValue.f16.positive.min, fract: 0.5, exp: -13 },
+ { input: kValue.f16.negative.max, fract: -0.5, exp: -13 },
+ { input: kValue.f16.negative.min, fract: -0.99951171875, exp: 16 },
+ { input: kValue.f16.positive.subnormal.max, fract: 0.9990234375, exp: -14 },
+ { input: kValue.f16.positive.subnormal.min, fract: 0.5, exp: -23 },
+ { input: kValue.f16.negative.subnormal.max, fract: -0.5, exp: -23 },
+ { input: kValue.f16.negative.subnormal.min, fract: -0.9990234375, exp: -14 },
+ ] as frexpCase[],
+ f64: [
+ { input: kValue.f64.positive.max, fract: reinterpretU64AsF64(0x3fef_ffff_ffff_ffffn) /* ~0.9999999999999999 */, exp: 1024 },
+ { input: kValue.f64.positive.min, fract: 0.5, exp: -1021 },
+ { input: kValue.f64.negative.max, fract: -0.5, exp: -1021 },
+ { input: kValue.f64.negative.min, fract: reinterpretU64AsF64(0xbfef_ffff_ffff_ffffn) /* ~-0.9999999999999999 */, exp: 1024 },
+ { input: kValue.f64.positive.subnormal.max, fract: reinterpretU64AsF64(0x3fef_ffff_ffff_fffen) /* ~0.9999999999999998 */, exp: -1022 },
+ { input: kValue.f64.positive.subnormal.min, fract: 0.5, exp: -1073 },
+ { input: kValue.f64.negative.subnormal.max, fract: -0.5, exp: -1073 },
+ { input: kValue.f64.negative.subnormal.min, fract: reinterpretU64AsF64(0xbfef_ffff_ffff_fffen) /* ~-0.9999999999999998 */, exp: -1022 },
+ ] as frexpCase[],
+} as const;
+
+g.test('frexp')
+ .params(u =>
+ u
+ .combine('trait', ['f32', 'f16', 'f64'] as const)
+ .beginSubcases()
+ .expandWithParams<frexpCase>(p => {
+ // prettier-ignore
+ return [
+ // +/- 0.0
+ { input: 0, fract: 0, exp: 0 },
+ { input: -0, fract: -0, exp: 0 },
+ // Normal float values that can be exactly represented by all float types
+ { input: 0.171875, fract: 0.6875, exp: -2 },
+ { input: -0.171875, fract: -0.6875, exp: -2 },
+ { input: 0.5, fract: 0.5, exp: 0 },
+ { input: -0.5, fract: -0.5, exp: 0 },
+ { input: 1, fract: 0.5, exp: 1 },
+ { input: -1, fract: -0.5, exp: 1 },
+ { input: 2, fract: 0.5, exp: 2 },
+ { input: -2, fract: -0.5, exp: 2 },
+ { input: 10000, fract: 0.6103515625, exp: 14 },
+ { input: -10000, fract: -0.6103515625, exp: 14 },
+ // Normal ans subnormal cases that are different for each type
+ ...kFrexpCases[p.trait],
+ // Inf and NaN
+ { input: Number.POSITIVE_INFINITY, fract: Number.POSITIVE_INFINITY, exp: 0 },
+ { input: Number.NEGATIVE_INFINITY, fract: Number.NEGATIVE_INFINITY, exp: 0 },
+ { input: Number.NaN, fract: Number.NaN, exp: 0 },
+ ];
+ })
+ )
+ .fn(test => {
+ const input = test.params.input;
+ const got = frexp(input, test.params.trait);
+ const expect = { fract: test.params.fract, exp: test.params.exp };
+
+ test.expect(
+ objectEquals(got, expect),
+ `frexp(${input}, ${test.params.trait}) returned { fract: ${got.fract}, exp: ${got.exp} }. Expected { fract: ${expect.fract}, exp: ${expect.exp} }`
+ );
+ });
+
+interface lerpCase {
+ a: number;
+ b: number;
+ t: number;
+ result: number;
+}
+
+g.test('lerp')
+ .paramsSimple<lerpCase>([
+ // Infinite cases
+ { a: 0.0, b: Number.POSITIVE_INFINITY, t: 0.5, result: Number.NaN },
+ { a: Number.POSITIVE_INFINITY, b: 0.0, t: 0.5, result: Number.NaN },
+ { a: Number.NEGATIVE_INFINITY, b: 1.0, t: 0.5, result: Number.NaN },
+ { a: 1.0, b: Number.NEGATIVE_INFINITY, t: 0.5, result: Number.NaN },
+ { a: Number.NEGATIVE_INFINITY, b: Number.POSITIVE_INFINITY, t: 0.5, result: Number.NaN },
+ { a: Number.POSITIVE_INFINITY, b: Number.NEGATIVE_INFINITY, t: 0.5, result: Number.NaN },
+ { a: 0.0, b: 1.0, t: Number.NEGATIVE_INFINITY, result: Number.NaN },
+ { a: 1.0, b: 0.0, t: Number.NEGATIVE_INFINITY, result: Number.NaN },
+ { a: 0.0, b: 1.0, t: Number.POSITIVE_INFINITY, result: Number.NaN },
+ { a: 1.0, b: 0.0, t: Number.POSITIVE_INFINITY, result: Number.NaN },
+
+ // [0.0, 1.0] cases
+ { a: 0.0, b: 1.0, t: -1.0, result: -1.0 },
+ { a: 0.0, b: 1.0, t: 0.0, result: 0.0 },
+ { a: 0.0, b: 1.0, t: 0.1, result: 0.1 },
+ { a: 0.0, b: 1.0, t: 0.01, result: 0.01 },
+ { a: 0.0, b: 1.0, t: 0.001, result: 0.001 },
+ { a: 0.0, b: 1.0, t: 0.25, result: 0.25 },
+ { a: 0.0, b: 1.0, t: 0.5, result: 0.5 },
+ { a: 0.0, b: 1.0, t: 0.9, result: 0.9 },
+ { a: 0.0, b: 1.0, t: 0.99, result: 0.99 },
+ { a: 0.0, b: 1.0, t: 0.999, result: 0.999 },
+ { a: 0.0, b: 1.0, t: 1.0, result: 1.0 },
+ { a: 0.0, b: 1.0, t: 2.0, result: 2.0 },
+
+ // [1.0, 0.0] cases
+ { a: 1.0, b: 0.0, t: -1.0, result: 2.0 },
+ { a: 1.0, b: 0.0, t: 0.0, result: 1.0 },
+ { a: 1.0, b: 0.0, t: 0.1, result: 0.9 },
+ { a: 1.0, b: 0.0, t: 0.01, result: 0.99 },
+ { a: 1.0, b: 0.0, t: 0.001, result: 0.999 },
+ { a: 1.0, b: 0.0, t: 0.25, result: 0.75 },
+ { a: 1.0, b: 0.0, t: 0.5, result: 0.5 },
+ { a: 1.0, b: 0.0, t: 0.9, result: 0.1 },
+ { a: 1.0, b: 0.0, t: 0.99, result: 0.01 },
+ { a: 1.0, b: 0.0, t: 0.999, result: 0.001 },
+ { a: 1.0, b: 0.0, t: 1.0, result: 0.0 },
+ { a: 1.0, b: 0.0, t: 2.0, result: -1.0 },
+
+ // [0.0, 10.0] cases
+ { a: 0.0, b: 10.0, t: -1.0, result: -10.0 },
+ { a: 0.0, b: 10.0, t: 0.0, result: 0.0 },
+ { a: 0.0, b: 10.0, t: 0.1, result: 1.0 },
+ { a: 0.0, b: 10.0, t: 0.01, result: 0.1 },
+ { a: 0.0, b: 10.0, t: 0.001, result: 0.01 },
+ { a: 0.0, b: 10.0, t: 0.25, result: 2.5 },
+ { a: 0.0, b: 10.0, t: 0.5, result: 5.0 },
+ { a: 0.0, b: 10.0, t: 0.9, result: 9.0 },
+ { a: 0.0, b: 10.0, t: 0.99, result: 9.9 },
+ { a: 0.0, b: 10.0, t: 0.999, result: 9.99 },
+ { a: 0.0, b: 10.0, t: 1.0, result: 10.0 },
+ { a: 0.0, b: 10.0, t: 2.0, result: 20.0 },
+
+ // [10.0, 0.0] cases
+ { a: 10.0, b: 0.0, t: -1.0, result: 20.0 },
+ { a: 10.0, b: 0.0, t: 0.0, result: 10.0 },
+ { a: 10.0, b: 0.0, t: 0.1, result: 9 },
+ { a: 10.0, b: 0.0, t: 0.01, result: 9.9 },
+ { a: 10.0, b: 0.0, t: 0.001, result: 9.99 },
+ { a: 10.0, b: 0.0, t: 0.25, result: 7.5 },
+ { a: 10.0, b: 0.0, t: 0.5, result: 5.0 },
+ { a: 10.0, b: 0.0, t: 0.9, result: 1.0 },
+ { a: 10.0, b: 0.0, t: 0.99, result: 0.1 },
+ { a: 10.0, b: 0.0, t: 0.999, result: 0.01 },
+ { a: 10.0, b: 0.0, t: 1.0, result: 0.0 },
+ { a: 10.0, b: 0.0, t: 2.0, result: -10.0 },
+
+ // [2.0, 10.0] cases
+ { a: 2.0, b: 10.0, t: -1.0, result: -6.0 },
+ { a: 2.0, b: 10.0, t: 0.0, result: 2.0 },
+ { a: 2.0, b: 10.0, t: 0.1, result: 2.8 },
+ { a: 2.0, b: 10.0, t: 0.01, result: 2.08 },
+ { a: 2.0, b: 10.0, t: 0.001, result: 2.008 },
+ { a: 2.0, b: 10.0, t: 0.25, result: 4.0 },
+ { a: 2.0, b: 10.0, t: 0.5, result: 6.0 },
+ { a: 2.0, b: 10.0, t: 0.9, result: 9.2 },
+ { a: 2.0, b: 10.0, t: 0.99, result: 9.92 },
+ { a: 2.0, b: 10.0, t: 0.999, result: 9.992 },
+ { a: 2.0, b: 10.0, t: 1.0, result: 10.0 },
+ { a: 2.0, b: 10.0, t: 2.0, result: 18.0 },
+
+ // [10.0, 2.0] cases
+ { a: 10.0, b: 2.0, t: -1.0, result: 18.0 },
+ { a: 10.0, b: 2.0, t: 0.0, result: 10.0 },
+ { a: 10.0, b: 2.0, t: 0.1, result: 9.2 },
+ { a: 10.0, b: 2.0, t: 0.01, result: 9.92 },
+ { a: 10.0, b: 2.0, t: 0.001, result: 9.992 },
+ { a: 10.0, b: 2.0, t: 0.25, result: 8.0 },
+ { a: 10.0, b: 2.0, t: 0.5, result: 6.0 },
+ { a: 10.0, b: 2.0, t: 0.9, result: 2.8 },
+ { a: 10.0, b: 2.0, t: 0.99, result: 2.08 },
+ { a: 10.0, b: 2.0, t: 0.999, result: 2.008 },
+ { a: 10.0, b: 2.0, t: 1.0, result: 2.0 },
+ { a: 10.0, b: 2.0, t: 2.0, result: -6.0 },
+
+ // [-1.0, 1.0] cases
+ { a: -1.0, b: 1.0, t: -2.0, result: -5.0 },
+ { a: -1.0, b: 1.0, t: 0.0, result: -1.0 },
+ { a: -1.0, b: 1.0, t: 0.1, result: -0.8 },
+ { a: -1.0, b: 1.0, t: 0.01, result: -0.98 },
+ { a: -1.0, b: 1.0, t: 0.001, result: -0.998 },
+ { a: -1.0, b: 1.0, t: 0.25, result: -0.5 },
+ { a: -1.0, b: 1.0, t: 0.5, result: 0.0 },
+ { a: -1.0, b: 1.0, t: 0.9, result: 0.8 },
+ { a: -1.0, b: 1.0, t: 0.99, result: 0.98 },
+ { a: -1.0, b: 1.0, t: 0.999, result: 0.998 },
+ { a: -1.0, b: 1.0, t: 1.0, result: 1.0 },
+ { a: -1.0, b: 1.0, t: 2.0, result: 3.0 },
+
+ // [1.0, -1.0] cases
+ { a: 1.0, b: -1.0, t: -2.0, result: 5.0 },
+ { a: 1.0, b: -1.0, t: 0.0, result: 1.0 },
+ { a: 1.0, b: -1.0, t: 0.1, result: 0.8 },
+ { a: 1.0, b: -1.0, t: 0.01, result: 0.98 },
+ { a: 1.0, b: -1.0, t: 0.001, result: 0.998 },
+ { a: 1.0, b: -1.0, t: 0.25, result: 0.5 },
+ { a: 1.0, b: -1.0, t: 0.5, result: 0.0 },
+ { a: 1.0, b: -1.0, t: 0.9, result: -0.8 },
+ { a: 1.0, b: -1.0, t: 0.99, result: -0.98 },
+ { a: 1.0, b: -1.0, t: 0.999, result: -0.998 },
+ { a: 1.0, b: -1.0, t: 1.0, result: -1.0 },
+ { a: 1.0, b: -1.0, t: 2.0, result: -3.0 },
+
+ // [-1.0, 0.0] cases
+ { a: -1.0, b: 0.0, t: -1.0, result: -2.0 },
+ { a: -1.0, b: 0.0, t: 0.0, result: -1.0 },
+ { a: -1.0, b: 0.0, t: 0.1, result: -0.9 },
+ { a: -1.0, b: 0.0, t: 0.01, result: -0.99 },
+ { a: -1.0, b: 0.0, t: 0.001, result: -0.999 },
+ { a: -1.0, b: 0.0, t: 0.25, result: -0.75 },
+ { a: -1.0, b: 0.0, t: 0.5, result: -0.5 },
+ { a: -1.0, b: 0.0, t: 0.9, result: -0.1 },
+ { a: -1.0, b: 0.0, t: 0.99, result: -0.01 },
+ { a: -1.0, b: 0.0, t: 0.999, result: -0.001 },
+ { a: -1.0, b: 0.0, t: 1.0, result: 0.0 },
+ { a: -1.0, b: 0.0, t: 2.0, result: 1.0 },
+
+ // [0.0, -1.0] cases
+ { a: 0.0, b: -1.0, t: -1.0, result: 1.0 },
+ { a: 0.0, b: -1.0, t: 0.0, result: 0.0 },
+ { a: 0.0, b: -1.0, t: 0.1, result: -0.1 },
+ { a: 0.0, b: -1.0, t: 0.01, result: -0.01 },
+ { a: 0.0, b: -1.0, t: 0.001, result: -0.001 },
+ { a: 0.0, b: -1.0, t: 0.25, result: -0.25 },
+ { a: 0.0, b: -1.0, t: 0.5, result: -0.5 },
+ { a: 0.0, b: -1.0, t: 0.9, result: -0.9 },
+ { a: 0.0, b: -1.0, t: 0.99, result: -0.99 },
+ { a: 0.0, b: -1.0, t: 0.999, result: -0.999 },
+ { a: 0.0, b: -1.0, t: 1.0, result: -1.0 },
+ { a: 0.0, b: -1.0, t: 2.0, result: -2.0 },
+ ])
+ .fn(test => {
+ const a = test.params.a;
+ const b = test.params.b;
+ const t = test.params.t;
+ const got = lerp(a, b, t);
+ const expect = test.params.result;
+
+ test.expect(
+ (Number.isNaN(got) && Number.isNaN(expect)) || withinOneULPF32(got, expect, 'flush'),
+ `lerp(${a}, ${b}, ${t}) returned ${got}. Expected ${expect}`
+ );
+ });
+
+interface lerpBigIntCase {
+ a: bigint;
+ b: bigint;
+ idx: number;
+ steps: number;
+ result: bigint;
+}
+
+g.test('lerpBigInt')
+ .paramsSimple<lerpBigIntCase>([
+ // [0n, 1000n] cases
+ { a: 0n, b: 1000n, idx: 0, steps: 1, result: 0n },
+ { a: 0n, b: 1000n, idx: 0, steps: 2, result: 0n },
+ { a: 0n, b: 1000n, idx: 1, steps: 2, result: 1000n },
+ { a: 0n, b: 1000n, idx: 0, steps: 1000, result: 0n },
+ { a: 0n, b: 1000n, idx: 500, steps: 1000, result: 500n },
+ { a: 0n, b: 1000n, idx: 999, steps: 1000, result: 1000n },
+
+ // [1000n, 0n] cases
+ { a: 1000n, b: 0n, idx: 0, steps: 1, result: 1000n },
+ { a: 1000n, b: 0n, idx: 0, steps: 2, result: 1000n },
+ { a: 1000n, b: 0n, idx: 1, steps: 2, result: 0n },
+ { a: 1000n, b: 0n, idx: 0, steps: 1000, result: 1000n },
+ { a: 1000n, b: 0n, idx: 500, steps: 1000, result: 500n },
+ { a: 1000n, b: 0n, idx: 999, steps: 1000, result: 0n },
+
+ // [0n, -1000n] cases
+ { a: 0n, b: -1000n, idx: 0, steps: 1, result: 0n },
+ { a: 0n, b: -1000n, idx: 0, steps: 2, result: 0n },
+ { a: 0n, b: -1000n, idx: 1, steps: 2, result: -1000n },
+ { a: 0n, b: -1000n, idx: 0, steps: 1000, result: 0n },
+ { a: 0n, b: -1000n, idx: 500, steps: 1000, result: -500n },
+ { a: 0n, b: -1000n, idx: 999, steps: 1000, result: -1000n },
+
+ // [-1000n, 0n] cases
+ { a: -1000n, b: 0n, idx: 0, steps: 1, result: -1000n },
+ { a: -1000n, b: 0n, idx: 0, steps: 2, result: -1000n },
+ { a: -1000n, b: 0n, idx: 1, steps: 2, result: 0n },
+ { a: -1000n, b: 0n, idx: 0, steps: 1000, result: -1000n },
+ { a: -1000n, b: 0n, idx: 500, steps: 1000, result: -500n },
+ { a: -1000n, b: 0n, idx: 999, steps: 1000, result: 0n },
+
+ // [100n, 1000n] cases
+ { a: 100n, b: 1000n, idx: 0, steps: 1, result: 100n },
+ { a: 100n, b: 1000n, idx: 0, steps: 2, result: 100n },
+ { a: 100n, b: 1000n, idx: 1, steps: 2, result: 1000n },
+ { a: 100n, b: 1000n, idx: 0, steps: 9, result: 100n },
+ { a: 100n, b: 1000n, idx: 4, steps: 9, result: 550n },
+ { a: 100n, b: 1000n, idx: 8, steps: 9, result: 1000n },
+
+ // [1000n, 100n] cases
+ { a: 1000n, b: 100n, idx: 0, steps: 1, result: 1000n },
+ { a: 1000n, b: 100n, idx: 0, steps: 2, result: 1000n },
+ { a: 1000n, b: 100n, idx: 1, steps: 2, result: 100n },
+ { a: 1000n, b: 100n, idx: 0, steps: 9, result: 1000n },
+ { a: 1000n, b: 100n, idx: 4, steps: 9, result: 550n },
+ { a: 1000n, b: 100n, idx: 8, steps: 9, result: 100n },
+
+ // [01000n, 1000n] cases
+ { a: -1000n, b: 1000n, idx: 0, steps: 1, result: -1000n },
+ { a: -1000n, b: 1000n, idx: 0, steps: 2, result: -1000n },
+ { a: -1000n, b: 1000n, idx: 1, steps: 2, result: 1000n },
+ { a: -1000n, b: 1000n, idx: 0, steps: 9, result: -1000n },
+ { a: -1000n, b: 1000n, idx: 4, steps: 9, result: 0n },
+ { a: -1000n, b: 1000n, idx: 8, steps: 9, result: 1000n },
+ ])
+ .fn(test => {
+ const a = test.params.a;
+ const b = test.params.b;
+ const idx = test.params.idx;
+ const steps = test.params.steps;
+ const got = lerpBigInt(a, b, idx, steps);
+ const expect = test.params.result;
+
+ test.expect(
+ got === expect,
+ `lerpBigInt(${a}, ${b}, ${idx}, ${steps}) returned ${got}. Expected ${expect}`
+ );
+ });
+
+interface rangeCase {
+ a: number;
+ b: number;
+ num_steps: number;
+ result: number[];
+}
+
+g.test('linearRange')
+ .paramsSimple<rangeCase>(
+ // prettier-ignore
+ [
+ { a: 0.0, b: Number.POSITIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: Number.POSITIVE_INFINITY, b: 0.0, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: Number.NEGATIVE_INFINITY, b: 1.0, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: 1.0, b: Number.NEGATIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: Number.NEGATIVE_INFINITY, b: Number.POSITIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: Number.POSITIVE_INFINITY, b: Number.NEGATIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: 0.0, b: 0.0, num_steps: 10, result: new Array<number>(10).fill(0.0) },
+ { a: 10.0, b: 10.0, num_steps: 10, result: new Array<number>(10).fill(10.0) },
+ { a: 0.0, b: 10.0, num_steps: 1, result: [0.0] },
+ { a: 10.0, b: 0.0, num_steps: 1, result: [10] },
+ { a: 0.0, b: 10.0, num_steps: 11, result: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] },
+ { a: 10.0, b: 0.0, num_steps: 11, result: [10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0] },
+ { a: 0.0, b: 1000.0, num_steps: 11, result: [0.0, 100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0] },
+ { a: 1000.0, b: 0.0, num_steps: 11, result: [1000.0, 900.0, 800.0, 700.0, 600.0, 500.0, 400.0, 300.0, 200.0, 100.0, 0.0] },
+ { a: 1.0, b: 5.0, num_steps: 5, result: [1.0, 2.0, 3.0, 4.0, 5.0] },
+ { a: 5.0, b: 1.0, num_steps: 5, result: [5.0, 4.0, 3.0, 2.0, 1.0] },
+ { a: 0.0, b: 1.0, num_steps: 11, result: [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] },
+ { a: 1.0, b: 0.0, num_steps: 11, result: [1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0] },
+ { a: 0.0, b: 1.0, num_steps: 5, result: [0.0, 0.25, 0.5, 0.75, 1.0] },
+ { a: 1.0, b: 0.0, num_steps: 5, result: [1.0, 0.75, 0.5, 0.25, 0.0] },
+ { a: -1.0, b: 1.0, num_steps: 11, result: [-1.0, -0.8, -0.6, -0.4, -0.2, 0.0, 0.2, 0.4, 0.6, 0.8, 1.0] },
+ { a: 1.0, b: -1.0, num_steps: 11, result: [1.0, 0.8, 0.6, 0.4, 0.2, 0.0, -0.2, -0.4, -0.6, -0.8, -1.0] },
+ { a: -1.0, b: 0, num_steps: 11, result: [-1.0, -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1, 0.0] },
+ { a: 0.0, b: -1.0, num_steps: 11, result: [0.0, -0.1, -0.2, -0.3, -0.4, -0.5, -0.6, -0.7, -0.8, -0.9, -1.0] },
+ ]
+ )
+ .fn(test => {
+ const a = test.params.a;
+ const b = test.params.b;
+ const num_steps = test.params.num_steps;
+ const got = linearRange(a, b, num_steps);
+ const expect = test.params.result;
+
+ test.expect(
+ compareArrayOfNumbersF32(got, expect, 'no-flush'),
+ `linearRange(${a}, ${b}, ${num_steps}) returned ${got}. Expected ${expect}`
+ );
+ });
+
+g.test('biasedRange')
+ .paramsSimple<rangeCase>(
+ // prettier-ignore
+ [
+ { a: 0.0, b: Number.POSITIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: Number.POSITIVE_INFINITY, b: 0.0, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: Number.NEGATIVE_INFINITY, b: 1.0, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: 1.0, b: Number.NEGATIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: Number.NEGATIVE_INFINITY, b: Number.POSITIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: Number.POSITIVE_INFINITY, b: Number.NEGATIVE_INFINITY, num_steps: 10, result: new Array<number>(10).fill(Number.NaN) },
+ { a: 0.0, b: 0.0, num_steps: 10, result: new Array<number>(10).fill(0.0) },
+ { a: 10.0, b: 10.0, num_steps: 10, result: new Array<number>(10).fill(10.0) },
+ { a: 0.0, b: 10.0, num_steps: 1, result: [0.0] },
+ { a: 10.0, b: 0.0, num_steps: 1, result: [10.0] },
+ { a: 0.0, b: 10.0, num_steps: 11, result: [0.0, 0.1, 0.4, 0.9, 1.6, 2.5, 3.6, 4.9, 6.4, 8.1, 10.0] },
+ { a: 10.0, b: 0.0, num_steps: 11, result: [10.0, 9.9, 9.6, 9.1, 8.4, 7.5, 6.4, 5.1, 3.6, 1.9, 0.0] },
+ { a: 0.0, b: 1000.0, num_steps: 11, result: [0.0, 10.0, 40.0, 90.0, 160.0, 250.0, 360.0, 490.0, 640.0, 810.0, 1000.0] },
+ { a: 1000.0, b: 0.0, num_steps: 11, result: [1000.0, 990.0, 960.0, 910.0, 840.0, 750.0, 640.0, 510.0, 360.0, 190.0, 0.0] },
+ { a: 1.0, b: 5.0, num_steps: 5, result: [1.0, 1.25, 2.0, 3.25, 5.0] },
+ { a: 5.0, b: 1.0, num_steps: 5, result: [5.0, 4.75, 4.0, 2.75, 1.0] },
+ { a: 0.0, b: 1.0, num_steps: 11, result: [0.0, 0.01, 0.04, 0.09, 0.16, 0.25, 0.36, 0.49, 0.64, 0.81, 1.0] },
+ { a: 1.0, b: 0.0, num_steps: 11, result: [1.0, 0.99, 0.96, 0.91, 0.84, 0.75, 0.64, 0.51, 0.36, 0.19, 0.0] },
+ { a: 0.0, b: 1.0, num_steps: 5, result: [0.0, 0.0625, 0.25, 0.5625, 1.0] },
+ { a: 1.0, b: 0.0, num_steps: 5, result: [1.0, 0.9375, 0.75, 0.4375, 0.0] },
+ { a: -1.0, b: 1.0, num_steps: 11, result: [-1.0, -0.98, -0.92, -0.82, -0.68, -0.5, -0.28 ,-0.02, 0.28, 0.62, 1.0] },
+ { a: 1.0, b: -1.0, num_steps: 11, result: [1.0, 0.98, 0.92, 0.82, 0.68, 0.5, 0.28 ,0.02, -0.28, -0.62, -1.0] },
+ { a: -1.0, b: 0, num_steps: 11, result: [-1.0 , -0.99, -0.96, -0.91, -0.84, -0.75, -0.64, -0.51, -0.36, -0.19, 0.0] },
+ { a: 0.0, b: -1.0, num_steps: 11, result: [0.0, -0.01, -0.04, -0.09, -0.16, -0.25, -0.36, -0.49, -0.64, -0.81, -1.0] },
+ ]
+ )
+ .fn(test => {
+ const a = test.params.a;
+ const b = test.params.b;
+ const num_steps = test.params.num_steps;
+ const got = biasedRange(a, b, num_steps);
+ const expect = test.params.result;
+
+ test.expect(
+ compareArrayOfNumbersF32(got, expect, 'no-flush'),
+ `biasedRange(${a}, ${b}, ${num_steps}) returned ${got}. Expected ${expect}`
+ );
+ });
+
+interface rangeBigIntCase {
+ a: bigint;
+ b: bigint;
+ num_steps: number;
+ result: bigint[];
+}
+
+g.test('linearRangeBigInt')
+ .paramsSimple<rangeBigIntCase>(
+ // prettier-ignore
+ [
+ { a: 0n, b: 0n, num_steps: 10, result: new Array<bigint>(10).fill(0n) },
+ { a: 10n, b: 10n, num_steps: 10, result: new Array<bigint>(10).fill(10n) },
+ { a: 0n, b: 10n, num_steps: 1, result: [0n] },
+ { a: 10n, b: 0n, num_steps: 1, result: [10n] },
+ { a: 0n, b: 10n, num_steps: 11, result: [0n, 1n, 2n, 3n, 4n, 5n, 6n, 7n, 8n, 9n, 10n] },
+ { a: 10n, b: 0n, num_steps: 11, result: [10n, 9n, 8n, 7n, 6n, 5n, 4n, 3n, 2n, 1n, 0n] },
+ { a: 0n, b: 1000n, num_steps: 11, result: [0n, 100n, 200n, 300n, 400n, 500n, 600n, 700n, 800n, 900n, 1000n] },
+ { a: 1000n, b: 0n, num_steps: 11, result: [1000n, 900n, 800n, 700n, 600n, 500n, 400n, 300n, 200n, 100n, 0n] },
+ { a: 1n, b: 5n, num_steps: 5, result: [1n, 2n, 3n, 4n, 5n] },
+ { a: 5n, b: 1n, num_steps: 5, result: [5n, 4n, 3n, 2n, 1n] },
+ { a: 0n, b: 10n, num_steps: 5, result: [0n, 2n, 5n, 7n, 10n] },
+ { a: 10n, b: 0n, num_steps: 5, result: [10n, 8n, 5n, 3n, 0n] },
+ { a: -10n, b: 10n, num_steps: 11, result: [-10n, -8n, -6n, -4n, -2n, 0n, 2n, 4n, 6n, 8n, 10n] },
+ { a: 10n, b: -10n, num_steps: 11, result: [10n, 8n, 6n, 4n, 2n, 0n, -2n, -4n, -6n, -8n, -10n] },
+ { a: -10n, b: 0n, num_steps: 11, result: [-10n, -9n, -8n, -7n, -6n, -5n, -4n, -3n, -2n, -1n, 0n] },
+ { a: 0n, b: -10n, num_steps: 11, result: [0n, -1n, -2n, -3n, -4n, -5n, -6n, -7n, -8n, -9n, -10n] },
+ ]
+ )
+ .fn(test => {
+ const a = test.params.a;
+ const b = test.params.b;
+ const num_steps = test.params.num_steps;
+ const got = linearRangeBigInt(a, b, num_steps);
+ const expect = test.params.result;
+
+ test.expect(
+ objectEquals(got, expect),
+ `linearRangeBigInt(${a}, ${b}, ${num_steps}) returned ${got}. Expected ${expect}`
+ );
+ });
+
+interface fullF32RangeCase {
+ neg_norm: number;
+ neg_sub: number;
+ pos_sub: number;
+ pos_norm: number;
+ expect: Array<number>;
+}
+
+g.test('fullF32Range')
+ .paramsSimple<fullF32RangeCase>(
+ // prettier-ignore
+ [
+ { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ -0.0, 0.0 ] },
+ { neg_norm: 1, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f32.negative.min, -0.0, 0.0] },
+ { neg_norm: 2, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f32.negative.min, kValue.f32.negative.max, -0.0, 0.0 ] },
+ { neg_norm: 3, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f32.negative.min, -1.9999998807907104, kValue.f32.negative.max, -0.0, 0.0 ] },
+ { neg_norm: 0, neg_sub: 1, pos_sub: 0, pos_norm: 0, expect: [ kValue.f32.negative.subnormal.min, -0.0, 0.0 ] },
+ { neg_norm: 0, neg_sub: 2, pos_sub: 0, pos_norm: 0, expect: [ kValue.f32.negative.subnormal.min, kValue.f32.negative.subnormal.max, -0.0, 0.0 ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 1, pos_norm: 0, expect: [ -0.0, 0.0, kValue.f32.positive.subnormal.min ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 2, pos_norm: 0, expect: [ -0.0, 0.0, kValue.f32.positive.subnormal.min, kValue.f32.positive.subnormal.max ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 1, expect: [ -0.0, 0.0, kValue.f32.positive.min ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 2, expect: [ -0.0, 0.0, kValue.f32.positive.min, kValue.f32.positive.max ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 3, expect: [ -0.0, 0.0, kValue.f32.positive.min, 1.9999998807907104, kValue.f32.positive.max ] },
+ { neg_norm: 1, neg_sub: 1, pos_sub: 1, pos_norm: 1, expect: [ kValue.f32.negative.min, kValue.f32.negative.subnormal.min, -0.0, 0.0, kValue.f32.positive.subnormal.min, kValue.f32.positive.min ] },
+ { neg_norm: 2, neg_sub: 2, pos_sub: 2, pos_norm: 2, expect: [ kValue.f32.negative.min, kValue.f32.negative.max, kValue.f32.negative.subnormal.min, kValue.f32.negative.subnormal.max, -0.0, 0.0, kValue.f32.positive.subnormal.min, kValue.f32.positive.subnormal.max, kValue.f32.positive.min, kValue.f32.positive.max ] },
+ ]
+ )
+ .fn(test => {
+ const neg_norm = test.params.neg_norm;
+ const neg_sub = test.params.neg_sub;
+ const pos_sub = test.params.pos_sub;
+ const pos_norm = test.params.pos_norm;
+ const got = fullF32Range({ neg_norm, neg_sub, pos_sub, pos_norm });
+ const expect = test.params.expect;
+
+ test.expect(
+ compareArrayOfNumbersF32(got, expect, 'no-flush'),
+ `fullF32Range(${neg_norm}, ${neg_sub}, ${pos_sub}, ${pos_norm}) returned [${got}]. Expected [${expect}]`
+ );
+ });
+
+interface fullF16RangeCase {
+ neg_norm: number;
+ neg_sub: number;
+ pos_sub: number;
+ pos_norm: number;
+ expect: Array<number>;
+}
+
+g.test('fullF16Range')
+ .paramsSimple<fullF16RangeCase>(
+ // prettier-ignore
+ [
+ { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ -0.0, 0.0 ] },
+ { neg_norm: 1, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f16.negative.min, -0.0, 0.0] },
+ { neg_norm: 2, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f16.negative.min, kValue.f16.negative.max, -0.0, 0.0 ] },
+ { neg_norm: 3, neg_sub: 0, pos_sub: 0, pos_norm: 0, expect: [ kValue.f16.negative.min, -1.9990234375, kValue.f16.negative.max, -0.0, 0.0 ] },
+ { neg_norm: 0, neg_sub: 1, pos_sub: 0, pos_norm: 0, expect: [ kValue.f16.negative.subnormal.min, -0.0, 0.0 ] },
+ { neg_norm: 0, neg_sub: 2, pos_sub: 0, pos_norm: 0, expect: [ kValue.f16.negative.subnormal.min, kValue.f16.negative.subnormal.max, -0.0, 0.0 ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 1, pos_norm: 0, expect: [ -0.0, 0.0, kValue.f16.positive.subnormal.min ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 2, pos_norm: 0, expect: [ -0.0, 0.0, kValue.f16.positive.subnormal.min, kValue.f16.positive.subnormal.max ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 1, expect: [ -0.0, 0.0, kValue.f16.positive.min ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 2, expect: [ -0.0, 0.0, kValue.f16.positive.min, kValue.f16.positive.max ] },
+ { neg_norm: 0, neg_sub: 0, pos_sub: 0, pos_norm: 3, expect: [ -0.0, 0.0, kValue.f16.positive.min, 1.9990234375, kValue.f16.positive.max ] },
+ { neg_norm: 1, neg_sub: 1, pos_sub: 1, pos_norm: 1, expect: [ kValue.f16.negative.min, kValue.f16.negative.subnormal.min, -0.0, 0.0, kValue.f16.positive.subnormal.min, kValue.f16.positive.min ] },
+ { neg_norm: 2, neg_sub: 2, pos_sub: 2, pos_norm: 2, expect: [ kValue.f16.negative.min, kValue.f16.negative.max, kValue.f16.negative.subnormal.min, kValue.f16.negative.subnormal.max, -0.0, 0.0, kValue.f16.positive.subnormal.min, kValue.f16.positive.subnormal.max, kValue.f16.positive.min, kValue.f16.positive.max ] },
+ ]
+ )
+ .fn(test => {
+ const neg_norm = test.params.neg_norm;
+ const neg_sub = test.params.neg_sub;
+ const pos_sub = test.params.pos_sub;
+ const pos_norm = test.params.pos_norm;
+ const got = fullF16Range({ neg_norm, neg_sub, pos_sub, pos_norm });
+ const expect = test.params.expect;
+
+ test.expect(
+ compareArrayOfNumbersF32(got, expect),
+ `fullF16Range(${neg_norm}, ${neg_sub}, ${pos_sub}, ${pos_norm}) returned [${got}]. Expected [${expect}]`
+ );
+ });
+
+interface fullI32RangeCase {
+ neg_count: number;
+ pos_count: number;
+ expect: Array<number>;
+}
+
+g.test('fullI32Range')
+ .paramsSimple<fullI32RangeCase>(
+ // prettier-ignore
+ [
+ { neg_count: 0, pos_count: 0, expect: [0] },
+ { neg_count: 1, pos_count: 0, expect: [kValue.i32.negative.min, 0] },
+ { neg_count: 2, pos_count: 0, expect: [kValue.i32.negative.min, -1, 0] },
+ { neg_count: 3, pos_count: 0, expect: [kValue.i32.negative.min, -1610612736, -1, 0] },
+ { neg_count: 0, pos_count: 1, expect: [0, 1] },
+ { neg_count: 0, pos_count: 2, expect: [0, 1, kValue.i32.positive.max] },
+ { neg_count: 0, pos_count: 3, expect: [0, 1, 536870912, kValue.i32.positive.max] },
+ { neg_count: 1, pos_count: 1, expect: [kValue.i32.negative.min, 0, 1] },
+ { neg_count: 2, pos_count: 2, expect: [kValue.i32.negative.min, -1, 0, 1, kValue.i32.positive.max ] },
+ ]
+ )
+ .fn(test => {
+ const neg_count = test.params.neg_count;
+ const pos_count = test.params.pos_count;
+ const got = fullI32Range({ negative: neg_count, positive: pos_count });
+ const expect = test.params.expect;
+
+ test.expect(
+ compareArrayOfNumbersF32(got, expect),
+ `fullI32Range(${neg_count}, ${pos_count}) returned [${got}]. Expected [${expect}]`
+ );
+ });
+
+interface limitsBigIntBitsF64Case {
+ bits: bigint;
+ value: number;
+}
+
+// Test to confirm kBit and kValue constants are equivalent for f64
+g.test('f64LimitsEquivalency')
+ .paramsSimple<limitsBigIntBitsF64Case>([
+ { bits: kBit.f64.positive.max, value: kValue.f64.positive.max },
+ { bits: kBit.f64.positive.min, value: kValue.f64.positive.min },
+ { bits: kBit.f64.positive.nearest_max, value: kValue.f64.positive.nearest_max },
+ { bits: kBit.f64.positive.less_than_one, value: kValue.f64.positive.less_than_one },
+ { bits: kBit.f64.positive.pi.whole, value: kValue.f64.positive.pi.whole },
+ { bits: kBit.f64.positive.pi.three_quarters, value: kValue.f64.positive.pi.three_quarters },
+ { bits: kBit.f64.positive.pi.half, value: kValue.f64.positive.pi.half },
+ { bits: kBit.f64.positive.pi.third, value: kValue.f64.positive.pi.third },
+ { bits: kBit.f64.positive.pi.quarter, value: kValue.f64.positive.pi.quarter },
+ { bits: kBit.f64.positive.pi.sixth, value: kValue.f64.positive.pi.sixth },
+ { bits: kBit.f64.positive.e, value: kValue.f64.positive.e },
+ { bits: kBit.f64.max_ulp, value: kValue.f64.max_ulp },
+ { bits: kBit.f64.negative.max, value: kValue.f64.negative.max },
+ { bits: kBit.f64.negative.min, value: kValue.f64.negative.min },
+ { bits: kBit.f64.negative.nearest_min, value: kValue.f64.negative.nearest_min },
+ { bits: kBit.f64.negative.pi.whole, value: kValue.f64.negative.pi.whole },
+ { bits: kBit.f64.negative.pi.three_quarters, value: kValue.f64.negative.pi.three_quarters },
+ { bits: kBit.f64.negative.pi.half, value: kValue.f64.negative.pi.half },
+ { bits: kBit.f64.negative.pi.third, value: kValue.f64.negative.pi.third },
+ { bits: kBit.f64.negative.pi.quarter, value: kValue.f64.negative.pi.quarter },
+ { bits: kBit.f64.negative.pi.sixth, value: kValue.f64.negative.pi.sixth },
+ { bits: kBit.f64.positive.subnormal.max, value: kValue.f64.positive.subnormal.max },
+ { bits: kBit.f64.positive.subnormal.min, value: kValue.f64.positive.subnormal.min },
+ { bits: kBit.f64.negative.subnormal.max, value: kValue.f64.negative.subnormal.max },
+ { bits: kBit.f64.negative.subnormal.min, value: kValue.f64.negative.subnormal.min },
+ { bits: kBit.f64.positive.infinity, value: kValue.f64.positive.infinity },
+ { bits: kBit.f64.negative.infinity, value: kValue.f64.negative.infinity },
+ ])
+ .fn(test => {
+ const bits = test.params.bits;
+ const value = test.params.value;
+
+ const val_to_bits = bits === float64ToUint64(value);
+ const bits_to_val = value === uint64ToFloat64(bits);
+ test.expect(
+ val_to_bits && bits_to_val,
+ `bits = ${bits}, value = ${value}, returned val_to_bits as ${val_to_bits}, and bits_to_val as ${bits_to_val}, they are expected to be equivalent`
+ );
+ });
+
+interface limitsNumberBitsCase {
+ bits: number;
+ value: number;
+}
+
+// Test to confirm kBit and kValue constants are equivalent for f32
+g.test('f32LimitsEquivalency')
+ .paramsSimple<limitsNumberBitsCase>([
+ { bits: kBit.f32.positive.max, value: kValue.f32.positive.max },
+ { bits: kBit.f32.positive.min, value: kValue.f32.positive.min },
+ { bits: kBit.f32.positive.nearest_max, value: kValue.f32.positive.nearest_max },
+ { bits: kBit.f32.positive.less_than_one, value: kValue.f32.positive.less_than_one },
+ { bits: kBit.f32.positive.pi.whole, value: kValue.f32.positive.pi.whole },
+ { bits: kBit.f32.positive.pi.three_quarters, value: kValue.f32.positive.pi.three_quarters },
+ { bits: kBit.f32.positive.pi.half, value: kValue.f32.positive.pi.half },
+ { bits: kBit.f32.positive.pi.third, value: kValue.f32.positive.pi.third },
+ { bits: kBit.f32.positive.pi.quarter, value: kValue.f32.positive.pi.quarter },
+ { bits: kBit.f32.positive.pi.sixth, value: kValue.f32.positive.pi.sixth },
+ { bits: kBit.f32.positive.e, value: kValue.f32.positive.e },
+ { bits: kBit.f32.max_ulp, value: kValue.f32.max_ulp },
+ { bits: kBit.f32.negative.max, value: kValue.f32.negative.max },
+ { bits: kBit.f32.negative.min, value: kValue.f32.negative.min },
+ { bits: kBit.f32.negative.nearest_min, value: kValue.f32.negative.nearest_min },
+ { bits: kBit.f32.negative.pi.whole, value: kValue.f32.negative.pi.whole },
+ { bits: kBit.f32.negative.pi.three_quarters, value: kValue.f32.negative.pi.three_quarters },
+ { bits: kBit.f32.negative.pi.half, value: kValue.f32.negative.pi.half },
+ { bits: kBit.f32.negative.pi.third, value: kValue.f32.negative.pi.third },
+ { bits: kBit.f32.negative.pi.quarter, value: kValue.f32.negative.pi.quarter },
+ { bits: kBit.f32.negative.pi.sixth, value: kValue.f32.negative.pi.sixth },
+ { bits: kBit.f32.positive.subnormal.max, value: kValue.f32.positive.subnormal.max },
+ { bits: kBit.f32.positive.subnormal.min, value: kValue.f32.positive.subnormal.min },
+ { bits: kBit.f32.negative.subnormal.max, value: kValue.f32.negative.subnormal.max },
+ { bits: kBit.f32.negative.subnormal.min, value: kValue.f32.negative.subnormal.min },
+ { bits: kBit.f32.positive.infinity, value: kValue.f32.positive.infinity },
+ { bits: kBit.f32.negative.infinity, value: kValue.f32.negative.infinity },
+ ])
+ .fn(test => {
+ const bits = test.params.bits;
+ const value = test.params.value;
+
+ const val_to_bits = bits === float32ToUint32(value);
+ const bits_to_val = value === uint32ToFloat32(bits);
+ test.expect(
+ val_to_bits && bits_to_val,
+ `bits = ${bits}, value = ${value}, returned val_to_bits as ${val_to_bits}, and bits_to_val as ${bits_to_val}, they are expected to be equivalent`
+ );
+ });
+
+// Test to confirm kBit and kValue constants are equivalent for f16
+g.test('f16LimitsEquivalency')
+ .paramsSimple<limitsNumberBitsCase>([
+ { bits: kBit.f16.positive.max, value: kValue.f16.positive.max },
+ { bits: kBit.f16.positive.min, value: kValue.f16.positive.min },
+ { bits: kBit.f16.positive.nearest_max, value: kValue.f16.positive.nearest_max },
+ { bits: kBit.f16.positive.less_than_one, value: kValue.f16.positive.less_than_one },
+ { bits: kBit.f16.positive.pi.whole, value: kValue.f16.positive.pi.whole },
+ { bits: kBit.f16.positive.pi.three_quarters, value: kValue.f16.positive.pi.three_quarters },
+ { bits: kBit.f16.positive.pi.half, value: kValue.f16.positive.pi.half },
+ { bits: kBit.f16.positive.pi.third, value: kValue.f16.positive.pi.third },
+ { bits: kBit.f16.positive.pi.quarter, value: kValue.f16.positive.pi.quarter },
+ { bits: kBit.f16.positive.pi.sixth, value: kValue.f16.positive.pi.sixth },
+ { bits: kBit.f16.positive.e, value: kValue.f16.positive.e },
+ { bits: kBit.f16.max_ulp, value: kValue.f16.max_ulp },
+ { bits: kBit.f16.negative.max, value: kValue.f16.negative.max },
+ { bits: kBit.f16.negative.min, value: kValue.f16.negative.min },
+ { bits: kBit.f16.negative.nearest_min, value: kValue.f16.negative.nearest_min },
+ { bits: kBit.f16.negative.pi.whole, value: kValue.f16.negative.pi.whole },
+ { bits: kBit.f16.negative.pi.three_quarters, value: kValue.f16.negative.pi.three_quarters },
+ { bits: kBit.f16.negative.pi.half, value: kValue.f16.negative.pi.half },
+ { bits: kBit.f16.negative.pi.third, value: kValue.f16.negative.pi.third },
+ { bits: kBit.f16.negative.pi.quarter, value: kValue.f16.negative.pi.quarter },
+ { bits: kBit.f16.negative.pi.sixth, value: kValue.f16.negative.pi.sixth },
+ { bits: kBit.f16.positive.subnormal.max, value: kValue.f16.positive.subnormal.max },
+ { bits: kBit.f16.positive.subnormal.min, value: kValue.f16.positive.subnormal.min },
+ { bits: kBit.f16.negative.subnormal.max, value: kValue.f16.negative.subnormal.max },
+ { bits: kBit.f16.negative.subnormal.min, value: kValue.f16.negative.subnormal.min },
+ { bits: kBit.f16.positive.infinity, value: kValue.f16.positive.infinity },
+ { bits: kBit.f16.negative.infinity, value: kValue.f16.negative.infinity },
+ ])
+ .fn(test => {
+ const bits = test.params.bits;
+ const value = test.params.value;
+
+ const val_to_bits = bits === float16ToUint16(value);
+ const bits_to_val = value === uint16ToFloat16(bits);
+ test.expect(
+ val_to_bits && bits_to_val,
+ `bits = ${bits}, value = ${value}, returned val_to_bits as ${val_to_bits}, and bits_to_val as ${bits_to_val}, they are expected to be equivalent`
+ );
+ });
+
+interface cartesianProductCase<T> {
+ inputs: T[][];
+ result: T[][];
+}
+
+g.test('cartesianProductNumber')
+ .paramsSimple<cartesianProductCase<number>>(
+ // prettier-ignore
+ [
+ { inputs: [[0], [1]], result: [[0, 1]] },
+ { inputs: [[0, 1], [2]], result: [[0, 2],
+ [1, 2]] },
+ { inputs: [[0], [1, 2]], result: [[0, 1],
+ [0, 2]] },
+ { inputs: [[0, 1], [2, 3]], result: [[0,2],
+ [1, 2],
+ [0, 3],
+ [1, 3]] },
+ { inputs: [[0, 1, 2], [3, 4, 5]], result: [[0, 3],
+ [1, 3],
+ [2, 3],
+ [0, 4],
+ [1, 4],
+ [2, 4],
+ [0, 5],
+ [1, 5],
+ [2, 5]] },
+ { inputs: [[0, 1], [2, 3], [4, 5]], result: [[0, 2, 4],
+ [1, 2, 4],
+ [0, 3, 4],
+ [1, 3, 4],
+ [0, 2, 5],
+ [1, 2, 5],
+ [0, 3, 5],
+ [1, 3, 5]] },
+
+ ]
+ )
+ .fn(test => {
+ const inputs = test.params.inputs;
+ const got = cartesianProduct(...inputs);
+ const expect = test.params.result;
+
+ test.expect(
+ objectEquals(got, expect),
+ `cartesianProduct(${JSON.stringify(inputs)}) returned ${JSON.stringify(
+ got
+ )}. Expected ${JSON.stringify(expect)} `
+ );
+ });
+
+g.test('cartesianProductArray')
+ .paramsSimple<cartesianProductCase<number[]>>(
+ // prettier-ignore
+ [
+ { inputs: [[[0, 1], [2, 3]], [[4, 5], [6, 7]]], result: [[[0, 1], [4, 5]],
+ [[2, 3], [4, 5]],
+ [[0, 1], [6, 7]],
+ [[2, 3], [6, 7]]]},
+ { inputs: [[[0, 1], [2, 3]], [[4, 5], [6, 7]], [[8, 9]]], result: [[[0, 1], [4, 5], [8, 9]],
+ [[2, 3], [4, 5], [8, 9]],
+ [[0, 1], [6, 7], [8, 9]],
+ [[2, 3], [6, 7], [8, 9]]]},
+ { inputs: [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[2, 1, 0], [5, 4, 3], [8, 7, 6]]], result: [[[0, 1, 2], [2, 1, 0]],
+ [[3, 4, 5], [2, 1, 0]],
+ [[6, 7, 8], [2, 1, 0]],
+ [[0, 1, 2], [5, 4, 3]],
+ [[3, 4, 5], [5, 4, 3]],
+ [[6, 7, 8], [5, 4, 3]],
+ [[0, 1, 2], [8, 7, 6]],
+ [[3, 4, 5], [8, 7, 6]],
+ [[6, 7, 8], [8, 7, 6]]]}
+
+ ]
+ )
+ .fn(test => {
+ const inputs = test.params.inputs;
+ const got = cartesianProduct(...inputs);
+ const expect = test.params.result;
+
+ test.expect(
+ objectEquals(got, expect),
+ `cartesianProduct(${JSON.stringify(inputs)}) returned ${JSON.stringify(
+ got
+ )}. Expected ${JSON.stringify(expect)} `
+ );
+ });
+
+interface calculatePermutationsCase<T> {
+ input: T[];
+ result: T[][];
+}
+
+g.test('calculatePermutations')
+ .paramsSimple<calculatePermutationsCase<number>>(
+ // prettier-ignore
+ [
+ { input: [0, 1], result: [[0, 1],
+ [1, 0]] },
+ { input: [0, 1, 2], result: [[0, 1, 2],
+ [0, 2, 1],
+ [1, 0, 2],
+ [1, 2, 0],
+ [2, 0, 1],
+ [2, 1, 0]] },
+ { input: [0, 1, 2, 3], result: [[0, 1, 2, 3],
+ [0, 1, 3, 2],
+ [0, 2, 1, 3],
+ [0, 2, 3, 1],
+ [0, 3, 1, 2],
+ [0, 3, 2, 1],
+ [1, 0, 2, 3],
+ [1, 0, 3, 2],
+ [1, 2, 0, 3],
+ [1, 2, 3, 0],
+ [1, 3, 0, 2],
+ [1, 3, 2, 0],
+ [2, 0, 1, 3],
+ [2, 0, 3, 1],
+ [2, 1, 0, 3],
+ [2, 1, 3, 0],
+ [2, 3, 0, 1],
+ [2, 3, 1, 0],
+ [3, 0, 1, 2],
+ [3, 0, 2, 1],
+ [3, 1, 0, 2],
+ [3, 1, 2, 0],
+ [3, 2, 0, 1],
+ [3, 2, 1, 0]] },
+ ]
+ )
+ .fn(test => {
+ const input = test.params.input;
+ const got = calculatePermutations(input);
+ const expect = test.params.result;
+
+ test.expect(
+ objectEquals(got, expect),
+ `calculatePermutations(${JSON.stringify(input)}) returned ${JSON.stringify(
+ got
+ )}. Expected ${JSON.stringify(expect)} `
+ );
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_and_utils.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_and_utils.spec.ts
new file mode 100644
index 0000000000..47e2eb335f
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_and_utils.spec.ts
@@ -0,0 +1,549 @@
+export const description = `
+Unit tests for parameterization helpers.
+`;
+
+import { TestParams } from '../common/framework/fixture.js';
+import {
+ kUnitCaseParamsBuilder,
+ CaseSubcaseIterable,
+ ParamsBuilderBase,
+ builderIterateCasesWithSubcases,
+} from '../common/framework/params_builder.js';
+import { makeTestGroup } from '../common/framework/test_group.js';
+import {
+ mergeParams,
+ mergeParamsChecked,
+ publicParamsEquals,
+} from '../common/internal/params_utils.js';
+import { assert, objectEquals } from '../common/util/util.js';
+
+import { UnitTest } from './unit_test.js';
+
+class ParamsTest extends UnitTest {
+ expectParams<CaseP extends {}, SubcaseP extends {}>(
+ act: ParamsBuilderBase<CaseP, SubcaseP>,
+ exp: CaseSubcaseIterable<{}, {}>,
+ caseFilter: TestParams | null = null
+ ): void {
+ const a = Array.from(builderIterateCasesWithSubcases(act, caseFilter)).map(
+ ([caseP, subcases]) => [caseP, subcases ? Array.from(subcases) : undefined]
+ );
+ const e = Array.from(exp);
+ this.expect(
+ objectEquals(a, e),
+ `
+got ${JSON.stringify(a)}
+expected ${JSON.stringify(e)}`
+ );
+ }
+}
+
+export const g = makeTestGroup(ParamsTest);
+
+const u = kUnitCaseParamsBuilder;
+
+g.test('combine').fn(t => {
+ t.expectParams<{ hello: number }, {}>(u.combine('hello', [1, 2, 3]), [
+ [{ hello: 1 }, undefined],
+ [{ hello: 2 }, undefined],
+ [{ hello: 3 }, undefined],
+ ]);
+ t.expectParams<{ hello: number }, {}>(
+ u.combine('hello', [1, 2, 3]),
+ [
+ [{ hello: 1 }, undefined],
+ [{ hello: 2 }, undefined],
+ [{ hello: 3 }, undefined],
+ ],
+ {}
+ );
+ t.expectParams<{ hello: number }, {}>(
+ u.combine('hello', [1, 2, 3]),
+ [[{ hello: 2 }, undefined]],
+ { hello: 2 }
+ );
+ t.expectParams<{ hello: 1 | 2 | 3 }, {}>(u.combine('hello', [1, 2, 3] as const), [
+ [{ hello: 1 }, undefined],
+ [{ hello: 2 }, undefined],
+ [{ hello: 3 }, undefined],
+ ]);
+ t.expectParams<{}, { hello: number }>(u.beginSubcases().combine('hello', [1, 2, 3]), [
+ [{}, [{ hello: 1 }, { hello: 2 }, { hello: 3 }]],
+ ]);
+ t.expectParams<{}, { hello: number }>(
+ u.beginSubcases().combine('hello', [1, 2, 3]),
+ [[{}, [{ hello: 1 }, { hello: 2 }, { hello: 3 }]]],
+ {}
+ );
+ t.expectParams<{}, { hello: number }>(u.beginSubcases().combine('hello', [1, 2, 3]), [], {
+ hello: 2,
+ });
+ t.expectParams<{}, { hello: 1 | 2 | 3 }>(u.beginSubcases().combine('hello', [1, 2, 3] as const), [
+ [{}, [{ hello: 1 }, { hello: 2 }, { hello: 3 }]],
+ ]);
+});
+
+g.test('empty').fn(t => {
+ t.expectParams<{}, {}>(u, [
+ [{}, undefined], //
+ ]);
+ t.expectParams<{}, {}>(u.beginSubcases(), [
+ [{}, [{}]], //
+ ]);
+});
+
+g.test('combine,zeroes_and_ones').fn(t => {
+ t.expectParams<{}, {}>(u.combineWithParams([]).combineWithParams([]), []);
+ t.expectParams<{}, {}>(u.combineWithParams([]).combineWithParams([{}]), []);
+ t.expectParams<{}, {}>(u.combineWithParams([{}]).combineWithParams([]), []);
+ t.expectParams<{}, {}>(u.combineWithParams([{}]).combineWithParams([{}]), [
+ [{}, undefined], //
+ ]);
+
+ t.expectParams<{}, {}>(u.combine('x', []).combine('y', []), []);
+ t.expectParams<{}, {}>(u.combine('x', []).combine('y', [1]), []);
+ t.expectParams<{}, {}>(u.combine('x', [1]).combine('y', []), []);
+ t.expectParams<{}, {}>(u.combine('x', [1]).combine('y', [1]), [
+ [{ x: 1, y: 1 }, undefined], //
+ ]);
+});
+
+g.test('combine,mixed').fn(t => {
+ t.expectParams<{ x: number; y: string; p: number | undefined; q: number | undefined }, {}>(
+ u
+ .combine('x', [1, 2])
+ .combine('y', ['a', 'b'])
+ .combineWithParams([{ p: 4 }, { q: 5 }])
+ .combineWithParams([{}]),
+ [
+ [{ x: 1, y: 'a', p: 4 }, undefined],
+ [{ x: 1, y: 'a', q: 5 }, undefined],
+ [{ x: 1, y: 'b', p: 4 }, undefined],
+ [{ x: 1, y: 'b', q: 5 }, undefined],
+ [{ x: 2, y: 'a', p: 4 }, undefined],
+ [{ x: 2, y: 'a', q: 5 }, undefined],
+ [{ x: 2, y: 'b', p: 4 }, undefined],
+ [{ x: 2, y: 'b', q: 5 }, undefined],
+ ]
+ );
+});
+
+g.test('filter').fn(t => {
+ t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined }, {}>(
+ u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .filter(p => p.a),
+ [
+ [{ a: true, x: 1 }, undefined], //
+ ]
+ );
+
+ t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined }, {}>(
+ u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .beginSubcases()
+ .filter(p => p.a),
+ [
+ [{ a: true, x: 1 }, [{}]], //
+ // Case with no subcases is filtered out.
+ ]
+ );
+
+ t.expectParams<{}, { a: boolean; x: number | undefined; y: number | undefined }>(
+ u
+ .beginSubcases()
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .filter(p => p.a),
+ [
+ [{}, [{ a: true, x: 1 }]], //
+ ]
+ );
+});
+
+g.test('unless').fn(t => {
+ t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined }, {}>(
+ u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .unless(p => p.a),
+ [
+ [{ a: false, y: 2 }, undefined], //
+ ]
+ );
+
+ t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined }, {}>(
+ u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .beginSubcases()
+ .unless(p => p.a),
+ [
+ // Case with no subcases is filtered out.
+ [{ a: false, y: 2 }, [{}]], //
+ ]
+ );
+
+ t.expectParams<{}, { a: boolean; x: number | undefined; y: number | undefined }>(
+ u
+ .beginSubcases()
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .unless(p => p.a),
+ [
+ [{}, [{ a: false, y: 2 }]], //
+ ]
+ );
+});
+
+g.test('expandP').fn(t => {
+ // simple
+ t.expectParams<{}, {}>(
+ u.expandWithParams(function* () {}),
+ []
+ );
+ t.expectParams<{}, {}>(
+ u.expandWithParams(function* () {
+ yield {};
+ }),
+ [[{}, undefined]]
+ );
+ t.expectParams<{ z: number | undefined; w: number | undefined }, {}>(
+ u.expandWithParams(function* () {
+ yield* kUnitCaseParamsBuilder.combine('z', [3, 4]);
+ yield { w: 5 };
+ }),
+ [
+ [{ z: 3 }, undefined],
+ [{ z: 4 }, undefined],
+ [{ w: 5 }, undefined],
+ ]
+ );
+ t.expectParams<{ z: number | undefined; w: number | undefined }, {}>(
+ u.expandWithParams(function* () {
+ yield* kUnitCaseParamsBuilder.combine('z', [3, 4]);
+ yield { w: 5 };
+ }),
+ [
+ [{ z: 3 }, undefined],
+ [{ z: 4 }, undefined],
+ [{ w: 5 }, undefined],
+ ],
+ {}
+ );
+ t.expectParams<{ z: number | undefined; w: number | undefined }, {}>(
+ u.expandWithParams(function* () {
+ yield* kUnitCaseParamsBuilder.combine('z', [3, 4]);
+ yield { w: 5 };
+ }),
+ [[{ z: 4 }, undefined]],
+ { z: 4 }
+ );
+ t.expectParams<{ z: number | undefined; w: number | undefined }, {}>(
+ u.expandWithParams(function* () {
+ yield* kUnitCaseParamsBuilder.combine('z', [3, 4]);
+ yield { w: 5 };
+ }),
+ [[{ z: 3 }, undefined]],
+ { z: 3 }
+ );
+ t.expectParams<{}, { z: number | undefined; w: number | undefined }>(
+ u.beginSubcases().expandWithParams(function* () {
+ yield* kUnitCaseParamsBuilder.combine('z', [3, 4]);
+ yield { w: 5 };
+ }),
+ [[{}, [{ z: 3 }, { z: 4 }, { w: 5 }]]]
+ );
+
+ t.expectParams<{ x: [] | {} }, {}>(
+ u.expand('x', () => [[], {}] as const),
+ [
+ [{ x: [] }, undefined],
+ [{ x: {} }, undefined],
+ ]
+ );
+ t.expectParams<{ x: [] | {} }, {}>(
+ u.expand('x', () => [[], {}] as const),
+ [[{ x: [] }, undefined]],
+ { x: [] }
+ );
+ t.expectParams<{ x: [] | {} }, {}>(
+ u.expand('x', () => [[], {}] as const),
+ [[{ x: {} }, undefined]],
+ { x: {} }
+ );
+
+ // more complex
+ {
+ const p = u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .expandWithParams(function* (p) {
+ if (p.a) {
+ yield { z: 3 };
+ yield { z: 4 };
+ } else {
+ yield { w: 5 };
+ }
+ });
+ type T = {
+ a: boolean;
+ x: number | undefined;
+ y: number | undefined;
+ z: number | undefined;
+ w: number | undefined;
+ };
+ t.expectParams<T, {}>(p, [
+ [{ a: true, x: 1, z: 3 }, undefined],
+ [{ a: true, x: 1, z: 4 }, undefined],
+ [{ a: false, y: 2, w: 5 }, undefined],
+ ]);
+ t.expectParams<T, {}>(
+ p,
+ [
+ [{ a: true, x: 1, z: 3 }, undefined],
+ [{ a: true, x: 1, z: 4 }, undefined],
+ [{ a: false, y: 2, w: 5 }, undefined],
+ ],
+ {}
+ );
+ t.expectParams<T, {}>(
+ p,
+ [
+ [{ a: true, x: 1, z: 3 }, undefined],
+ [{ a: true, x: 1, z: 4 }, undefined],
+ ],
+ { a: true }
+ );
+ t.expectParams<T, {}>(p, [[{ a: false, y: 2, w: 5 }, undefined]], { a: false });
+ }
+
+ t.expectParams<
+ { a: boolean; x: number | undefined; y: number | undefined },
+ { z: number | undefined; w: number | undefined }
+ >(
+ u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .beginSubcases()
+ .expandWithParams(function* (p) {
+ if (p.a) {
+ yield { z: 3 };
+ yield { z: 4 };
+ } else {
+ yield { w: 5 };
+ }
+ }),
+ [
+ [{ a: true, x: 1 }, [{ z: 3 }, { z: 4 }]],
+ [{ a: false, y: 2 }, [{ w: 5 }]],
+ ]
+ );
+});
+
+g.test('expand').fn(t => {
+ // simple
+ t.expectParams<{}, {}>(
+ u.expand('x', function* () {}),
+ []
+ );
+ t.expectParams<{ z: number }, {}>(
+ u.expand('z', function* () {
+ yield 3;
+ yield 4;
+ }),
+ [
+ [{ z: 3 }, undefined],
+ [{ z: 4 }, undefined],
+ ]
+ );
+ t.expectParams<{ z: number }, {}>(
+ u.expand('z', function* () {
+ yield 3;
+ yield 4;
+ }),
+ [
+ [{ z: 3 }, undefined],
+ [{ z: 4 }, undefined],
+ ],
+ {}
+ );
+ t.expectParams<{ z: number }, {}>(
+ u.expand('z', function* () {
+ yield 3;
+ yield 4;
+ }),
+ [[{ z: 3 }, undefined]],
+ { z: 3 }
+ );
+ t.expectParams<{}, { z: number }>(
+ u.beginSubcases().expand('z', function* () {
+ yield 3;
+ yield 4;
+ }),
+ [[{}, [{ z: 3 }, { z: 4 }]]]
+ );
+
+ // more complex
+ t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined; z: number }, {}>(
+ u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .expand('z', function* (p) {
+ if (p.a) {
+ yield 3;
+ } else {
+ yield 5;
+ }
+ }),
+ [
+ [{ a: true, x: 1, z: 3 }, undefined],
+ [{ a: false, y: 2, z: 5 }, undefined],
+ ]
+ );
+ t.expectParams<{ a: boolean; x: number | undefined; y: number | undefined }, { z: number }>(
+ u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, y: 2 },
+ ])
+ .beginSubcases()
+ .expand('z', function* (p) {
+ if (p.a) {
+ yield 3;
+ } else {
+ yield 5;
+ }
+ }),
+ [
+ [{ a: true, x: 1 }, [{ z: 3 }]],
+ [{ a: false, y: 2 }, [{ z: 5 }]],
+ ]
+ );
+});
+
+g.test('invalid,shadowing').fn(t => {
+ // Existing CaseP is shadowed by a new CaseP.
+ {
+ const p = u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, x: 2 },
+ ])
+ .expandWithParams(function* (p) {
+ if (p.a) {
+ yield { x: 3 };
+ } else {
+ yield { w: 5 };
+ }
+ });
+ // Iterating causes merging e.g. ({x:1}, {x:3}), which fails.
+ t.shouldThrow('Error', () => {
+ Array.from(p.iterateCasesWithSubcases(null));
+ });
+ }
+ // Existing SubcaseP is shadowed by a new SubcaseP.
+ {
+ const p = u
+ .beginSubcases()
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, x: 2 },
+ ])
+ .expandWithParams(function* (p) {
+ if (p.a) {
+ yield { x: 3 };
+ } else {
+ yield { w: 5 };
+ }
+ });
+ // Iterating causes merging e.g. ({x:1}, {x:3}), which fails.
+ t.shouldThrow('Error', () => {
+ Array.from(p.iterateCasesWithSubcases(null));
+ });
+ }
+ // Existing CaseP is shadowed by a new SubcaseP.
+ {
+ const p = u
+ .combineWithParams([
+ { a: true, x: 1 },
+ { a: false, x: 2 },
+ ])
+ .beginSubcases()
+ .expandWithParams(function* (p) {
+ if (p.a) {
+ yield { x: 3 };
+ } else {
+ yield { w: 5 };
+ }
+ });
+ const cases = Array.from(p.iterateCasesWithSubcases(null));
+ // Iterating cases is fine...
+ for (const [caseP, subcases] of cases) {
+ assert(subcases !== undefined);
+ // Iterating subcases is fine...
+ for (const subcaseP of subcases) {
+ if (caseP.a) {
+ assert(subcases !== undefined);
+
+ // Only errors once we try to merge e.g. ({x:1}, {x:3}).
+ mergeParams(caseP, subcaseP);
+ t.shouldThrow('Error', () => {
+ mergeParamsChecked(caseP, subcaseP);
+ });
+ }
+ }
+ }
+ }
+});
+
+g.test('undefined').fn(t => {
+ t.expect(!publicParamsEquals({ a: undefined }, {}));
+ t.expect(!publicParamsEquals({}, { a: undefined }));
+});
+
+g.test('private').fn(t => {
+ t.expect(publicParamsEquals({ _a: 0 }, {}));
+ t.expect(publicParamsEquals({}, { _a: 0 }));
+});
+
+g.test('value,array').fn(t => {
+ t.expectParams<{ a: number[] }, {}>(u.combineWithParams([{ a: [1, 2] }]), [
+ [{ a: [1, 2] }, undefined], //
+ ]);
+ t.expectParams<{}, { a: number[] }>(u.beginSubcases().combineWithParams([{ a: [1, 2] }]), [
+ [{}, [{ a: [1, 2] }]], //
+ ]);
+});
+
+g.test('value,object').fn(t => {
+ t.expectParams<{ a: { [k: string]: number } }, {}>(u.combineWithParams([{ a: { x: 1 } }]), [
+ [{ a: { x: 1 } }, undefined], //
+ ]);
+ t.expectParams<{}, { a: { [k: string]: number } }>(
+ u.beginSubcases().combineWithParams([{ a: { x: 1 } }]),
+ [
+ [{}, [{ a: { x: 1 } }]], //
+ ]
+ );
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_toplevel.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_toplevel.spec.ts
new file mode 100644
index 0000000000..08a84b23e7
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/params_builder_toplevel.spec.ts
@@ -0,0 +1,112 @@
+export const description = `
+Unit tests for parameterization.
+`;
+
+import { TestParams } from '../common/framework/fixture.js';
+import { kUnitCaseParamsBuilder } from '../common/framework/params_builder.js';
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { makeTestGroupForUnitTesting } from '../common/internal/test_group.js';
+
+import { TestGroupTest } from './test_group_test.js';
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(TestGroupTest);
+
+g.test('combine_none,arg_unit')
+ .params(u => u.combineWithParams([]))
+ .fn(t => {
+ t.fail("this test shouldn't run");
+ });
+
+g.test('combine_none,arg_ignored')
+ .params(() => kUnitCaseParamsBuilder.combineWithParams([]))
+ .fn(t => {
+ t.fail("this test shouldn't run");
+ });
+
+g.test('combine_none,plain_builder')
+ .params(kUnitCaseParamsBuilder.combineWithParams([]))
+ .fn(t => {
+ t.fail("this test shouldn't run");
+ });
+
+g.test('combine_none,plain_array')
+ .paramsSimple([])
+ .fn(t => {
+ t.fail("this test shouldn't run");
+ });
+
+g.test('combine_one,case')
+ .params(u =>
+ u //
+ .combineWithParams([{ x: 1 }])
+ )
+ .fn(t => {
+ t.expect(t.params.x === 1);
+ });
+
+g.test('combine_one,subcase')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combineWithParams([{ x: 1 }])
+ )
+ .fn(t => {
+ t.expect(t.params.x === 1);
+ });
+
+g.test('filter')
+ .params(u =>
+ u
+ .combineWithParams([
+ { a: true, x: 1 }, //
+ { a: false, y: 2 },
+ ])
+ .filter(p => p.a)
+ )
+ .fn(t => {
+ t.expect(t.params.a);
+ });
+
+g.test('unless')
+ .params(u =>
+ u
+ .combineWithParams([
+ { a: true, x: 1 }, //
+ { a: false, y: 2 },
+ ])
+ .unless(p => p.a)
+ )
+ .fn(t => {
+ t.expect(!t.params.a);
+ });
+
+g.test('generator').fn(t0 => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ const ran: TestParams[] = [];
+
+ g.test('generator')
+ .params(u =>
+ u.combineWithParams({
+ *[Symbol.iterator]() {
+ for (let x = 0; x < 3; ++x) {
+ for (let y = 0; y < 2; ++y) {
+ yield { x, y };
+ }
+ }
+ },
+ })
+ )
+ .fn(t => {
+ ran.push(t.params);
+ });
+
+ t0.expectCases(g, [
+ { test: ['generator'], params: { x: 0, y: 0 } },
+ { test: ['generator'], params: { x: 0, y: 1 } },
+ { test: ['generator'], params: { x: 1, y: 0 } },
+ { test: ['generator'], params: { x: 1, y: 1 } },
+ { test: ['generator'], params: { x: 2, y: 0 } },
+ { test: ['generator'], params: { x: 2, y: 1 } },
+ ]);
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/preprocessor.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/preprocessor.spec.ts
new file mode 100644
index 0000000000..040629355d
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/preprocessor.spec.ts
@@ -0,0 +1,207 @@
+export const description = `
+Test for "pp" preprocessor.
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { pp } from '../common/util/preprocessor.js';
+
+import { UnitTest } from './unit_test.js';
+
+class F extends UnitTest {
+ test(act: string, exp: string): void {
+ this.expect(act === exp, 'got: ' + act.replace('\n', '⏎'));
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('empty').fn(t => {
+ t.test(pp``, '');
+ t.test(pp`\n`, '\n');
+ t.test(pp`\n\n`, '\n\n');
+});
+
+g.test('plain').fn(t => {
+ t.test(pp`a`, 'a');
+ t.test(pp`\na`, '\na');
+ t.test(pp`\n\na`, '\n\na');
+ t.test(pp`\na\n`, '\na\n');
+ t.test(pp`a\n\n`, 'a\n\n');
+});
+
+g.test('substitutions,1').fn(t => {
+ const act = pp`a ${3} b`;
+ const exp = 'a 3 b';
+ t.test(act, exp);
+});
+
+g.test('substitutions,2').fn(t => {
+ const act = pp`a ${'x'}`;
+ const exp = 'a x';
+ t.test(act, exp);
+});
+
+g.test('substitutions,3').fn(t => {
+ const act = pp`a ${'x'} b`;
+ const exp = 'a x b';
+ t.test(act, exp);
+});
+
+g.test('substitutions,4').fn(t => {
+ const act = pp`
+a
+${pp._if(false)}
+${'x'}
+${pp._endif}
+b`;
+ const exp = '\na\n\nb';
+ t.test(act, exp);
+});
+
+g.test('if,true').fn(t => {
+ const act = pp`
+a
+${pp._if(true)}c${pp._endif}
+d
+`;
+ const exp = '\na\nc\nd\n';
+ t.test(act, exp);
+});
+
+g.test('if,false').fn(t => {
+ const act = pp`
+a
+${pp._if(false)}c${pp._endif}
+d
+`;
+ const exp = '\na\n\nd\n';
+ t.test(act, exp);
+});
+
+g.test('else,1').fn(t => {
+ const act = pp`
+a
+${pp._if(true)}
+b
+${pp._else}
+c
+${pp._endif}
+d
+`;
+ const exp = '\na\n\nb\n\nd\n';
+ t.test(act, exp);
+});
+
+g.test('else,2').fn(t => {
+ const act = pp`
+a
+${pp._if(false)}
+b
+${pp._else}
+c
+${pp._endif}
+d
+`;
+ const exp = '\na\n\nc\n\nd\n';
+ t.test(act, exp);
+});
+
+g.test('elif,1').fn(t => {
+ const act = pp`
+a
+${pp._if(false)}
+b
+${pp._elif(true)}
+e
+${pp._else}
+c
+${pp._endif}
+d
+`;
+ const exp = '\na\n\ne\n\nd\n';
+ t.test(act, exp);
+});
+
+g.test('elif,2').fn(t => {
+ const act = pp`
+a
+${pp._if(true)}
+b
+${pp._elif(true)}
+e
+${pp._else}
+c
+${pp._endif}
+d
+`;
+ const exp = '\na\n\nb\n\nd\n';
+ t.test(act, exp);
+});
+
+g.test('nested,1').fn(t => {
+ const act = pp`
+a
+${pp._if(false)}
+b
+${pp.__if(true)}
+e
+${pp.__endif}
+c
+${pp._endif}
+d
+`;
+ const exp = '\na\n\nd\n';
+ t.test(act, exp);
+});
+
+g.test('nested,2').fn(t => {
+ const act = pp`
+a
+${pp._if(false)}
+b
+${pp._else}
+h
+${pp.__if(false)}
+e
+${pp.__elif(true)}
+f
+${pp.__else}
+g
+${pp.__endif}
+c
+${pp._endif}
+d
+`;
+ const exp = '\na\n\nh\n\nf\n\nc\n\nd\n';
+ t.test(act, exp);
+});
+
+g.test('errors,pass').fn(() => {
+ pp`${pp._if(true)}${pp._endif}`;
+ pp`${pp._if(true)}${pp._else}${pp._endif}`;
+ pp`${pp._if(true)}${pp.__if(true)}${pp.__endif}${pp._endif}`;
+});
+
+g.test('errors,fail').fn(t => {
+ const e = (fn: () => void) => t.shouldThrow('Error', fn);
+ e(() => pp`${pp._if(true)}`);
+ e(() => pp`${pp._elif(true)}`);
+ e(() => pp`${pp._else}`);
+ e(() => pp`${pp._endif}`);
+ e(() => pp`${pp.__if(true)}`);
+ e(() => pp`${pp.__elif(true)}`);
+ e(() => pp`${pp.__else}`);
+ e(() => pp`${pp.__endif}`);
+
+ e(() => pp`${pp._if(true)}${pp._elif(true)}`);
+ e(() => pp`${pp._if(true)}${pp._elif(true)}${pp._else}`);
+ e(() => pp`${pp._if(true)}${pp._else}`);
+ e(() => pp`${pp._else}${pp._endif}`);
+
+ e(() => pp`${pp._if(true)}${pp.__endif}`);
+ e(() => pp`${pp.__if(true)}${pp.__endif}`);
+ e(() => pp`${pp.__if(true)}${pp._endif}`);
+
+ e(() => pp`${pp._if(true)}${pp._else}${pp._else}${pp._endif}`);
+ e(() => pp`${pp._if(true)}${pp.__if(true)}${pp.__else}${pp.__else}${pp.__endif}${pp._endif}`);
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/prng.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/prng.spec.ts
new file mode 100644
index 0000000000..6317a98eea
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/prng.spec.ts
@@ -0,0 +1,74 @@
+export const description = `
+Unittests for the pseudo random number generator
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { fullU32Range } from '../webgpu/util/math.js';
+import { PRNG } from '../webgpu/util/prng.js';
+
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+// There exist more formal tests for the quality of random number generators
+// that are out of the scope for testing here (and are checked against the
+// original C implementation).
+// These tests are just intended to be smoke tests for implementation.
+
+// Test against the reference u32 values from the original C implementation
+// https://github.com/MersenneTwister-Lab/TinyMT/blob/master/tinymt/check32.out.txt
+g.test('check').fn(t => {
+ const p = new PRNG(1);
+ // prettier-ignore
+ const expected = [
+ 2545341989, 981918433, 3715302833, 2387538352, 3591001365,
+ 3820442102, 2114400566, 2196103051, 2783359912, 764534509,
+ 643179475, 1822416315, 881558334, 4207026366, 3690273640,
+ 3240535687, 2921447122, 3984931427, 4092394160, 44209675,
+ 2188315343, 2908663843, 1834519336, 3774670961, 3019990707,
+ 4065554902, 1239765502, 4035716197, 3412127188, 552822483,
+ 161364450, 353727785, 140085994, 149132008, 2547770827,
+ 4064042525, 4078297538, 2057335507, 622384752, 2041665899,
+ 2193913817, 1080849512, 33160901, 662956935, 642999063,
+ 3384709977, 1723175122, 3866752252, 521822317, 2292524454,
+ ];
+ expected.forEach((_, i) => {
+ const val = p.randomU32();
+ t.expect(
+ val === expected[i],
+ `PRNG(1) failed produced the ${i}th expected item, ${val} instead of ${expected[i]})`
+ );
+ });
+});
+
+// Prove that generator is deterministic for at least 1000 values with different
+// seeds.
+g.test('deterministic_random').fn(t => {
+ fullU32Range().forEach(seed => {
+ const lhs = new PRNG(seed);
+ const rhs = new PRNG(seed);
+ for (let i = 0; i < 1000; i++) {
+ const lhs_val = lhs.random();
+ const rhs_val = rhs.random();
+ t.expect(
+ lhs_val === rhs_val,
+ `For seed ${seed}, the ${i}th item, PRNG was non-deterministic (${lhs_val} vs ${rhs_val})`
+ );
+ }
+ });
+});
+
+g.test('deterministic_randomU32').fn(t => {
+ fullU32Range().forEach(seed => {
+ const lhs = new PRNG(seed);
+ const rhs = new PRNG(seed);
+ for (let i = 0; i < 1000; i++) {
+ const lhs_val = lhs.randomU32();
+ const rhs_val = rhs.randomU32();
+ t.expect(
+ lhs_val === rhs_val,
+ `For seed ${seed}, the ${i}th item, PRNG was non-deterministic (${lhs_val} vs ${rhs_val})`
+ );
+ }
+ });
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/query_compare.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/query_compare.spec.ts
new file mode 100644
index 0000000000..b53b76a4df
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/query_compare.spec.ts
@@ -0,0 +1,144 @@
+export const description = `
+Tests for TestQuery comparison
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { compareQueries, Ordering } from '../common/internal/query/compare.js';
+import {
+ TestQuery,
+ TestQuerySingleCase,
+ TestQueryMultiFile,
+ TestQueryMultiTest,
+ TestQueryMultiCase,
+} from '../common/internal/query/query.js';
+
+import { UnitTest } from './unit_test.js';
+
+class F extends UnitTest {
+ expectQ(a: TestQuery, exp: '<' | '=' | '>' | '!', b: TestQuery) {
+ const [expOrdering, expInvOrdering] =
+ exp === '<'
+ ? [Ordering.StrictSubset, Ordering.StrictSuperset]
+ : exp === '='
+ ? [Ordering.Equal, Ordering.Equal]
+ : exp === '>'
+ ? [Ordering.StrictSuperset, Ordering.StrictSubset]
+ : [Ordering.Unordered, Ordering.Unordered];
+ {
+ const act = compareQueries(a, b);
+ this.expect(act === expOrdering, `${a} ${b} got ${act}, exp ${expOrdering}`);
+ }
+ {
+ const act = compareQueries(a, b);
+ this.expect(act === expOrdering, `${b} ${a} got ${act}, exp ${expInvOrdering}`);
+ }
+ }
+
+ expectWellOrdered(...qs: TestQuery[]) {
+ for (let i = 0; i < qs.length; ++i) {
+ this.expectQ(qs[i], '=', qs[i]);
+ for (let j = i + 1; j < qs.length; ++j) {
+ this.expectQ(qs[i], '>', qs[j]);
+ }
+ }
+ }
+
+ expectUnordered(...qs: TestQuery[]) {
+ for (let i = 0; i < qs.length; ++i) {
+ this.expectQ(qs[i], '=', qs[i]);
+ for (let j = i + 1; j < qs.length; ++j) {
+ this.expectQ(qs[i], '!', qs[j]);
+ }
+ }
+ }
+}
+
+export const g = makeTestGroup(F);
+
+// suite:* > suite:a,* > suite:a,b,* > suite:a,b:*
+// suite:a,b:* > suite:a,b:c,* > suite:a,b:c,d,* > suite:a,b:c,d:*
+// suite:a,b:c,d:* > suite:a,b:c,d:x=1;* > suite:a,b:c,d:x=1;y=2;* > suite:a,b:c,d:x=1;y=2
+// suite:a;* (unordered) suite:b;*
+g.test('well_ordered').fn(t => {
+ t.expectWellOrdered(
+ new TestQueryMultiFile('suite', []),
+ new TestQueryMultiFile('suite', ['a']),
+ new TestQueryMultiFile('suite', ['a', 'b']),
+ new TestQueryMultiTest('suite', ['a', 'b'], []),
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c']),
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'd']),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], {}),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 1 }),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }),
+ new TestQuerySingleCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 })
+ );
+ t.expectWellOrdered(
+ new TestQueryMultiFile('suite', []),
+ new TestQueryMultiFile('suite', ['a']),
+ new TestQueryMultiFile('suite', ['a', 'b']),
+ new TestQueryMultiTest('suite', ['a', 'b'], []),
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c']),
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'd']),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], {}),
+ new TestQuerySingleCase('suite', ['a', 'b'], ['c', 'd'], {})
+ );
+});
+
+g.test('unordered').fn(t => {
+ t.expectUnordered(
+ new TestQueryMultiFile('suite', ['a']), //
+ new TestQueryMultiFile('suite', ['x'])
+ );
+ t.expectUnordered(
+ new TestQueryMultiFile('suite', ['a', 'b']),
+ new TestQueryMultiFile('suite', ['a', 'x'])
+ );
+ t.expectUnordered(
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c']),
+ new TestQueryMultiTest('suite', ['a', 'b'], ['x']),
+ new TestQueryMultiTest('suite', ['a'], []),
+ new TestQueryMultiTest('suite', ['a', 'x'], [])
+ );
+ t.expectUnordered(
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'd']),
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'x']),
+ new TestQueryMultiTest('suite', ['a'], []),
+ new TestQueryMultiTest('suite', ['a', 'x'], [])
+ );
+ t.expectUnordered(
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'd']),
+ new TestQueryMultiTest('suite', ['a', 'b'], ['c', 'x']),
+ new TestQueryMultiTest('suite', ['a'], []),
+ new TestQueryMultiTest('suite', ['a', 'x'], ['c'])
+ );
+ t.expectUnordered(
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 1 }),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 9 }),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c'], { x: 9 })
+ );
+ t.expectUnordered(
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 8 }),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c'], { x: 1, y: 8 })
+ );
+ t.expectUnordered(
+ new TestQuerySingleCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }),
+ new TestQuerySingleCase('suite', ['a', 'b'], ['c', 'd'], { x: 1, y: 8 }),
+ new TestQuerySingleCase('suite', ['a', 'b'], ['c'], { x: 1, y: 8 })
+ );
+ t.expectUnordered(
+ new TestQuerySingleCase('suite1', ['bar', 'buzz', 'buzz'], ['zap'], {}),
+ new TestQueryMultiTest('suite1', ['bar'], [])
+ );
+ // Expect that 0.0 and -0.0 are treated as different queries
+ t.expectUnordered(
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: 0.0 }),
+ new TestQueryMultiCase('suite', ['a', 'b'], ['c', 'd'], { x: -0.0 })
+ );
+ t.expectUnordered(
+ new TestQuerySingleCase('suite', ['a', 'b'], ['c', 'd'], { x: 0.0, y: 0.0 }),
+ new TestQuerySingleCase('suite', ['a', 'b'], ['c', 'd'], { x: 0.0, y: -0.0 }),
+ new TestQuerySingleCase('suite', ['a', 'b'], ['c', 'd'], { x: -0.0, y: 0.0 }),
+ new TestQuerySingleCase('suite', ['a', 'b'], ['c', 'd'], { x: -0.0, y: -0.0 })
+ );
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/query_string.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/query_string.spec.ts
new file mode 100644
index 0000000000..040acd1b87
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/query_string.spec.ts
@@ -0,0 +1,268 @@
+export const description = `
+Unit tests for TestQuery strings.
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { compareQueries, Ordering } from '../common/internal/query/compare.js';
+import {
+ TestQuery,
+ TestQuerySingleCase,
+ TestQueryMultiCase,
+ TestQueryMultiTest,
+ TestQueryMultiFile,
+ relativeQueryString,
+} from '../common/internal/query/query.js';
+
+import { UnitTest } from './unit_test.js';
+
+class T extends UnitTest {
+ expectQueryString(q: TestQuery, exp: string): void {
+ const s = q.toString();
+ this.expect(s === exp, `got ${s} expected ${exp}`);
+ }
+
+ expectRelativeQueryString(parent: TestQuery, child: TestQuery, exp: string): void {
+ const s = relativeQueryString(parent, child);
+ this.expect(s === exp, `got ${s} expected ${exp}`);
+
+ if (compareQueries(parent, child) !== Ordering.Equal) {
+ // Test in reverse
+ this.shouldThrow('Error', () => {
+ relativeQueryString(child, parent);
+ });
+ }
+ }
+}
+
+export const g = makeTestGroup(T);
+
+g.test('stringifyQuery,single_case').fn(t => {
+ t.expectQueryString(
+ new TestQuerySingleCase('a', ['b_1', '2_c'], ['d_3', '4_e'], {
+ f: 'g',
+ _pri1: 0,
+ x: 3,
+ _pri2: 1,
+ }),
+ 'a:b_1,2_c:d_3,4_e:f="g";x=3'
+ );
+});
+
+g.test('stringifyQuery,single_case,json').fn(t => {
+ t.expectQueryString(
+ new TestQuerySingleCase('a', ['b_1', '2_c'], ['d_3', '4_e'], {
+ f: 'g',
+ x: { p: 2, q: 'Q' },
+ }),
+ 'a:b_1,2_c:d_3,4_e:f="g";x={"p":2,"q":"Q"}'
+ );
+});
+
+g.test('stringifyQuery,multi_case').fn(t => {
+ t.expectQueryString(
+ new TestQueryMultiCase('a', ['b_1', '2_c'], ['d_3', '4_e'], {
+ f: 'g',
+ _pri1: 0,
+ a: 3,
+ _pri2: 1,
+ }),
+ 'a:b_1,2_c:d_3,4_e:f="g";a=3;*'
+ );
+
+ t.expectQueryString(
+ new TestQueryMultiCase('a', ['b_1', '2_c'], ['d_3', '4_e'], {}),
+ 'a:b_1,2_c:d_3,4_e:*'
+ );
+});
+
+g.test('stringifyQuery,multi_test').fn(t => {
+ t.expectQueryString(
+ new TestQueryMultiTest('a', ['b_1', '2_c'], ['d_3', '4_e']),
+ 'a:b_1,2_c:d_3,4_e,*'
+ );
+
+ t.expectQueryString(
+ new TestQueryMultiTest('a', ['b_1', '2_c'], []), //
+ 'a:b_1,2_c:*'
+ );
+});
+
+g.test('stringifyQuery,multi_file').fn(t => {
+ t.expectQueryString(
+ new TestQueryMultiFile('a', ['b_1', '2_c']), //
+ 'a:b_1,2_c,*'
+ );
+
+ t.expectQueryString(
+ new TestQueryMultiFile('a', []), //
+ 'a:*'
+ );
+});
+
+g.test('relativeQueryString,equal_or_child').fn(t => {
+ // Depth difference = 0
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', []), //
+ new TestQueryMultiFile('a', []), //
+ ''
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', ['b', 'c']), //
+ new TestQueryMultiFile('a', ['b', 'c']), //
+ ''
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), //
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), //
+ ''
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), //
+ ''
+ );
+ t.expectRelativeQueryString(
+ new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ ''
+ );
+
+ // Depth difference = 1
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', []), //
+ new TestQueryMultiFile('a', ['b']), //
+ ':b,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', ['b']), //
+ new TestQueryMultiFile('a', ['b', 'c']), //
+ ',c,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', ['b', 'c']), //
+ new TestQueryMultiTest('a', ['b', 'c'], []), //
+ ':*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], []), //
+ new TestQueryMultiTest('a', ['b', 'c'], ['d']), //
+ ':d,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], ['d']), //
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), //
+ ',e,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], {}), //
+ ':*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], {}), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), //
+ ':f=0;*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ ';g=1;*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ ''
+ );
+
+ // Depth difference = 2
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', []), //
+ new TestQueryMultiFile('a', ['b', 'c']), //
+ ':b,c,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', ['b', 'c']), //
+ new TestQueryMultiTest('a', ['b', 'c'], ['d']), //
+ ':d,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], ['d']), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], {}), //
+ ',e:*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], {}), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ ':f=0;g=1;*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1, h: 2 }), //
+ ';h=2'
+ );
+ // Depth difference = 2
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', ['b']), //
+ new TestQueryMultiTest('a', ['b', 'c'], []), //
+ ',c:*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], []), //
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), //
+ ':d,e,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), //
+ ':f=0;*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), //
+ new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ ';g=1'
+ );
+
+ // Depth difference = 4
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', []), //
+ new TestQueryMultiTest('a', ['b', 'c'], ['d']), //
+ ':b,c:d,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], ['d']), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ ',e:f=0;g=1;*'
+ );
+ // Depth difference = 4
+ t.expectRelativeQueryString(
+ new TestQueryMultiFile('a', ['b']), //
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), //
+ ',c:d,e,*'
+ );
+ t.expectRelativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']), //
+ new TestQuerySingleCase('a', ['b', 'c'], ['d', 'e'], { f: 0, g: 1 }), //
+ ':f=0;g=1'
+ );
+});
+
+g.test('relativeQueryString,unrelated').fn(t => {
+ t.shouldThrow('Error', () => {
+ relativeQueryString(
+ new TestQueryMultiFile('a', ['b', 'x']), //
+ new TestQueryMultiFile('a', ['b', 'c']) //
+ );
+ });
+ t.shouldThrow('Error', () => {
+ relativeQueryString(
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'x']), //
+ new TestQueryMultiTest('a', ['b', 'c'], ['d', 'e']) //
+ );
+ });
+ t.shouldThrow('Error', () => {
+ relativeQueryString(
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 0 }), //
+ new TestQueryMultiCase('a', ['b', 'c'], ['d', 'e'], { f: 1 }) //
+ );
+ });
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/serialization.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/serialization.spec.ts
new file mode 100644
index 0000000000..9717ba3ecf
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/serialization.spec.ts
@@ -0,0 +1,413 @@
+export const description = `Unit tests for data cache serialization`;
+
+import { getIsBuildingDataCache, setIsBuildingDataCache } from '../common/framework/data_cache.js';
+import { makeTestGroup } from '../common/internal/test_group.js';
+import { objectEquals } from '../common/util/util.js';
+import {
+ deserializeExpectation,
+ serializeExpectation,
+} from '../webgpu/shader/execution/expression/case_cache.js';
+import BinaryStream from '../webgpu/util/binary_stream.js';
+import {
+ anyOf,
+ deserializeComparator,
+ serializeComparator,
+ skipUndefined,
+} from '../webgpu/util/compare.js';
+import { kValue } from '../webgpu/util/constants.js';
+import {
+ bool,
+ deserializeValue,
+ f16,
+ f32,
+ i16,
+ i32,
+ i8,
+ serializeValue,
+ toMatrix,
+ u16,
+ u32,
+ u8,
+ vec2,
+ vec3,
+ vec4,
+} from '../webgpu/util/conversion.js';
+import { deserializeFPInterval, FP, serializeFPInterval } from '../webgpu/util/floating_point.js';
+
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(UnitTest);
+
+g.test('value').fn(t => {
+ for (const value of [
+ u32(kValue.u32.min + 0),
+ u32(kValue.u32.min + 1),
+ u32(kValue.u32.min + 2),
+ u32(kValue.u32.max - 2),
+ u32(kValue.u32.max - 1),
+ u32(kValue.u32.max - 0),
+
+ u16(kValue.u16.min + 0),
+ u16(kValue.u16.min + 1),
+ u16(kValue.u16.min + 2),
+ u16(kValue.u16.max - 2),
+ u16(kValue.u16.max - 1),
+ u16(kValue.u16.max - 0),
+
+ u8(kValue.u8.min + 0),
+ u8(kValue.u8.min + 1),
+ u8(kValue.u8.min + 2),
+ u8(kValue.u8.max - 2),
+ u8(kValue.u8.max - 1),
+ u8(kValue.u8.max - 0),
+
+ i32(kValue.i32.negative.min + 0),
+ i32(kValue.i32.negative.min + 1),
+ i32(kValue.i32.negative.min + 2),
+ i32(kValue.i32.negative.max - 2),
+ i32(kValue.i32.negative.max - 1),
+ i32(kValue.i32.positive.min - 0),
+ i32(kValue.i32.positive.min + 1),
+ i32(kValue.i32.positive.min + 2),
+ i32(kValue.i32.positive.max - 2),
+ i32(kValue.i32.positive.max - 1),
+ i32(kValue.i32.positive.max - 0),
+
+ i16(kValue.i16.negative.min + 0),
+ i16(kValue.i16.negative.min + 1),
+ i16(kValue.i16.negative.min + 2),
+ i16(kValue.i16.negative.max - 2),
+ i16(kValue.i16.negative.max - 1),
+ i16(kValue.i16.positive.min + 0),
+ i16(kValue.i16.positive.min + 1),
+ i16(kValue.i16.positive.min + 2),
+ i16(kValue.i16.positive.max - 2),
+ i16(kValue.i16.positive.max - 1),
+ i16(kValue.i16.positive.max - 0),
+
+ i8(kValue.i8.negative.min + 0),
+ i8(kValue.i8.negative.min + 1),
+ i8(kValue.i8.negative.min + 2),
+ i8(kValue.i8.negative.max - 2),
+ i8(kValue.i8.negative.max - 1),
+ i8(kValue.i8.positive.min + 0),
+ i8(kValue.i8.positive.min + 1),
+ i8(kValue.i8.positive.min + 2),
+ i8(kValue.i8.positive.max - 2),
+ i8(kValue.i8.positive.max - 1),
+ i8(kValue.i8.positive.max - 0),
+
+ f32(0),
+ f32(-0),
+ f32(1),
+ f32(-1),
+ f32(0.5),
+ f32(-0.5),
+ f32(kValue.f32.positive.max),
+ f32(kValue.f32.positive.min),
+ f32(kValue.f32.positive.subnormal.max),
+ f32(kValue.f32.positive.subnormal.min),
+ f32(kValue.f32.negative.subnormal.max),
+ f32(kValue.f32.negative.subnormal.min),
+ f32(kValue.f32.positive.infinity),
+ f32(kValue.f32.negative.infinity),
+
+ f16(0),
+ f16(-0),
+ f16(1),
+ f16(-1),
+ f16(0.5),
+ f16(-0.5),
+ f16(kValue.f16.positive.max),
+ f16(kValue.f16.positive.min),
+ f16(kValue.f16.positive.subnormal.max),
+ f16(kValue.f16.positive.subnormal.min),
+ f16(kValue.f16.negative.subnormal.max),
+ f16(kValue.f16.negative.subnormal.min),
+ f16(kValue.f16.positive.infinity),
+ f16(kValue.f16.negative.infinity),
+
+ bool(true),
+ bool(false),
+
+ vec2(f32(1), f32(2)),
+ vec3(u32(1), u32(2), u32(3)),
+ vec4(bool(false), bool(true), bool(false), bool(true)),
+
+ toMatrix(
+ [
+ [0.0, 1.0],
+ [2.0, 3.0],
+ ],
+ f32
+ ),
+ toMatrix(
+ [
+ [0.0, 1.0, 2.0],
+ [3.0, 4.0, 5.0],
+ ],
+ f16
+ ),
+ toMatrix(
+ [
+ [0.0, 1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0, 7.0],
+ ],
+ f32
+ ),
+ toMatrix(
+ [
+ [0.0, 1.0],
+ [2.0, 3.0],
+ [4.0, 5.0],
+ ],
+ f16
+ ),
+ toMatrix(
+ [
+ [0.0, 1.0, 2.0],
+ [3.0, 4.0, 5.0],
+ [6.0, 7.0, 8.0],
+ ],
+ f32
+ ),
+ toMatrix(
+ [
+ [0.0, 1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0, 7.0],
+ [8.0, 9.0, 10.0, 11.0],
+ ],
+ f16
+ ),
+ toMatrix(
+ [
+ [0.0, 1.0],
+ [2.0, 3.0],
+ [4.0, 5.0],
+ [6.0, 7.0],
+ ],
+ f32
+ ),
+ toMatrix(
+ [
+ [0.0, 1.0, 2.0],
+ [3.0, 4.0, 5.0],
+ [6.0, 7.0, 8.0],
+ [9.0, 10.0, 11.0],
+ ],
+ f16
+ ),
+ toMatrix(
+ [
+ [0.0, 1.0, 2.0, 3.0],
+ [4.0, 5.0, 6.0, 7.0],
+ [8.0, 9.0, 10.0, 11.0],
+ [12.0, 13.0, 14.0, 15.0],
+ ],
+ f32
+ ),
+ ]) {
+ const s = new BinaryStream(new Uint8Array(1024).buffer);
+ serializeValue(s, value);
+ const d = new BinaryStream(s.buffer().buffer);
+ const deserialized = deserializeValue(d);
+ t.expect(
+ objectEquals(value, deserialized),
+ `${value.type} ${value} -> serialize -> deserialize -> ${deserialized}
+buffer: ${s.buffer()}`
+ );
+ }
+});
+
+g.test('fpinterval_f32').fn(t => {
+ for (const interval of [
+ FP.f32.toInterval(0),
+ FP.f32.toInterval(-0),
+ FP.f32.toInterval(1),
+ FP.f32.toInterval(-1),
+ FP.f32.toInterval(0.5),
+ FP.f32.toInterval(-0.5),
+ FP.f32.toInterval(kValue.f32.positive.max),
+ FP.f32.toInterval(kValue.f32.positive.min),
+ FP.f32.toInterval(kValue.f32.positive.subnormal.max),
+ FP.f32.toInterval(kValue.f32.positive.subnormal.min),
+ FP.f32.toInterval(kValue.f32.negative.subnormal.max),
+ FP.f32.toInterval(kValue.f32.negative.subnormal.min),
+ FP.f32.toInterval(kValue.f32.positive.infinity),
+ FP.f32.toInterval(kValue.f32.negative.infinity),
+
+ FP.f32.toInterval([-0, 0]),
+ FP.f32.toInterval([-1, 1]),
+ FP.f32.toInterval([-0.5, 0.5]),
+ FP.f32.toInterval([kValue.f32.positive.min, kValue.f32.positive.max]),
+ FP.f32.toInterval([kValue.f32.positive.subnormal.min, kValue.f32.positive.subnormal.max]),
+ FP.f32.toInterval([kValue.f32.negative.subnormal.min, kValue.f32.negative.subnormal.max]),
+ FP.f32.toInterval([kValue.f32.negative.infinity, kValue.f32.positive.infinity]),
+ ]) {
+ const s = new BinaryStream(new Uint8Array(1024).buffer);
+ serializeFPInterval(s, interval);
+ const d = new BinaryStream(s.buffer().buffer);
+ const deserialized = deserializeFPInterval(d);
+ t.expect(
+ objectEquals(interval, deserialized),
+ `interval ${interval} -> serialize -> deserialize -> ${deserialized}`
+ );
+ }
+});
+
+g.test('fpinterval_f16').fn(t => {
+ for (const interval of [
+ FP.f16.toInterval(0),
+ FP.f16.toInterval(-0),
+ FP.f16.toInterval(1),
+ FP.f16.toInterval(-1),
+ FP.f16.toInterval(0.5),
+ FP.f16.toInterval(-0.5),
+ FP.f16.toInterval(kValue.f16.positive.max),
+ FP.f16.toInterval(kValue.f16.positive.min),
+ FP.f16.toInterval(kValue.f16.positive.subnormal.max),
+ FP.f16.toInterval(kValue.f16.positive.subnormal.min),
+ FP.f16.toInterval(kValue.f16.negative.subnormal.max),
+ FP.f16.toInterval(kValue.f16.negative.subnormal.min),
+ FP.f16.toInterval(kValue.f16.positive.infinity),
+ FP.f16.toInterval(kValue.f16.negative.infinity),
+
+ FP.f16.toInterval([-0, 0]),
+ FP.f16.toInterval([-1, 1]),
+ FP.f16.toInterval([-0.5, 0.5]),
+ FP.f16.toInterval([kValue.f16.positive.min, kValue.f16.positive.max]),
+ FP.f16.toInterval([kValue.f16.positive.subnormal.min, kValue.f16.positive.subnormal.max]),
+ FP.f16.toInterval([kValue.f16.negative.subnormal.min, kValue.f16.negative.subnormal.max]),
+ FP.f16.toInterval([kValue.f16.negative.infinity, kValue.f16.positive.infinity]),
+ ]) {
+ const s = new BinaryStream(new Uint8Array(1024).buffer);
+ serializeFPInterval(s, interval);
+ const d = new BinaryStream(s.buffer().buffer);
+ const deserialized = deserializeFPInterval(d);
+ t.expect(
+ objectEquals(interval, deserialized),
+ `interval ${interval} -> serialize -> deserialize -> ${deserialized}`
+ );
+ }
+});
+
+g.test('fpinterval_abstract').fn(t => {
+ for (const interval of [
+ FP.abstract.toInterval(0),
+ FP.abstract.toInterval(-0),
+ FP.abstract.toInterval(1),
+ FP.abstract.toInterval(-1),
+ FP.abstract.toInterval(0.5),
+ FP.abstract.toInterval(-0.5),
+ FP.abstract.toInterval(kValue.f64.positive.max),
+ FP.abstract.toInterval(kValue.f64.positive.min),
+ FP.abstract.toInterval(kValue.f64.positive.subnormal.max),
+ FP.abstract.toInterval(kValue.f64.positive.subnormal.min),
+ FP.abstract.toInterval(kValue.f64.negative.subnormal.max),
+ FP.abstract.toInterval(kValue.f64.negative.subnormal.min),
+ FP.abstract.toInterval(kValue.f64.positive.infinity),
+ FP.abstract.toInterval(kValue.f64.negative.infinity),
+
+ FP.abstract.toInterval([-0, 0]),
+ FP.abstract.toInterval([-1, 1]),
+ FP.abstract.toInterval([-0.5, 0.5]),
+ FP.abstract.toInterval([kValue.f64.positive.min, kValue.f64.positive.max]),
+ FP.abstract.toInterval([kValue.f64.positive.subnormal.min, kValue.f64.positive.subnormal.max]),
+ FP.abstract.toInterval([kValue.f64.negative.subnormal.min, kValue.f64.negative.subnormal.max]),
+ FP.abstract.toInterval([kValue.f64.negative.infinity, kValue.f64.positive.infinity]),
+ ]) {
+ const s = new BinaryStream(new Uint8Array(1024).buffer);
+ serializeFPInterval(s, interval);
+ const d = new BinaryStream(s.buffer().buffer);
+ const deserialized = deserializeFPInterval(d);
+ t.expect(
+ objectEquals(interval, deserialized),
+ `interval ${interval} -> serialize -> deserialize -> ${deserialized}`
+ );
+ }
+});
+
+g.test('expression_expectation').fn(t => {
+ for (const expectation of [
+ // Value
+ f32(123),
+ vec2(f32(1), f32(2)),
+ // Interval
+ FP.f32.toInterval([-0.5, 0.5]),
+ FP.f32.toInterval([kValue.f32.positive.min, kValue.f32.positive.max]),
+ // Intervals
+ [FP.f32.toInterval([-8.0, 0.5]), FP.f32.toInterval([2.0, 4.0])],
+ ]) {
+ const s = new BinaryStream(new Uint8Array(1024).buffer);
+ serializeExpectation(s, expectation);
+ const d = new BinaryStream(s.buffer().buffer);
+ const deserialized = deserializeExpectation(d);
+ t.expect(
+ objectEquals(expectation, deserialized),
+ `expectation ${expectation} -> serialize -> deserialize -> ${deserialized}`
+ );
+ }
+});
+
+/**
+ * Temporarily enabled building of the data cache.
+ * Required for Comparators to serialize.
+ */
+function enableBuildingDataCache(f: () => void) {
+ const wasBuildingDataCache = getIsBuildingDataCache();
+ setIsBuildingDataCache(true);
+ f();
+ setIsBuildingDataCache(wasBuildingDataCache);
+}
+
+g.test('anyOf').fn(t => {
+ enableBuildingDataCache(() => {
+ for (const c of [
+ {
+ comparator: anyOf(i32(123)),
+ testCases: [f32(0), f32(10), f32(122), f32(123), f32(124), f32(200)],
+ },
+ ]) {
+ const s = new BinaryStream(new Uint8Array(1024).buffer);
+ serializeComparator(s, c.comparator);
+ const d = new BinaryStream(s.buffer().buffer);
+ const deserialized = deserializeComparator(d);
+ for (const val of c.testCases) {
+ const got = deserialized.compare(val);
+ const expect = c.comparator.compare(val);
+ t.expect(
+ got.matched === expect.matched,
+ `comparator(${val}): got: ${expect.matched}, expect: ${got.matched}`
+ );
+ }
+ }
+ });
+});
+
+g.test('skipUndefined').fn(t => {
+ enableBuildingDataCache(() => {
+ for (const c of [
+ {
+ comparator: skipUndefined(i32(123)),
+ testCases: [f32(0), f32(10), f32(122), f32(123), f32(124), f32(200)],
+ },
+ {
+ comparator: skipUndefined(undefined),
+ testCases: [f32(0), f32(10), f32(122), f32(123), f32(124), f32(200)],
+ },
+ ]) {
+ const s = new BinaryStream(new Uint8Array(1024).buffer);
+ serializeComparator(s, c.comparator);
+ const d = new BinaryStream(s.buffer().buffer);
+ const deserialized = deserializeComparator(d);
+ for (const val of c.testCases) {
+ const got = deserialized.compare(val);
+ const expect = c.comparator.compare(val);
+ t.expect(
+ got.matched === expect.matched,
+ `comparator(${val}): got: ${expect.matched}, expect: ${got.matched}`
+ );
+ }
+ }
+ });
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/test_group.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/test_group.spec.ts
new file mode 100644
index 0000000000..aca8d298e6
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/test_group.spec.ts
@@ -0,0 +1,437 @@
+/* eslint-disable @typescript-eslint/require-await */
+export const description = `
+Unit tests for TestGroup.
+`;
+
+import { Fixture } from '../common/framework/fixture.js';
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { TestQueryMultiFile } from '../common/internal/query/query.js';
+import { kQueryMaxLength, makeTestGroupForUnitTesting } from '../common/internal/test_group.js';
+import { assert } from '../common/util/util.js';
+
+import { TestGroupTest } from './test_group_test.js';
+import { UnitTest } from './unit_test.js';
+
+export const g = makeTestGroup(TestGroupTest);
+
+g.test('UnitTest_fixture').fn(async t0 => {
+ let seen = 0;
+ function count(_t: Fixture): void {
+ seen++;
+ }
+
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ g.test('test').fn(count);
+ g.test('testp')
+ .paramsSimple([{ a: 1 }])
+ .fn(count);
+
+ await t0.run(g);
+ t0.expect(seen === 2);
+});
+
+g.test('custom_fixture').fn(async t0 => {
+ let seen = 0;
+ class Counter extends UnitTest {
+ count(): void {
+ seen++;
+ }
+ }
+
+ const g = makeTestGroupForUnitTesting(Counter);
+
+ g.test('test').fn(t => {
+ t.count();
+ });
+ g.test('testp')
+ .paramsSimple([{ a: 1 }])
+ .fn(t => {
+ t.count();
+ });
+
+ await t0.run(g);
+ t0.expect(seen === 2);
+});
+
+g.test('stack').fn(async t0 => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ const doNestedThrow1 = () => {
+ throw new Error('goodbye');
+ };
+
+ const doNestedThrow2 = () => doNestedThrow1();
+
+ g.test('fail').fn(t => {
+ t.fail();
+ });
+ g.test('throw').fn(_t => {
+ throw new Error('hello');
+ });
+ g.test('throw_nested').fn(_t => {
+ doNestedThrow2();
+ });
+
+ const res = await t0.run(g);
+
+ const search = /unittests[/\\]test_group\.spec\.[tj]s/;
+ t0.expect(res.size > 0);
+ for (const { logs } of res.values()) {
+ assert(logs !== undefined, 'expected logs');
+ t0.expect(logs.some(l => search.test(l.toJSON())));
+ t0.expect(search.test(logs[logs.length - 1].toJSON()));
+ }
+});
+
+g.test('no_fn').fn(t => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ g.test('missing');
+
+ t.shouldThrow('Error', () => {
+ g.validate(new TestQueryMultiFile('s', ['f']));
+ });
+});
+
+g.test('duplicate_test_name').fn(t => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('abc').fn(() => {});
+
+ t.shouldThrow('Error', () => {
+ g.test('abc').fn(() => {});
+ });
+});
+
+g.test('duplicate_test_params,none').fn(() => {
+ {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('abc')
+ .paramsSimple([])
+ .fn(() => {});
+ g.validate(new TestQueryMultiFile('s', ['f']));
+ }
+
+ {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('abc').fn(() => {});
+ g.validate(new TestQueryMultiFile('s', ['f']));
+ }
+
+ {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('abc')
+ .paramsSimple([
+ { a: 1 }, //
+ ])
+ .fn(() => {});
+ g.validate(new TestQueryMultiFile('s', ['f']));
+ }
+});
+
+g.test('duplicate_test_params,basic').fn(t => {
+ {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ const builder = g.test('abc');
+ t.shouldThrow('Error', () => {
+ builder.paramsSimple([
+ { a: 1 }, //
+ { a: 1 },
+ ]);
+ g.validate(new TestQueryMultiFile('s', ['f']));
+ });
+ }
+ {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('abc')
+ .params(u =>
+ u.expandWithParams(() => [
+ { a: 1 }, //
+ { a: 1 },
+ ])
+ )
+ .fn(() => {});
+ t.shouldThrow('Error', () => {
+ g.validate(new TestQueryMultiFile('s', ['f']));
+ });
+ }
+ {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('abc')
+ .paramsSimple([
+ { a: 1, b: 3 }, //
+ { b: 3, a: 1 },
+ ])
+ .fn(() => {});
+ t.shouldThrow('Error', () => {
+ g.validate(new TestQueryMultiFile('s', ['f']));
+ });
+ }
+});
+
+g.test('duplicate_test_params,with_different_private_params').fn(t => {
+ {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ const builder = g.test('abc');
+ t.shouldThrow('Error', () => {
+ builder.paramsSimple([
+ { a: 1, _b: 1 }, //
+ { a: 1, _b: 2 },
+ ]);
+ });
+ }
+ {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('abc')
+ .params(u =>
+ u.expandWithParams(() => [
+ { a: 1, _b: 1 }, //
+ { a: 1, _b: 2 },
+ ])
+ )
+ .fn(() => {});
+ t.shouldThrow('Error', () => {
+ g.validate(new TestQueryMultiFile('s', ['f']));
+ });
+ }
+});
+
+g.test('invalid_test_name').fn(t => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ const badChars = Array.from('"`~@#$+=\\|!^&*[]<>{}-\'. ');
+ for (const char of badChars) {
+ const name = 'a' + char + 'b';
+ t.shouldThrow(
+ 'Error',
+ () => {
+ g.test(name).fn(() => {});
+ },
+ { message: name }
+ );
+ }
+});
+
+g.test('long_test_query,long_test_name').fn(t => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ const long = Array(kQueryMaxLength - 5).join('a');
+
+ const fileQuery = new TestQueryMultiFile('s', ['f']);
+ g.test(long).unimplemented();
+ g.validate(fileQuery);
+
+ g.test(long + 'a').unimplemented();
+ t.shouldThrow(
+ 'Error',
+ () => {
+ g.validate(fileQuery);
+ },
+ { message: long }
+ );
+});
+
+g.test('long_case_query,long_test_name').fn(t => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ const long = Array(kQueryMaxLength - 5).join('a');
+
+ const fileQuery = new TestQueryMultiFile('s', ['f']);
+ g.test(long).fn(() => {});
+ g.validate(fileQuery);
+
+ g.test(long + 'a').fn(() => {});
+ t.shouldThrow(
+ 'Error',
+ () => {
+ g.validate(fileQuery);
+ },
+ { message: long }
+ );
+});
+
+g.test('long_case_query,long_case_name').fn(t => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ const long = Array(kQueryMaxLength - 9).join('a');
+
+ const fileQuery = new TestQueryMultiFile('s', ['f']);
+ g.test('t')
+ .paramsSimple([{ x: long }])
+ .fn(() => {});
+ g.validate(fileQuery);
+
+ g.test('u')
+ .paramsSimple([{ x: long + 'a' }])
+ .fn(() => {});
+ t.shouldThrow(
+ 'Error',
+ () => {
+ g.validate(fileQuery);
+ },
+ { message: long }
+ );
+});
+
+g.test('param_value,valid').fn(() => {
+ const g = makeTestGroup(UnitTest);
+ g.test('a').paramsSimple([{ x: JSON.stringify({ a: 1, b: 2 }) }]);
+});
+
+g.test('param_value,invalid').fn(t => {
+ for (const badChar of ';=*') {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ const builder = g.test('a');
+ t.shouldThrow('Error', () => {
+ builder.paramsSimple([{ badChar }]);
+ });
+ }
+});
+
+g.test('subcases').fn(async t0 => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('a')
+ .paramsSubcasesOnly(u =>
+ u //
+ .combineWithParams([{ a: 1 }])
+ )
+ .fn(t => {
+ t.expect(t.params.a === 1, 'a must be 1');
+ });
+
+ function* gen({ a, b }: { a?: number; b?: number }) {
+ if (b === 2) {
+ yield { ret: 2 };
+ } else if (a === 1) {
+ yield { ret: 1 };
+ } else {
+ yield { ret: -1 };
+ }
+ }
+ g.test('b')
+ .params(u =>
+ u
+ .combineWithParams([{ a: 1 }, { b: 2 }])
+ .beginSubcases()
+ .expandWithParams(gen)
+ )
+ .fn(t => {
+ const { a, b, ret } = t.params;
+ t.expect((a === 1 && ret === 1) || (b === 2 && ret === 2));
+ });
+
+ const result = await t0.run(g);
+ t0.expect(Array.from(result.values()).every(v => v.status === 'pass'));
+});
+
+g.test('subcases,skip')
+ .desc(
+ 'If all tests are skipped then status is "skip". If at least one test passed, status is "pass"'
+ )
+ .params(u => u.combine('allSkip', [false, true]))
+ .fn(async t0 => {
+ const { allSkip } = t0.params;
+ const g = makeTestGroupForUnitTesting(UnitTest);
+ g.test('a')
+ .params(u => u.beginSubcases().combine('do', ['pass', 'skip', 'pass']))
+ .fn(t => {
+ t.skipIf(allSkip || t.params.do === 'skip');
+ });
+ const result = await t0.run(g);
+ const values = Array.from(result.values());
+ t0.expect(values.length === 1);
+ const expectedStatus = allSkip ? 'skip' : 'pass';
+ t0.expect(
+ values[0].status === expectedStatus,
+ `expect: ${values[0].status} === ${expectedStatus}}, allSkip: ${allSkip}`
+ );
+ });
+
+g.test('exceptions')
+ .params(u =>
+ u
+ .combine('useSubcases', [false, true]) //
+ .combine('useDOMException', [false, true])
+ )
+ .fn(async t0 => {
+ const { useSubcases, useDOMException } = t0.params;
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ const b1 = g.test('a');
+ let b2;
+ if (useSubcases) {
+ b2 = b1.paramsSubcasesOnly(u => u);
+ } else {
+ b2 = b1.params(u => u);
+ }
+ b2.fn(_t => {
+ if (useDOMException) {
+ throw new DOMException('Message!', 'Name!');
+ } else {
+ throw new Error('Message!');
+ }
+ });
+
+ const result = await t0.run(g);
+ const values = Array.from(result.values());
+ t0.expect(values.length === 1);
+ t0.expect(values[0].status === 'fail');
+ });
+
+g.test('throws').fn(async t0 => {
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ g.test('a').fn(_t => {
+ throw new Error();
+ });
+
+ const result = await t0.run(g);
+ const values = Array.from(result.values());
+ t0.expect(values.length === 1);
+ t0.expect(values[0].status === 'fail');
+});
+
+g.test('shouldThrow').fn(async t0 => {
+ t0.shouldThrow('TypeError', () => {
+ throw new TypeError();
+ });
+
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ g.test('a').fn(t => {
+ t.shouldThrow('Error', () => {
+ throw new TypeError();
+ });
+ });
+
+ const result = await t0.run(g);
+ const values = Array.from(result.values());
+ t0.expect(values.length === 1);
+ t0.expect(values[0].status === 'fail');
+});
+
+g.test('shouldReject').fn(async t0 => {
+ t0.shouldReject(
+ 'TypeError',
+ (async () => {
+ throw new TypeError();
+ })()
+ );
+
+ const g = makeTestGroupForUnitTesting(UnitTest);
+
+ g.test('a').fn(t => {
+ t.shouldReject(
+ 'Error',
+ (async () => {
+ throw new TypeError();
+ })()
+ );
+ });
+
+ const result = await t0.run(g);
+ // Fails even though shouldReject doesn't fail until after the test function ends
+ const values = Array.from(result.values());
+ t0.expect(values.length === 1);
+ t0.expect(values[0].status === 'fail');
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/test_group_test.ts b/dom/webgpu/tests/cts/checkout/src/unittests/test_group_test.ts
new file mode 100644
index 0000000000..5fdc02177b
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/test_group_test.ts
@@ -0,0 +1,34 @@
+import { Logger, LogResults } from '../common/internal/logging/logger.js';
+import { TestQuerySingleCase } from '../common/internal/query/query.js';
+import { IterableTestGroup, TestCaseID } from '../common/internal/test_group.js';
+import { objectEquals } from '../common/util/util.js';
+
+import { UnitTest } from './unit_test.js';
+
+export class TestGroupTest extends UnitTest {
+ async run(g: IterableTestGroup): Promise<LogResults> {
+ const logger = new Logger({ overrideDebugMode: true });
+ for (const t of g.iterate()) {
+ for (const rc of t.iterate(null)) {
+ const query = new TestQuerySingleCase('xx', ['yy'], rc.id.test, rc.id.params);
+ const [rec] = logger.record(query.toString());
+ await rc.run(rec, query, []);
+ }
+ }
+ return logger.results;
+ }
+
+ expectCases(g: IterableTestGroup, cases: TestCaseID[]): void {
+ const gcases = [];
+ for (const t of g.iterate()) {
+ gcases.push(...Array.from(t.iterate(null), c => c.id));
+ }
+ this.expect(
+ objectEquals(gcases, cases),
+ `expected
+ ${JSON.stringify(cases)}
+got
+ ${JSON.stringify(gcases)}`
+ );
+ }
+}
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/test_query.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/test_query.spec.ts
new file mode 100644
index 0000000000..4a744c49e9
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/test_query.spec.ts
@@ -0,0 +1,143 @@
+export const description = `
+Tests for TestQuery
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { parseQuery } from '../common/internal/query/parseQuery.js';
+import {
+ TestQueryMultiFile,
+ TestQueryMultiTest,
+ TestQueryMultiCase,
+ TestQuerySingleCase,
+ TestQuery,
+} from '../common/internal/query/query.js';
+
+import { UnitTest } from './unit_test.js';
+
+class F extends UnitTest {
+ expectToString(q: TestQuery, exp: string) {
+ this.expect(q.toString() === exp);
+ }
+
+ expectQueriesEqual(q1: TestQuery, q2: TestQuery) {
+ this.expect(q1.level === q2.level);
+
+ if (q1.level >= 1) {
+ this.expect(q1.isMultiFile === q2.isMultiFile);
+ this.expect(q1.suite === q2.suite);
+ this.expect(q1.filePathParts.length === q2.filePathParts.length);
+ for (let i = 0; i < q1.filePathParts.length; i++) {
+ this.expect(q1.filePathParts[i] === q2.filePathParts[i]);
+ }
+ }
+
+ if (q1.level >= 2) {
+ const p1 = q1 as TestQueryMultiTest;
+ const p2 = q2 as TestQueryMultiTest;
+
+ this.expect(p1.isMultiTest === p2.isMultiTest);
+ this.expect(p1.testPathParts.length === p2.testPathParts.length);
+ for (let i = 0; i < p1.testPathParts.length; i++) {
+ this.expect(p1.testPathParts[i] === p2.testPathParts[i]);
+ }
+ }
+
+ if (q1.level >= 3) {
+ const p1 = q1 as TestQueryMultiCase;
+ const p2 = q2 as TestQueryMultiCase;
+
+ this.expect(p1.isMultiCase === p2.isMultiCase);
+ this.expect(Object.keys(p1.params).length === Object.keys(p2.params).length);
+ for (const key of Object.keys(p1.params)) {
+ this.expect(key in p2.params);
+ const v1 = p1.params[key];
+ const v2 = p2.params[key];
+ this.expect(
+ v1 === v2 ||
+ (typeof v1 === 'number' && isNaN(v1)) === (typeof v2 === 'number' && isNaN(v2))
+ );
+ this.expect(Object.is(v1, -0) === Object.is(v2, -0));
+ }
+ }
+ }
+
+ expectQueryParse(s: string, q: TestQuery) {
+ this.expectQueriesEqual(q, parseQuery(s));
+ }
+}
+
+export const g = makeTestGroup(F);
+
+g.test('constructor').fn(t => {
+ t.shouldThrow('Error', () => new TestQueryMultiTest('suite', [], []));
+
+ t.shouldThrow('Error', () => new TestQueryMultiCase('suite', ['a'], [], {}));
+ t.shouldThrow('Error', () => new TestQueryMultiCase('suite', [], ['c'], {}));
+ t.shouldThrow('Error', () => new TestQueryMultiCase('suite', [], [], {}));
+
+ t.shouldThrow('Error', () => new TestQuerySingleCase('suite', ['a'], [], {}));
+ t.shouldThrow('Error', () => new TestQuerySingleCase('suite', [], ['c'], {}));
+ t.shouldThrow('Error', () => new TestQuerySingleCase('suite', [], [], {}));
+});
+
+g.test('toString').fn(t => {
+ t.expectToString(new TestQueryMultiFile('s', []), 's:*');
+ t.expectToString(new TestQueryMultiFile('s', ['a']), 's:a,*');
+ t.expectToString(new TestQueryMultiFile('s', ['a', 'b']), 's:a,b,*');
+ t.expectToString(new TestQueryMultiTest('s', ['a', 'b'], []), 's:a,b:*');
+ t.expectToString(new TestQueryMultiTest('s', ['a', 'b'], ['c']), 's:a,b:c,*');
+ t.expectToString(new TestQueryMultiTest('s', ['a', 'b'], ['c', 'd']), 's:a,b:c,d,*');
+ t.expectToString(new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], {}), 's:a,b:c,d:*');
+ t.expectToString(
+ new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], { x: 1 }),
+ 's:a,b:c,d:x=1;*'
+ );
+ t.expectToString(
+ new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }),
+ 's:a,b:c,d:x=1;y=2;*'
+ );
+ t.expectToString(
+ new TestQuerySingleCase('s', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 }),
+ 's:a,b:c,d:x=1;y=2'
+ );
+ t.expectToString(new TestQuerySingleCase('s', ['a', 'b'], ['c', 'd'], {}), 's:a,b:c,d:');
+
+ // Test handling of magic param value that convert to NaN/undefined/Infinity/etc.
+ t.expectToString(new TestQuerySingleCase('s', ['a'], ['b'], { c: NaN }), 's:a:b:c="_nan_"');
+ t.expectToString(
+ new TestQuerySingleCase('s', ['a'], ['b'], { c: undefined }),
+ 's:a:b:c="_undef_"'
+ );
+ t.expectToString(new TestQuerySingleCase('s', ['a'], ['b'], { c: -0 }), 's:a:b:c="_negzero_"');
+});
+
+g.test('parseQuery').fn(t => {
+ t.expectQueryParse('s:*', new TestQueryMultiFile('s', []));
+ t.expectQueryParse('s:a,*', new TestQueryMultiFile('s', ['a']));
+ t.expectQueryParse('s:a,b,*', new TestQueryMultiFile('s', ['a', 'b']));
+ t.expectQueryParse('s:a,b:*', new TestQueryMultiTest('s', ['a', 'b'], []));
+ t.expectQueryParse('s:a,b:c,*', new TestQueryMultiTest('s', ['a', 'b'], ['c']));
+ t.expectQueryParse('s:a,b:c,d,*', new TestQueryMultiTest('s', ['a', 'b'], ['c', 'd']));
+ t.expectQueryParse('s:a,b:c,d:*', new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], {}));
+ t.expectQueryParse(
+ 's:a,b:c,d:x=1;*',
+ new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], { x: 1 })
+ );
+ t.expectQueryParse(
+ 's:a,b:c,d:x=1;y=2;*',
+ new TestQueryMultiCase('s', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 })
+ );
+ t.expectQueryParse(
+ 's:a,b:c,d:x=1;y=2',
+ new TestQuerySingleCase('s', ['a', 'b'], ['c', 'd'], { x: 1, y: 2 })
+ );
+ t.expectQueryParse('s:a,b:c,d:', new TestQuerySingleCase('s', ['a', 'b'], ['c', 'd'], {}));
+
+ // Test handling of magic param value that convert to NaN/undefined/Infinity/etc.
+ t.expectQueryParse('s:a:b:c="_nan_"', new TestQuerySingleCase('s', ['a'], ['b'], { c: NaN }));
+ t.expectQueryParse(
+ 's:a:b:c="_undef_"',
+ new TestQuerySingleCase('s', ['a'], ['b'], { c: undefined })
+ );
+ t.expectQueryParse('s:a:b:c="_negzero_"', new TestQuerySingleCase('s', ['a'], ['b'], { c: -0 }));
+});
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/texture_ok.spec.ts b/dom/webgpu/tests/cts/checkout/src/unittests/texture_ok.spec.ts
new file mode 100644
index 0000000000..f1e6971a74
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/texture_ok.spec.ts
@@ -0,0 +1,161 @@
+export const description = `
+Test for texture_ok utils.
+`;
+
+import { makeTestGroup } from '../common/framework/test_group.js';
+import { typedArrayFromParam, typedArrayParam } from '../common/util/util.js';
+import { RegularTextureFormat } from '../webgpu/format_info.js';
+import { TexelView } from '../webgpu/util/texture/texel_view.js';
+import { findFailedPixels } from '../webgpu/util/texture/texture_ok.js';
+
+import { UnitTest } from './unit_test.js';
+
+class F extends UnitTest {
+ test(act: string, exp: string): void {
+ this.expect(act === exp, 'got: ' + act.replace('\n', '⏎'));
+ }
+}
+
+export const g = makeTestGroup(F);
+g.test('findFailedPixels')
+ .desc(
+ `
+ Test findFailedPixels passes what is expected to pass and fails what is expected
+ to fail. For example NaN === NaN should be true in a texture that allows NaN.
+ 2 different representations of the same rgb9e5ufloat should compare as equal.
+ etc...
+ `
+ )
+ .params(u =>
+ u.combineWithParams([
+ // Sanity Check
+ {
+ format: 'rgba8unorm' as RegularTextureFormat,
+ actual: typedArrayParam('Uint8Array', [0x00, 0x40, 0x80, 0xff]),
+ expected: typedArrayParam('Uint8Array', [0x00, 0x40, 0x80, 0xff]),
+ isSame: true,
+ },
+ // Slightly different values
+ {
+ format: 'rgba8unorm' as RegularTextureFormat,
+ actual: typedArrayParam('Uint8Array', [0x00, 0x40, 0x80, 0xff]),
+ expected: typedArrayParam('Uint8Array', [0x00, 0x40, 0x81, 0xff]),
+ isSame: false,
+ },
+ // Different representations of the same value
+ {
+ format: 'rgb9e5ufloat' as RegularTextureFormat,
+ actual: typedArrayParam('Uint8Array', [0x78, 0x56, 0x34, 0x12]),
+ expected: typedArrayParam('Uint8Array', [0xf0, 0xac, 0x68, 0x0c]),
+ isSame: true,
+ },
+ // Slightly different values
+ {
+ format: 'rgb9e5ufloat' as RegularTextureFormat,
+ actual: typedArrayParam('Uint8Array', [0x78, 0x56, 0x34, 0x12]),
+ expected: typedArrayParam('Uint8Array', [0xf1, 0xac, 0x68, 0x0c]),
+ isSame: false,
+ },
+ // Test NaN === NaN
+ {
+ format: 'r32float' as RegularTextureFormat,
+ actual: typedArrayParam('Float32Array', [parseFloat('abc')]),
+ expected: typedArrayParam('Float32Array', [parseFloat('def')]),
+ isSame: true,
+ },
+ // Sanity Check
+ {
+ format: 'r32float' as RegularTextureFormat,
+ actual: typedArrayParam('Float32Array', [1.23]),
+ expected: typedArrayParam('Float32Array', [1.23]),
+ isSame: true,
+ },
+ // Slightly different values.
+ {
+ format: 'r32float' as RegularTextureFormat,
+ actual: typedArrayParam('Uint32Array', [0x3f9d70a4]),
+ expected: typedArrayParam('Uint32Array', [0x3f9d70a5]),
+ isSame: false,
+ },
+ // Slightly different
+ {
+ format: 'rg11b10ufloat' as RegularTextureFormat,
+ actual: typedArrayParam('Uint32Array', [0x3ce]),
+ expected: typedArrayParam('Uint32Array', [0x3cf]),
+ isSame: false,
+ },
+ // Positive.Infinity === Positive.Infinity (red)
+ {
+ format: 'rg11b10ufloat' as RegularTextureFormat,
+ actual: typedArrayParam('Uint32Array', [0b11111000000]),
+ expected: typedArrayParam('Uint32Array', [0b11111000000]),
+ isSame: true,
+ },
+ // Positive.Infinity === Positive.Infinity (green)
+ {
+ format: 'rg11b10ufloat' as RegularTextureFormat,
+ actual: typedArrayParam('Uint32Array', [0b11111000000_00000000000]),
+ expected: typedArrayParam('Uint32Array', [0b11111000000_00000000000]),
+ isSame: true,
+ },
+ // Positive.Infinity === Positive.Infinity (blue)
+ {
+ format: 'rg11b10ufloat' as RegularTextureFormat,
+ actual: typedArrayParam('Uint32Array', [0b1111100000_00000000000_00000000000]),
+ expected: typedArrayParam('Uint32Array', [0b1111100000_00000000000_00000000000]),
+ isSame: true,
+ },
+ // NaN === NaN (red)
+ {
+ format: 'rg11b10ufloat' as RegularTextureFormat,
+ actual: typedArrayParam('Uint32Array', [0b11111000001]),
+ expected: typedArrayParam('Uint32Array', [0b11111000010]),
+ isSame: true,
+ },
+ // NaN === NaN (green)
+ {
+ format: 'rg11b10ufloat' as RegularTextureFormat,
+ actual: typedArrayParam('Uint32Array', [0b11111000100_00000000000]),
+ expected: typedArrayParam('Uint32Array', [0b11111001000_00000000000]),
+ isSame: true,
+ },
+ // NaN === NaN (blue)
+ {
+ format: 'rg11b10ufloat' as RegularTextureFormat,
+ actual: typedArrayParam('Uint32Array', [0b1111110000_00000000000_00000000000]),
+ expected: typedArrayParam('Uint32Array', [0b1111101000_00000000000_00000000000]),
+ isSame: true,
+ },
+ ])
+ )
+ .fn(t => {
+ const { format, actual, expected, isSame } = t.params;
+ const actualData = new Uint8Array(typedArrayFromParam(actual).buffer);
+ const expectedData = new Uint8Array(typedArrayFromParam(expected).buffer);
+
+ const actTexelView = TexelView.fromTextureDataByReference(format, actualData, {
+ bytesPerRow: actualData.byteLength,
+ rowsPerImage: 1,
+ subrectOrigin: [0, 0, 0],
+ subrectSize: [1, 1, 1],
+ });
+ const expTexelView = TexelView.fromTextureDataByReference(format, expectedData, {
+ bytesPerRow: expectedData.byteLength,
+ rowsPerImage: 1,
+ subrectOrigin: [0, 0, 0],
+ subrectSize: [1, 1, 1],
+ });
+
+ const zero = { x: 0, y: 0, z: 0 };
+ const failedPixelsMessage = findFailedPixels(
+ format,
+ zero,
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
+ { actTexelView, expTexelView },
+ {
+ maxFractionalDiff: 0,
+ }
+ );
+
+ t.expect(isSame === !failedPixelsMessage, failedPixelsMessage);
+ });
diff --git a/dom/webgpu/tests/cts/checkout/src/unittests/unit_test.ts b/dom/webgpu/tests/cts/checkout/src/unittests/unit_test.ts
new file mode 100644
index 0000000000..876780e151
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/unittests/unit_test.ts
@@ -0,0 +1,3 @@
+import { Fixture } from '../common/framework/fixture.js';
+
+export class UnitTest extends Fixture {}