diff options
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/unittests')
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 {} |