From 0cd07292e9d18f88c72f49201ad4c632da156683 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Fri, 16 Oct 2026 13:01:38 +0200 Subject: [PATCH] feat: schemasEqualStructurally & schemasEqualNominally --- .../typegpu-react/src/core/helper-hooks.ts | 2 +- packages/typegpu/src/core/slot/accessor.ts | 9 +- packages/typegpu/src/data/index.ts | 6 +- .../data/{deepEqual.ts => schemasEqual.ts} | 64 +++-- packages/typegpu/tests/data/deepEqual.test.ts | 221 --------------- .../tests/data/schemasEqualNominally.test.ts | 258 ++++++++++++++++++ .../data/schemasEqualStructurally.test.ts | 229 ++++++++++++++++ .../typegpu/tests/internal/deepEqual.test.ts | 12 - .../internal/schemasEqualNominally.test.ts | 12 + .../internal/schemasEqualStructurally.test.ts | 12 + packages/typegpu/tests/serial.test.ts | 27 +- .../typegpu/tests/serializeDataSchema.test.ts | 6 +- 12 files changed, 590 insertions(+), 268 deletions(-) rename packages/typegpu/src/data/{deepEqual.ts => schemasEqual.ts} (51%) delete mode 100644 packages/typegpu/tests/data/deepEqual.test.ts create mode 100644 packages/typegpu/tests/data/schemasEqualNominally.test.ts create mode 100644 packages/typegpu/tests/data/schemasEqualStructurally.test.ts delete mode 100644 packages/typegpu/tests/internal/deepEqual.test.ts create mode 100644 packages/typegpu/tests/internal/schemasEqualNominally.test.ts create mode 100644 packages/typegpu/tests/internal/schemasEqualStructurally.test.ts diff --git a/packages/typegpu-react/src/core/helper-hooks.ts b/packages/typegpu-react/src/core/helper-hooks.ts index 0b3745f775..1efba71eaf 100644 --- a/packages/typegpu-react/src/core/helper-hooks.ts +++ b/packages/typegpu-react/src/core/helper-hooks.ts @@ -6,7 +6,7 @@ import useStableEvent from './use-stable-event.ts'; export function useStableSchema(schema: T): [T, /* schemaChanged */ boolean] { const prevSchemaRef = useRef(schema as d.AnyData); - if (!d.deepEqual(prevSchemaRef.current, schema as d.AnyData)) { + if (!d.schemasEqualStructurally(prevSchemaRef.current, schema as d.AnyData)) { prevSchemaRef.current = schema as d.AnyData; return [schema, true]; } diff --git a/packages/typegpu/src/core/slot/accessor.ts b/packages/typegpu/src/core/slot/accessor.ts index c9e72ee039..ce9d5af520 100644 --- a/packages/typegpu/src/core/slot/accessor.ts +++ b/packages/typegpu/src/core/slot/accessor.ts @@ -1,7 +1,7 @@ import { type AnyData, isData, undecorate } from '../../data/dataTypes.ts'; import { schemaCallWrapper } from '../../data/schemaCallWrapper.ts'; import { isSnippet, type Origin, snip, type Snippet } from '../../data/snippet.ts'; -import { deepEqual } from '../../data/deepEqual.ts'; +import { schemasEqualStructurally } from '../../data/schemasEqual.ts'; import { arrayOf } from '../../data/array.ts'; import { UnknownData } from '../../data/dataTypes.ts'; import { type AnyWgslData, type BaseData, isAbstract, isWgslArray } from '../../data/wgslTypes.ts'; @@ -103,9 +103,12 @@ function matchesSchema(accessorSchema: BaseData, providedType: BaseData | Unknow const expected = undecorate(accessorSchema); const provided = undecorate(providedType); if (isWgslArray(expected) && expected.elementCount === 0 && isWgslArray(provided)) { - return deepEqual(expected.elementType as AnyData, provided.elementType as AnyData); + return schemasEqualStructurally( + expected.elementType as AnyData, + provided.elementType as AnyData, + ); } - return deepEqual(expected as AnyData, provided as AnyData); + return schemasEqualStructurally(expected as AnyData, provided as AnyData); } /** diff --git a/packages/typegpu/src/data/index.ts b/packages/typegpu/src/data/index.ts index 8bcad1cfd9..dc2f633db4 100644 --- a/packages/typegpu/src/data/index.ts +++ b/packages/typegpu/src/data/index.ts @@ -200,7 +200,11 @@ export { PUBLIC_getLongestContiguousPrefix as getLongestContiguousPrefix } from export { memoryLayoutOf } from './offsetUtils.ts'; export { PUBLIC_alignmentOf as alignmentOf } from './alignmentOf.ts'; export { builtin } from '../builtin.ts'; -export { deepEqual } from './deepEqual.ts'; +export { + schemasEqualStructurally, + schemasEqualNominally, + /** @deprecated Use `schemasEqualStructurally` instead */ schemasEqualStructurally as deepEqual, +} from './schemasEqual.ts'; export type { AnyBuiltin, BuiltinClipDistances, diff --git a/packages/typegpu/src/data/deepEqual.ts b/packages/typegpu/src/data/schemasEqual.ts similarity index 51% rename from packages/typegpu/src/data/deepEqual.ts rename to packages/typegpu/src/data/schemasEqual.ts index 96881335e0..283c86e9ec 100644 --- a/packages/typegpu/src/data/deepEqual.ts +++ b/packages/typegpu/src/data/schemasEqual.ts @@ -5,25 +5,48 @@ import { isWgslStorageTexture, isWgslTexture } from './texture.ts'; import { isAtomic, isDecorated, isPtr, isWgslArray, isWgslStruct } from './wgslTypes.ts'; /** - * Performs a deep comparison of two TypeGPU data schemas. + * Performs a structural comparison of two TypeGPU data schemas. This + * means two struct definitions are considered equal when they have the + * same properties. Use `schemasEqualNominally` for a nominal comparison. * * @param a The first data schema to compare. * @param b The second data schema to compare. - * @returns `true` if the schemas are deeply equal, `false` otherwise. + * @returns `true` if the schemas are structurally equal, `false` otherwise. * * @example - * ```ts - * import { vec3f, struct, deepEqual } from 'typegpu/data'; + * import { vec3f, struct, schemasEqualStructurally } from 'typegpu/data'; * - * const schema1 = struct({ a: vec3f }); - * const schema2 = struct({ a: vec3f }); - * const schema3 = struct({ b: vec3f }); + * console.log(schemasEqualStructurally(struct({ a: vec3f }), struct({ a: vec3f }))); // true + * console.log(schemasEqualStructurally(struct({ a: vec3f }), struct({ b: vec3f }))); // false + */ +export function schemasEqualStructurally(a: AnyData, b: AnyData): boolean { + return schemasEqual(a, b, true); +} + +/** + * Performs a nominal comparison of two TypeGPU data schemas. This + * means two struct definitions are only considered equal when they + * have the same object identity. Use `schemasEqualStructurally` for + * a structural comparison. * - * console.log(deepEqual(schema1, schema2)); // true - * console.log(deepEqual(schema1, schema3)); // false - * ``` + * @param a The first data schema to compare. + * @param b The second data schema to compare. + * @returns `true` if the schemas are nominally equal, `false` otherwise. + * + * @example + * import { vec3f, struct, schemasEqualNominally } from 'typegpu/data'; + * + * const struct1 = struct({ a: vec3f }); + * const struct2 = struct({ a: vec3f }); + * + * console.log(schemasEqualNominally(d.arrayOf(struct1, 2), d.arrayOf(struct1, 2))); // true + * console.log(schemasEqualNominally(struct1, struct2)); // false */ -export function deepEqual(a: AnyData, b: AnyData): boolean { +export function schemasEqualNominally(a: AnyData, b: AnyData): boolean { + return schemasEqual(a, b, false); +} + +function schemasEqual(a: AnyData, b: AnyData, structural: boolean): boolean { if (a === b) { return true; } @@ -33,6 +56,13 @@ export function deepEqual(a: AnyData, b: AnyData): boolean { } if ((isWgslStruct(a) && isWgslStruct(b)) || (isUnstruct(a) && isUnstruct(b))) { + if (!structural) { + // With nominal typing, structs are only considered equal when they + // generate the same declaration, i.e. have the same object identity. + // If they didn't pass the previous condition, then they're not equal + return false; + } + const aProps = a.propTypes; const bProps = b.propTypes; const aKeys = Object.keys(aProps); @@ -49,7 +79,7 @@ export function deepEqual(a: AnyData, b: AnyData): boolean { keyA !== keyB || !keyA || !keyB || - !deepEqual(aProps[keyA] as AnyData, bProps[keyB] as AnyData) + !schemasEqual(aProps[keyA] as AnyData, bProps[keyB] as AnyData, structural) ) { return false; } @@ -60,7 +90,7 @@ export function deepEqual(a: AnyData, b: AnyData): boolean { if ((isWgslArray(a) && isWgslArray(b)) || (isDisarray(a) && isDisarray(b))) { return ( a.elementCount === b.elementCount && - deepEqual(a.elementType as AnyData, b.elementType as AnyData) + schemasEqual(a.elementType as AnyData, b.elementType as AnyData, structural) ); } @@ -69,17 +99,17 @@ export function deepEqual(a: AnyData, b: AnyData): boolean { a.addressSpace === b.addressSpace && a.access === b.access && a.implicit === b.implicit && - deepEqual(a.inner as AnyData, b.inner as AnyData) + schemasEqual(a.inner as AnyData, b.inner as AnyData, structural) ); } if (isAtomic(a) && isAtomic(b)) { - return deepEqual(a.inner, b.inner); + return schemasEqual(a.inner, b.inner, structural); } if (isWgslTexture(a) && isWgslTexture(b)) { // Dimension and multisampling are already encoded in the type - return deepEqual(a.sampleType as AnyData, b.sampleType as AnyData); + return schemasEqual(a.sampleType as AnyData, b.sampleType as AnyData, structural); } if (isWgslStorageTexture(a) && isWgslStorageTexture(b)) { @@ -88,7 +118,7 @@ export function deepEqual(a: AnyData, b: AnyData): boolean { } if ((isDecorated(a) && isDecorated(b)) || (isLooseDecorated(a) && isLooseDecorated(b))) { - if (!deepEqual(a.inner as AnyData, b.inner as AnyData)) { + if (!schemasEqual(a.inner as AnyData, b.inner as AnyData, structural)) { return false; } if (a.attribs.length !== b.attribs.length) { diff --git a/packages/typegpu/tests/data/deepEqual.test.ts b/packages/typegpu/tests/data/deepEqual.test.ts deleted file mode 100644 index 51072ec77d..0000000000 --- a/packages/typegpu/tests/data/deepEqual.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - align, - arrayOf, - atomic, - deepEqual, - disarrayOf, - f16, - f32, - i32, - location, - mat2x2f, - mat3x3f, - size, - struct, - u32, - uint16x2, - uint32, - uint8x4, - unstruct, - vec2f, - vec2u, - vec3f, - ptrPrivate, - ptrStorage, - ptrWorkgroup, - builtin, - interpolate, - texture2d, - textureMultisampled2d, - textureStorage2d, -} from 'typegpu/data'; - -describe('deepEqual', () => { - it('compares simple types', () => { - expect(deepEqual(f32, f32)).toBe(true); - expect(deepEqual(u32, u32)).toBe(true); - expect(deepEqual(f32, u32)).toBe(false); - expect(deepEqual(f32, f16)).toBe(false); - }); - - it('compares vector types', () => { - expect(deepEqual(vec2f, vec2f)).toBe(true); - expect(deepEqual(vec3f, vec3f)).toBe(true); - expect(deepEqual(vec2f, vec3f)).toBe(false); - expect(deepEqual(vec2f, vec2u)).toBe(false); - }); - - it('compares matrix types', () => { - expect(deepEqual(mat2x2f, mat2x2f)).toBe(true); - expect(deepEqual(mat3x3f, mat3x3f)).toBe(true); - expect(deepEqual(mat2x2f, mat3x3f)).toBe(false); - }); - - it('compares struct types', () => { - const struct1 = struct({ a: f32, b: vec2u }); - const struct2 = struct({ a: f32, b: vec2u }); - const struct3 = struct({ b: vec2u, a: f32 }); // different order - const struct4 = struct({ a: u32, b: vec2u }); // different prop type - const struct5 = struct({ a: f32, c: vec2u }); // different prop name - const struct6 = struct({ a: f32 }); // different number of props - - expect(deepEqual(struct1, struct2)).toBe(true); - expect(deepEqual(struct1, struct3)).toBe(false); // property order should matter - expect(deepEqual(struct1, struct4)).toBe(false); - expect(deepEqual(struct1, struct5)).toBe(false); - expect(deepEqual(struct1, struct6)).toBe(false); - }); - - it('compares nested struct types', () => { - const nested1 = struct({ c: i32 }); - const nested2 = struct({ c: i32 }); - const nested3 = struct({ c: u32 }); - - const struct1 = struct({ a: f32, b: nested1 }); - const struct2 = struct({ a: f32, b: nested2 }); - const struct3 = struct({ a: f32, b: nested3 }); - - expect(deepEqual(struct1, struct2)).toBe(true); - expect(deepEqual(struct1, struct3)).toBe(false); - }); - - it('compares array types', () => { - const array1 = arrayOf(f32, 4); - const array2 = arrayOf(f32, 4); - const array3 = arrayOf(u32, 4); - const array4 = arrayOf(f32, 5); - - expect(deepEqual(array1, array2)).toBe(true); - expect(deepEqual(array1, array3)).toBe(false); - expect(deepEqual(array1, array4)).toBe(false); - }); - - it('compares arrays of structs', () => { - const struct1 = struct({ a: f32 }); - const struct2 = struct({ a: f32 }); - const struct3 = struct({ a: u32 }); - - const array1 = arrayOf(struct1, 2); - const array2 = arrayOf(struct2, 2); - const array3 = arrayOf(struct3, 2); - - expect(deepEqual(array1, array2)).toBe(true); - expect(deepEqual(array1, array3)).toBe(false); - }); - - it('compares decorated types', () => { - const decorated1 = align(16, f32); - const decorated2 = align(16, f32); - const decorated3 = align(8, f32); - const decorated4 = align(16, u32); - const decorated5 = location(0, f32); - const decorated6 = size(8, align(16, u32)); - const decorated7 = align(16, size(8, u32)); - - expect(deepEqual(decorated1, decorated2)).toBe(true); - expect(deepEqual(decorated1, decorated3)).toBe(false); - expect(deepEqual(decorated1, decorated4)).toBe(false); - expect(deepEqual(decorated1, decorated5)).toBe(false); - expect(deepEqual(decorated6, decorated7)).toBe(false); // decorator order should matter - }); - - it('compares pointer types', () => { - const ptr1 = ptrPrivate(f32); - const ptr2 = ptrPrivate(f32); - const ptr3 = ptrWorkgroup(f32); - const ptr4 = ptrPrivate(u32); - const ptr5 = ptrStorage(f32, 'read'); - const ptr6 = ptrStorage(f32, 'read-write'); - - expect(deepEqual(ptr1, ptr2)).toBe(true); - expect(deepEqual(ptr1, ptr3)).toBe(false); - expect(deepEqual(ptr1, ptr4)).toBe(false); - expect(deepEqual(ptr5, ptr6)).toBe(false); - expect(deepEqual(ptrStorage(f32, 'read'), ptrStorage(f32, 'read'))).toBe(true); - }); - - it('compares sampled texture types', () => { - expect(deepEqual(texture2d(f32), texture2d(f32))).toBe(true); - expect(deepEqual(texture2d(f32), texture2d(i32))).toBe(false); - expect(deepEqual(texture2d(u32), texture2d(i32))).toBe(false); - expect(deepEqual(textureMultisampled2d(f32), textureMultisampled2d(u32))).toBe(false); - }); - - it('compares storage texture types', () => { - expect(deepEqual(textureStorage2d('rgba8unorm'), textureStorage2d('rgba8unorm'))).toBe(true); - expect(deepEqual(textureStorage2d('rgba8unorm'), textureStorage2d('rgba32float'))).toBe(false); - expect( - deepEqual( - textureStorage2d('rgba8unorm', 'read-only'), - textureStorage2d('rgba8unorm', 'write-only'), - ), - ).toBe(false); - }); - - it('compares attribute params', () => { - expect(deepEqual(builtin.vertexIndex, builtin.instanceIndex)).toBe(false); - expect(deepEqual(interpolate('flat, first', u32), interpolate('flat, either', u32))).toBe( - false, - ); - expect(deepEqual(interpolate('flat, first', u32), interpolate('flat, first', u32))).toBe(true); - }); - - it('compares atomic types', () => { - const atomic1 = atomic(u32); - const atomic2 = atomic(u32); - const atomic3 = atomic(i32); - - expect(deepEqual(atomic1, atomic2)).toBe(true); - expect(deepEqual(atomic1, atomic3)).toBe(false); - }); - - it('compares loose decorated types', () => { - const decorated1 = align(16, unstruct({ a: f32 })); - const decorated2 = align(16, unstruct({ a: f32 })); - const decorated3 = align(8, unstruct({ a: f32 })); - const decorated4 = align(16, unstruct({ a: u32 })); - const decorated5 = location(0, unstruct({ a: f32 })); - - expect(deepEqual(decorated1, decorated2)).toBe(true); - expect(deepEqual(decorated1, decorated3)).toBe(false); - expect(deepEqual(decorated1, decorated4)).toBe(false); - expect(deepEqual(decorated1, decorated5)).toBe(false); - }); - - it('compares loose data types', () => { - const unstruct1 = unstruct({ a: f32 }); - const unstruct2 = unstruct({ a: f32 }); - const unstruct3 = unstruct({ b: f32 }); - - const disarray1 = disarrayOf(u32, 4); - const disarray2 = disarrayOf(u32, 4); - const disarray3 = disarrayOf(u32, 5); - - expect(deepEqual(unstruct1, unstruct2)).toBe(true); - expect(deepEqual(unstruct1, unstruct3)).toBe(false); - expect(deepEqual(disarray1, disarray2)).toBe(true); - expect(deepEqual(disarray1, disarray3)).toBe(false); - }); - - it('compares vertex formats', () => { - expect(deepEqual(uint16x2, uint8x4)).toBe(false); - expect(deepEqual(uint16x2, uint32)).toBe(false); - expect(deepEqual(uint16x2, uint16x2)).toBe(true); - }); - - it('compares unstructs with vertex formats', () => { - const unstruct1 = unstruct({ a: uint16x2 }); - const unstruct2 = unstruct({ a: uint16x2 }); - const unstruct3 = unstruct({ a: uint32 }); - expect(deepEqual(unstruct1, unstruct2)).toBe(true); - expect(deepEqual(unstruct1, unstruct3)).toBe(false); - }); - - it('compares different kinds of types', () => { - expect(deepEqual(f32, vec2f)).toBe(false); - expect(deepEqual(struct({ a: f32 }), unstruct({ a: f32 }))).toBe(false); - expect(deepEqual(arrayOf(f32, 4), disarrayOf(f32, 4))).toBe(false); - expect(deepEqual(struct({ a: f32 }), f32)).toBe(false); - }); -}); diff --git a/packages/typegpu/tests/data/schemasEqualNominally.test.ts b/packages/typegpu/tests/data/schemasEqualNominally.test.ts new file mode 100644 index 0000000000..865ffa134a --- /dev/null +++ b/packages/typegpu/tests/data/schemasEqualNominally.test.ts @@ -0,0 +1,258 @@ +import { describe, expect, it } from 'vitest'; +import { + align, + arrayOf, + atomic, + schemasEqualNominally, + disarrayOf, + f16, + f32, + i32, + location, + mat2x2f, + mat3x3f, + size, + struct, + u32, + uint16x2, + uint32, + uint8x4, + unstruct, + vec2f, + vec2u, + vec3f, + ptrPrivate, + ptrStorage, + ptrWorkgroup, + builtin, + interpolate, + texture2d, + textureMultisampled2d, + textureStorage2d, +} from 'typegpu/data'; + +describe('schemasEqualNominally', () => { + it('compares simple types', () => { + expect(schemasEqualNominally(f32, f32)).toBe(true); + expect(schemasEqualNominally(u32, u32)).toBe(true); + expect(schemasEqualNominally(f32, u32)).toBe(false); + expect(schemasEqualNominally(f32, f16)).toBe(false); + }); + + it('compares vector types', () => { + expect(schemasEqualNominally(vec2f, vec2f)).toBe(true); + expect(schemasEqualNominally(vec3f, vec3f)).toBe(true); + expect(schemasEqualNominally(vec2f, vec3f)).toBe(false); + expect(schemasEqualNominally(vec2f, vec2u)).toBe(false); + }); + + it('compares matrix types', () => { + expect(schemasEqualNominally(mat2x2f, mat2x2f)).toBe(true); + expect(schemasEqualNominally(mat3x3f, mat3x3f)).toBe(true); + expect(schemasEqualNominally(mat2x2f, mat3x3f)).toBe(false); + }); + + it('compares struct types by identity', () => { + const struct1 = struct({ a: f32, b: vec2u }); + const struct2 = struct({ a: f32, b: vec2u }); // same shape, different identity + const struct3 = struct({ b: vec2u, a: f32 }); // different order + const struct4 = struct({ a: u32, b: vec2u }); // different prop type + const struct5 = struct({ a: f32, c: vec2u }); // different prop name + const struct6 = struct({ a: f32 }); // different number of props + + expect(schemasEqualNominally(struct1, struct1)).toBe(true); + expect(schemasEqualNominally(struct1, struct2)).toBe(false); + expect(schemasEqualNominally(struct1, struct3)).toBe(false); + expect(schemasEqualNominally(struct1, struct4)).toBe(false); + expect(schemasEqualNominally(struct1, struct5)).toBe(false); + expect(schemasEqualNominally(struct1, struct6)).toBe(false); + }); + + it('compares nested struct types by identity', () => { + const nested1 = struct({ c: i32 }); + const nested2 = struct({ c: i32 }); + + const struct1 = struct({ a: f32, b: nested1 }); + const struct2 = struct({ a: f32, b: nested1 }); // same nested struct, different identity + const struct3 = struct({ a: f32, b: nested2 }); + + expect(schemasEqualNominally(struct1, struct1)).toBe(true); + expect(schemasEqualNominally(struct1, struct2)).toBe(false); + expect(schemasEqualNominally(struct1, struct3)).toBe(false); + }); + + it('compares array types', () => { + const array1 = arrayOf(f32, 4); + const array2 = arrayOf(f32, 4); + const array3 = arrayOf(u32, 4); + const array4 = arrayOf(f32, 5); + + expect(schemasEqualNominally(array1, array2)).toBe(true); + expect(schemasEqualNominally(array1, array3)).toBe(false); + expect(schemasEqualNominally(array1, array4)).toBe(false); + }); + + it('compares arrays of structs', () => { + const struct1 = struct({ a: f32 }); + const struct2 = struct({ a: f32 }); + const struct3 = struct({ a: u32 }); + + const array1 = arrayOf(struct1, 2); + const array2 = arrayOf(struct1, 2); + const array3 = arrayOf(struct2, 2); + const array4 = arrayOf(struct3, 2); + + expect(schemasEqualNominally(array1, array2)).toBe(true); + expect(schemasEqualNominally(array1, array3)).toBe(false); + expect(schemasEqualNominally(array1, array4)).toBe(false); + }); + + it('compares decorated types', () => { + const decorated1 = align(16, f32); + const decorated2 = align(16, f32); + const decorated3 = align(8, f32); + const decorated4 = align(16, u32); + const decorated5 = location(0, f32); + const decorated6 = size(8, align(16, u32)); + const decorated7 = align(16, size(8, u32)); + + expect(schemasEqualNominally(decorated1, decorated2)).toBe(true); + expect(schemasEqualNominally(decorated1, decorated3)).toBe(false); + expect(schemasEqualNominally(decorated1, decorated4)).toBe(false); + expect(schemasEqualNominally(decorated1, decorated5)).toBe(false); + expect(schemasEqualNominally(decorated6, decorated7)).toBe(false); // decorator order should matter + }); + + it('compares decorated structs by identity', () => { + const struct1 = struct({ a: f32 }); + const struct2 = struct({ a: f32 }); + + expect(schemasEqualNominally(align(16, struct1), align(16, struct1))).toBe(true); + expect(schemasEqualNominally(align(16, struct1), align(16, struct2))).toBe(false); + }); + + it('compares pointer types', () => { + const ptr1 = ptrPrivate(f32); + const ptr2 = ptrPrivate(f32); + const ptr3 = ptrWorkgroup(f32); + const ptr4 = ptrPrivate(u32); + const ptr5 = ptrStorage(f32, 'read'); + const ptr6 = ptrStorage(f32, 'read-write'); + + expect(schemasEqualNominally(ptr1, ptr2)).toBe(true); + expect(schemasEqualNominally(ptr1, ptr3)).toBe(false); + expect(schemasEqualNominally(ptr1, ptr4)).toBe(false); + expect(schemasEqualNominally(ptr5, ptr6)).toBe(false); + expect(schemasEqualNominally(ptrStorage(f32, 'read'), ptrStorage(f32, 'read'))).toBe(true); + }); + + it('compares pointers to structs by identity', () => { + const struct1 = struct({ a: f32 }); + const struct2 = struct({ a: f32 }); + + expect(schemasEqualNominally(ptrPrivate(struct1), ptrPrivate(struct1))).toBe(true); + expect(schemasEqualNominally(ptrPrivate(struct1), ptrPrivate(struct2))).toBe(false); + }); + + it('compares sampled texture types', () => { + expect(schemasEqualNominally(texture2d(f32), texture2d(f32))).toBe(true); + expect(schemasEqualNominally(texture2d(f32), texture2d(i32))).toBe(false); + expect(schemasEqualNominally(texture2d(u32), texture2d(i32))).toBe(false); + expect(schemasEqualNominally(textureMultisampled2d(f32), textureMultisampled2d(u32))).toBe( + false, + ); + }); + + it('compares storage texture types', () => { + expect( + schemasEqualNominally(textureStorage2d('rgba8unorm'), textureStorage2d('rgba8unorm')), + ).toBe(true); + expect( + schemasEqualNominally(textureStorage2d('rgba8unorm'), textureStorage2d('rgba32float')), + ).toBe(false); + expect( + schemasEqualNominally( + textureStorage2d('rgba8unorm', 'read-only'), + textureStorage2d('rgba8unorm', 'write-only'), + ), + ).toBe(false); + }); + + it('compares attribute params', () => { + expect(schemasEqualNominally(builtin.vertexIndex, builtin.instanceIndex)).toBe(false); + expect( + schemasEqualNominally(interpolate('flat, first', u32), interpolate('flat, either', u32)), + ).toBe(false); + expect( + schemasEqualNominally(interpolate('flat, first', u32), interpolate('flat, first', u32)), + ).toBe(true); + }); + + it('compares atomic types', () => { + const atomic1 = atomic(u32); + const atomic2 = atomic(u32); + const atomic3 = atomic(i32); + + expect(schemasEqualNominally(atomic1, atomic2)).toBe(true); + expect(schemasEqualNominally(atomic1, atomic3)).toBe(false); + }); + + it('compares loose decorated types', () => { + const unstruct1 = unstruct({ a: f32 }); + const unstruct2 = unstruct({ a: f32 }); + + const decorated1 = align(16, unstruct1); + const decorated2 = align(16, unstruct1); + const decorated3 = align(16, unstruct2); // same shape, different identity + const decorated4 = align(8, unstruct1); + const decorated5 = location(0, unstruct1); + + expect(schemasEqualNominally(decorated1, decorated2)).toBe(true); + expect(schemasEqualNominally(decorated1, decorated3)).toBe(false); + expect(schemasEqualNominally(decorated1, decorated4)).toBe(false); + expect(schemasEqualNominally(decorated1, decorated5)).toBe(false); + }); + + it('compares loose data types', () => { + const unstruct1 = unstruct({ a: f32 }); + const unstruct2 = unstruct({ a: f32 }); + const unstruct3 = unstruct({ b: f32 }); + + const disarray1 = disarrayOf(u32, 4); + const disarray2 = disarrayOf(u32, 4); + const disarray3 = disarrayOf(u32, 5); + const disarray4 = disarrayOf(unstruct1, 4); + const disarray5 = disarrayOf(unstruct1, 4); + const disarray6 = disarrayOf(unstruct2, 4); + + expect(schemasEqualNominally(unstruct1, unstruct1)).toBe(true); + expect(schemasEqualNominally(unstruct1, unstruct2)).toBe(false); + expect(schemasEqualNominally(unstruct1, unstruct3)).toBe(false); + expect(schemasEqualNominally(disarray1, disarray2)).toBe(true); + expect(schemasEqualNominally(disarray1, disarray3)).toBe(false); + expect(schemasEqualNominally(disarray4, disarray5)).toBe(true); + expect(schemasEqualNominally(disarray4, disarray6)).toBe(false); + }); + + it('compares vertex formats', () => { + expect(schemasEqualNominally(uint16x2, uint8x4)).toBe(false); + expect(schemasEqualNominally(uint16x2, uint32)).toBe(false); + expect(schemasEqualNominally(uint16x2, uint16x2)).toBe(true); + }); + + it('compares unstructs with vertex formats by identity', () => { + const unstruct1 = unstruct({ a: uint16x2 }); + const unstruct2 = unstruct({ a: uint16x2 }); + const unstruct3 = unstruct({ a: uint32 }); + expect(schemasEqualNominally(unstruct1, unstruct1)).toBe(true); + expect(schemasEqualNominally(unstruct1, unstruct2)).toBe(false); + expect(schemasEqualNominally(unstruct1, unstruct3)).toBe(false); + }); + + it('compares different kinds of types', () => { + expect(schemasEqualNominally(f32, vec2f)).toBe(false); + expect(schemasEqualNominally(struct({ a: f32 }), unstruct({ a: f32 }))).toBe(false); + expect(schemasEqualNominally(arrayOf(f32, 4), disarrayOf(f32, 4))).toBe(false); + expect(schemasEqualNominally(struct({ a: f32 }), f32)).toBe(false); + }); +}); diff --git a/packages/typegpu/tests/data/schemasEqualStructurally.test.ts b/packages/typegpu/tests/data/schemasEqualStructurally.test.ts new file mode 100644 index 0000000000..04a02881b1 --- /dev/null +++ b/packages/typegpu/tests/data/schemasEqualStructurally.test.ts @@ -0,0 +1,229 @@ +import { describe, expect, it } from 'vitest'; +import { + align, + arrayOf, + atomic, + schemasEqualStructurally, + disarrayOf, + f16, + f32, + i32, + location, + mat2x2f, + mat3x3f, + size, + struct, + u32, + uint16x2, + uint32, + uint8x4, + unstruct, + vec2f, + vec2u, + vec3f, + ptrPrivate, + ptrStorage, + ptrWorkgroup, + builtin, + interpolate, + texture2d, + textureMultisampled2d, + textureStorage2d, +} from 'typegpu/data'; + +describe('schemasEqualStructurally', () => { + it('compares simple types', () => { + expect(schemasEqualStructurally(f32, f32)).toBe(true); + expect(schemasEqualStructurally(u32, u32)).toBe(true); + expect(schemasEqualStructurally(f32, u32)).toBe(false); + expect(schemasEqualStructurally(f32, f16)).toBe(false); + }); + + it('compares vector types', () => { + expect(schemasEqualStructurally(vec2f, vec2f)).toBe(true); + expect(schemasEqualStructurally(vec3f, vec3f)).toBe(true); + expect(schemasEqualStructurally(vec2f, vec3f)).toBe(false); + expect(schemasEqualStructurally(vec2f, vec2u)).toBe(false); + }); + + it('compares matrix types', () => { + expect(schemasEqualStructurally(mat2x2f, mat2x2f)).toBe(true); + expect(schemasEqualStructurally(mat3x3f, mat3x3f)).toBe(true); + expect(schemasEqualStructurally(mat2x2f, mat3x3f)).toBe(false); + }); + + it('compares struct types', () => { + const struct1 = struct({ a: f32, b: vec2u }); + const struct2 = struct({ a: f32, b: vec2u }); + const struct3 = struct({ b: vec2u, a: f32 }); // different order + const struct4 = struct({ a: u32, b: vec2u }); // different prop type + const struct5 = struct({ a: f32, c: vec2u }); // different prop name + const struct6 = struct({ a: f32 }); // different number of props + + expect(schemasEqualStructurally(struct1, struct2)).toBe(true); + expect(schemasEqualStructurally(struct1, struct3)).toBe(false); // property order should matter + expect(schemasEqualStructurally(struct1, struct4)).toBe(false); + expect(schemasEqualStructurally(struct1, struct5)).toBe(false); + expect(schemasEqualStructurally(struct1, struct6)).toBe(false); + }); + + it('compares nested struct types', () => { + const nested1 = struct({ c: i32 }); + const nested2 = struct({ c: i32 }); + const nested3 = struct({ c: u32 }); + + const struct1 = struct({ a: f32, b: nested1 }); + const struct2 = struct({ a: f32, b: nested2 }); + const struct3 = struct({ a: f32, b: nested3 }); + + expect(schemasEqualStructurally(struct1, struct2)).toBe(true); + expect(schemasEqualStructurally(struct1, struct3)).toBe(false); + }); + + it('compares array types', () => { + const array1 = arrayOf(f32, 4); + const array2 = arrayOf(f32, 4); + const array3 = arrayOf(u32, 4); + const array4 = arrayOf(f32, 5); + + expect(schemasEqualStructurally(array1, array2)).toBe(true); + expect(schemasEqualStructurally(array1, array3)).toBe(false); + expect(schemasEqualStructurally(array1, array4)).toBe(false); + }); + + it('compares arrays of structs', () => { + const struct1 = struct({ a: f32 }); + const struct2 = struct({ a: f32 }); + const struct3 = struct({ a: u32 }); + + const array1 = arrayOf(struct1, 2); + const array2 = arrayOf(struct2, 2); + const array3 = arrayOf(struct3, 2); + + expect(schemasEqualStructurally(array1, array2)).toBe(true); + expect(schemasEqualStructurally(array1, array3)).toBe(false); + }); + + it('compares decorated types', () => { + const decorated1 = align(16, f32); + const decorated2 = align(16, f32); + const decorated3 = align(8, f32); + const decorated4 = align(16, u32); + const decorated5 = location(0, f32); + const decorated6 = size(8, align(16, u32)); + const decorated7 = align(16, size(8, u32)); + + expect(schemasEqualStructurally(decorated1, decorated2)).toBe(true); + expect(schemasEqualStructurally(decorated1, decorated3)).toBe(false); + expect(schemasEqualStructurally(decorated1, decorated4)).toBe(false); + expect(schemasEqualStructurally(decorated1, decorated5)).toBe(false); + expect(schemasEqualStructurally(decorated6, decorated7)).toBe(false); // decorator order should matter + }); + + it('compares pointer types', () => { + const ptr1 = ptrPrivate(f32); + const ptr2 = ptrPrivate(f32); + const ptr3 = ptrWorkgroup(f32); + const ptr4 = ptrPrivate(u32); + const ptr5 = ptrStorage(f32, 'read'); + const ptr6 = ptrStorage(f32, 'read-write'); + + expect(schemasEqualStructurally(ptr1, ptr2)).toBe(true); + expect(schemasEqualStructurally(ptr1, ptr3)).toBe(false); + expect(schemasEqualStructurally(ptr1, ptr4)).toBe(false); + expect(schemasEqualStructurally(ptr5, ptr6)).toBe(false); + expect(schemasEqualStructurally(ptrStorage(f32, 'read'), ptrStorage(f32, 'read'))).toBe(true); + }); + + it('compares sampled texture types', () => { + expect(schemasEqualStructurally(texture2d(f32), texture2d(f32))).toBe(true); + expect(schemasEqualStructurally(texture2d(f32), texture2d(i32))).toBe(false); + expect(schemasEqualStructurally(texture2d(u32), texture2d(i32))).toBe(false); + expect(schemasEqualStructurally(textureMultisampled2d(f32), textureMultisampled2d(u32))).toBe( + false, + ); + }); + + it('compares storage texture types', () => { + expect( + schemasEqualStructurally(textureStorage2d('rgba8unorm'), textureStorage2d('rgba8unorm')), + ).toBe(true); + expect( + schemasEqualStructurally(textureStorage2d('rgba8unorm'), textureStorage2d('rgba32float')), + ).toBe(false); + expect( + schemasEqualStructurally( + textureStorage2d('rgba8unorm', 'read-only'), + textureStorage2d('rgba8unorm', 'write-only'), + ), + ).toBe(false); + }); + + it('compares attribute params', () => { + expect(schemasEqualStructurally(builtin.vertexIndex, builtin.instanceIndex)).toBe(false); + expect( + schemasEqualStructurally(interpolate('flat, first', u32), interpolate('flat, either', u32)), + ).toBe(false); + expect( + schemasEqualStructurally(interpolate('flat, first', u32), interpolate('flat, first', u32)), + ).toBe(true); + }); + + it('compares atomic types', () => { + const atomic1 = atomic(u32); + const atomic2 = atomic(u32); + const atomic3 = atomic(i32); + + expect(schemasEqualStructurally(atomic1, atomic2)).toBe(true); + expect(schemasEqualStructurally(atomic1, atomic3)).toBe(false); + }); + + it('compares loose decorated types', () => { + const decorated1 = align(16, unstruct({ a: f32 })); + const decorated2 = align(16, unstruct({ a: f32 })); + const decorated3 = align(8, unstruct({ a: f32 })); + const decorated4 = align(16, unstruct({ a: u32 })); + const decorated5 = location(0, unstruct({ a: f32 })); + + expect(schemasEqualStructurally(decorated1, decorated2)).toBe(true); + expect(schemasEqualStructurally(decorated1, decorated3)).toBe(false); + expect(schemasEqualStructurally(decorated1, decorated4)).toBe(false); + expect(schemasEqualStructurally(decorated1, decorated5)).toBe(false); + }); + + it('compares loose data types', () => { + const unstruct1 = unstruct({ a: f32 }); + const unstruct2 = unstruct({ a: f32 }); + const unstruct3 = unstruct({ b: f32 }); + + const disarray1 = disarrayOf(u32, 4); + const disarray2 = disarrayOf(u32, 4); + const disarray3 = disarrayOf(u32, 5); + + expect(schemasEqualStructurally(unstruct1, unstruct2)).toBe(true); + expect(schemasEqualStructurally(unstruct1, unstruct3)).toBe(false); + expect(schemasEqualStructurally(disarray1, disarray2)).toBe(true); + expect(schemasEqualStructurally(disarray1, disarray3)).toBe(false); + }); + + it('compares vertex formats', () => { + expect(schemasEqualStructurally(uint16x2, uint8x4)).toBe(false); + expect(schemasEqualStructurally(uint16x2, uint32)).toBe(false); + expect(schemasEqualStructurally(uint16x2, uint16x2)).toBe(true); + }); + + it('compares unstructs with vertex formats', () => { + const unstruct1 = unstruct({ a: uint16x2 }); + const unstruct2 = unstruct({ a: uint16x2 }); + const unstruct3 = unstruct({ a: uint32 }); + expect(schemasEqualStructurally(unstruct1, unstruct2)).toBe(true); + expect(schemasEqualStructurally(unstruct1, unstruct3)).toBe(false); + }); + + it('compares different kinds of types', () => { + expect(schemasEqualStructurally(f32, vec2f)).toBe(false); + expect(schemasEqualStructurally(struct({ a: f32 }), unstruct({ a: f32 }))).toBe(false); + expect(schemasEqualStructurally(arrayOf(f32, 4), disarrayOf(f32, 4))).toBe(false); + expect(schemasEqualStructurally(struct({ a: f32 }), f32)).toBe(false); + }); +}); diff --git a/packages/typegpu/tests/internal/deepEqual.test.ts b/packages/typegpu/tests/internal/deepEqual.test.ts deleted file mode 100644 index 13ab4f2657..0000000000 --- a/packages/typegpu/tests/internal/deepEqual.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { deepEqual, f32, ptrPrivate } from 'typegpu/data'; -import { implicitFrom } from '../../src/data/ptr.ts'; - -describe('deepEqual', () => { - it('distinguishes implicit pointers from explicit ones', () => { - const ptr = ptrPrivate(f32); - - expect(deepEqual(ptr, implicitFrom(ptr))).toBe(false); - expect(deepEqual(implicitFrom(ptr), implicitFrom(ptrPrivate(f32)))).toBe(true); - }); -}); diff --git a/packages/typegpu/tests/internal/schemasEqualNominally.test.ts b/packages/typegpu/tests/internal/schemasEqualNominally.test.ts new file mode 100644 index 0000000000..25c4a76864 --- /dev/null +++ b/packages/typegpu/tests/internal/schemasEqualNominally.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest'; +import { schemasEqualNominally, f32, ptrPrivate } from 'typegpu/data'; +import { implicitFrom } from '../../src/data/ptr.ts'; + +describe('schemasEqualNominally', () => { + it('distinguishes implicit pointers from explicit ones', () => { + const ptr = ptrPrivate(f32); + + expect(schemasEqualNominally(ptr, implicitFrom(ptr))).toBe(false); + expect(schemasEqualNominally(implicitFrom(ptr), implicitFrom(ptrPrivate(f32)))).toBe(true); + }); +}); diff --git a/packages/typegpu/tests/internal/schemasEqualStructurally.test.ts b/packages/typegpu/tests/internal/schemasEqualStructurally.test.ts new file mode 100644 index 0000000000..6c7bbc070d --- /dev/null +++ b/packages/typegpu/tests/internal/schemasEqualStructurally.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest'; +import { schemasEqualStructurally, f32, ptrPrivate } from 'typegpu/data'; +import { implicitFrom } from '../../src/data/ptr.ts'; + +describe('schemasEqualStructurally', () => { + it('distinguishes implicit pointers from explicit ones', () => { + const ptr = ptrPrivate(f32); + + expect(schemasEqualStructurally(ptr, implicitFrom(ptr))).toBe(false); + expect(schemasEqualStructurally(implicitFrom(ptr), implicitFrom(ptrPrivate(f32)))).toBe(true); + }); +}); diff --git a/packages/typegpu/tests/serial.test.ts b/packages/typegpu/tests/serial.test.ts index d14cf588c1..9b871a7146 100644 --- a/packages/typegpu/tests/serial.test.ts +++ b/packages/typegpu/tests/serial.test.ts @@ -1,6 +1,5 @@ import { describe, expect, vi } from 'vitest'; import { tgpu, d, type TgpuRoot } from 'typegpu'; -import { deepEqual } from 'typegpu/data'; import { getName, isNonTransferableResource, @@ -264,9 +263,12 @@ describe('resource snapshot protocol', () => { expect(restoredVertexLayout.resourceType).toBe('vertex-layout'); expect(restoredVertexLayout.stepMode).toBe(vertexLayout.stepMode); expect(restoredVertexLayout.stride).toBe(vertexLayout.stride); - expect(deepEqual(restoredVertexLayout.schemaForCount(4), vertexLayout.schemaForCount(4))).toBe( - true, - ); + expect( + d.schemasEqualStructurally( + restoredVertexLayout.schemaForCount(4), + vertexLayout.schemaForCount(4), + ), + ).toBe(true); }); it('round-trips immediate variables and pipeline-held immediate data', ({ @@ -280,7 +282,7 @@ describe('resource snapshot protocol', () => { const restoredParams = deepRoundTrip(params, root); expect(restoredParams.resourceType).toBe('immediate-var'); - expect(d.deepEqual(restoredParams.dataType, Params)).toBe(true); + expect(d.schemasEqualStructurally(restoredParams.dataType, Params)).toBe(true); expect(restoredParams.dataType).not.toBe(Params); expect(restoredParams.defaultValue).toEqual({ scale: 1, offset: d.vec3f(2, 3, 4) }); expect(getName(restoredParams)).toBe('params'); @@ -306,7 +308,7 @@ describe('resource snapshot protocol', () => { const Particle = d.struct({ pos: d.vec2f, alive: d.u32 }); const buffer = root.createBuffer(d.arrayOf(Particle, 2)).$usage('storage'); const restoredBuffer = deepRoundTrip(buffer, root); - expect(d.deepEqual(restoredBuffer.dataType, buffer.dataType)).toBe(true); + expect(d.schemasEqualStructurally(restoredBuffer.dataType, buffer.dataType)).toBe(true); expect(restoredBuffer.dataType).not.toBe(buffer.dataType); expect(root.unwrap(restoredBuffer)).toBe(root.unwrap(buffer)); @@ -319,16 +321,19 @@ describe('resource snapshot protocol', () => { .$idx(1); const restoredLayout = deepRoundTrip(layout, root); expect(restoredLayout.index).toBe(1); - expect(d.deepEqual(restoredLayout.entries.color.texture, layout.entries.color.texture)).toBe( - true, - ); expect( - d.deepEqual( + d.schemasEqualStructurally( + restoredLayout.entries.color.texture, + layout.entries.color.texture, + ), + ).toBe(true); + expect( + d.schemasEqualStructurally( restoredLayout.entries.target.storageTexture, layout.entries.target.storageTexture, ), ).toBe(true); - expect(d.deepEqual(restoredLayout.entries.params.uniform, Particle)).toBe(true); + expect(d.schemasEqualStructurally(restoredLayout.entries.params.uniform, Particle)).toBe(true); const groupLayout = tgpu.bindGroupLayout({ values: { storage: d.arrayOf(d.u32, 4) } }); const bindGroup = root.createBindGroup(groupLayout, { diff --git a/packages/typegpu/tests/serializeDataSchema.test.ts b/packages/typegpu/tests/serializeDataSchema.test.ts index 84377221c9..5aef259cb0 100644 --- a/packages/typegpu/tests/serializeDataSchema.test.ts +++ b/packages/typegpu/tests/serializeDataSchema.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest'; import { d } from 'typegpu'; -import { deepEqual } from 'typegpu/data'; import { deserializeDataSchema, serializeDataSchema } from 'typegpu/~internal'; const schemas = [ @@ -271,7 +270,10 @@ describe('data schema serialization', () => { it('round-trips transferable schemas', () => { for (const [index, schema] of schemas.entries()) { const restored = deserializeDataSchema(serializeDataSchema(schema)); - expect(deepEqual(restored, schema), `schema #${index} (${schema.type})`).toBe(true); + expect( + d.schemasEqualStructurally(restored, schema), + `schema #${index} (${schema.type})`, + ).toBe(true); } });