vitest-pool-assemblyscript 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +10 -13
  2. package/assembly/compare.ts +469 -76
  3. package/assembly/expect.ts +160 -67
  4. package/assembly/index.ts +0 -10
  5. package/assembly/utils.ts +168 -0
  6. package/dist/{addon-interface-_pNXcbib.mjs → addon-interface-CYFXMbK7.mjs} +4 -3
  7. package/dist/addon-interface-CYFXMbK7.mjs.map +1 -0
  8. package/dist/{ast-visitor-C5gQqWD2.mjs → ast-visitor-CWEOd3UH.mjs} +15 -2
  9. package/dist/ast-visitor-CWEOd3UH.mjs.map +1 -0
  10. package/dist/{compile-runner-DOMhsLQE.mjs → compile-runner-BGQhBkBo.mjs} +8 -7
  11. package/dist/compile-runner-BGQhBkBo.mjs.map +1 -0
  12. package/dist/compiler/transforms/deep-equals.d.mts +14 -0
  13. package/dist/compiler/transforms/deep-equals.d.mts.map +1 -0
  14. package/dist/compiler/transforms/deep-equals.mjs +245 -0
  15. package/dist/compiler/transforms/deep-equals.mjs.map +1 -0
  16. package/dist/compiler/transforms/strip-inline.mjs +2 -2
  17. package/dist/{compiler-CIXpfKq0.mjs → compiler-hUlDr5vL.mjs} +14 -6
  18. package/dist/compiler-hUlDr5vL.mjs.map +1 -0
  19. package/dist/config/index-v3.d.mts +33 -3
  20. package/dist/config/index-v3.d.mts.map +1 -1
  21. package/dist/config/index-v3.mjs.map +1 -1
  22. package/dist/config/index.d.mts +29 -4
  23. package/dist/config/index.d.mts.map +1 -0
  24. package/dist/config/index.mjs +6 -6
  25. package/dist/{constants-DuBLuMjt.mjs → constants-DbxJ3hzg.mjs} +10 -97
  26. package/dist/constants-DbxJ3hzg.mjs.map +1 -0
  27. package/dist/coverage-provider/index.mjs +30 -10
  28. package/dist/coverage-provider/index.mjs.map +1 -1
  29. package/dist/{debug-Cm1VFmaz.mjs → debug-DtRAL4rM.mjs} +38 -4
  30. package/dist/debug-DtRAL4rM.mjs.map +1 -0
  31. package/dist/{feature-check-ELxw_Mji.mjs → feature-check-Bje3ntpV.mjs} +2 -2
  32. package/dist/{feature-check-ELxw_Mji.mjs.map → feature-check-Bje3ntpV.mjs.map} +1 -1
  33. package/dist/index-internal.d.mts +1 -1
  34. package/dist/index-internal.d.mts.map +1 -1
  35. package/dist/index-internal.mjs +4 -4
  36. package/dist/index-v3.d.mts.map +1 -1
  37. package/dist/index-v3.mjs +8 -7
  38. package/dist/index-v3.mjs.map +1 -1
  39. package/dist/index.d.mts +1 -2
  40. package/dist/index.mjs +6 -6
  41. package/dist/{load-user-imports-B3Iy_K8k.mjs → load-user-imports-Bx5ZlhSm.mjs} +9 -9
  42. package/dist/load-user-imports-Bx5ZlhSm.mjs.map +1 -0
  43. package/dist/{pool-runner-init-BDQtAGwQ.mjs → pool-runner-init-BqkwQ2tk.mjs} +7 -7
  44. package/dist/pool-runner-init-BqkwQ2tk.mjs.map +1 -0
  45. package/dist/{pool-runner-init-CvnB0-iN.d.mts → pool-runner-init-CNpRdA5u.d.mts} +2 -2
  46. package/dist/pool-runner-init-CNpRdA5u.d.mts.map +1 -0
  47. package/dist/pool-thread/compile-worker-thread.d.mts +1 -1
  48. package/dist/pool-thread/compile-worker-thread.d.mts.map +1 -1
  49. package/dist/pool-thread/compile-worker-thread.mjs +10 -13
  50. package/dist/pool-thread/compile-worker-thread.mjs.map +1 -1
  51. package/dist/pool-thread/test-worker-thread.d.mts +1 -1
  52. package/dist/pool-thread/test-worker-thread.d.mts.map +1 -1
  53. package/dist/pool-thread/test-worker-thread.mjs +8 -11
  54. package/dist/pool-thread/test-worker-thread.mjs.map +1 -1
  55. package/dist/pool-thread/v3-tinypool-thread.d.mts +1 -1
  56. package/dist/pool-thread/v3-tinypool-thread.d.mts.map +1 -1
  57. package/dist/pool-thread/v3-tinypool-thread.mjs +12 -18
  58. package/dist/pool-thread/v3-tinypool-thread.mjs.map +1 -1
  59. package/dist/{resolve-config-DhZ4lOSK.mjs → resolve-config-s9gSJSMc.mjs} +14 -5
  60. package/dist/resolve-config-s9gSJSMc.mjs.map +1 -0
  61. package/dist/{test-runner-C4I9VknR.mjs → test-runner-BGqc9uCK.mjs} +4 -4
  62. package/dist/{test-runner-C4I9VknR.mjs.map → test-runner-BGqc9uCK.mjs.map} +1 -1
  63. package/dist/{types-D0nprJo1.d.mts → types-DHVk5iAx.d.mts} +17 -11
  64. package/dist/types-DHVk5iAx.d.mts.map +1 -0
  65. package/dist/vitest-file-tasks-D8sOClGX.mjs +149 -0
  66. package/dist/vitest-file-tasks-D8sOClGX.mjs.map +1 -0
  67. package/dist/{vitest-tasks--ow4pacR.mjs → vitest-tasks-BZ24sghI.mjs} +6 -4
  68. package/dist/vitest-tasks-BZ24sghI.mjs.map +1 -0
  69. package/package.json +11 -14
  70. package/prebuilds/darwin-arm64/vitest-pool-assemblyscript.glibc.node +0 -0
  71. package/prebuilds/darwin-x64/vitest-pool-assemblyscript.glibc.node +0 -0
  72. package/prebuilds/linux-arm64/vitest-pool-assemblyscript.glibc.node +0 -0
  73. package/prebuilds/linux-x64/vitest-pool-assemblyscript.glibc.node +0 -0
  74. package/prebuilds/linux-x64/vitest-pool-assemblyscript.musl.node +0 -0
  75. package/prebuilds/win32-arm64/vitest-pool-assemblyscript.glibc.node +0 -0
  76. package/prebuilds/win32-x64/vitest-pool-assemblyscript.glibc.node +0 -0
  77. package/src/instrumentation/native/addon.cpp +29 -3
  78. package/dist/addon-interface-_pNXcbib.mjs.map +0 -1
  79. package/dist/ast-visitor-C5gQqWD2.mjs.map +0 -1
  80. package/dist/compile-runner-DOMhsLQE.mjs.map +0 -1
  81. package/dist/compiler-CIXpfKq0.mjs.map +0 -1
  82. package/dist/constants-DuBLuMjt.mjs.map +0 -1
  83. package/dist/custom-provider-options-YTk1m7At.d.mts +0 -26
  84. package/dist/custom-provider-options-YTk1m7At.d.mts.map +0 -1
  85. package/dist/debug-Cm1VFmaz.mjs.map +0 -1
  86. package/dist/load-user-imports-B3Iy_K8k.mjs.map +0 -1
  87. package/dist/pool-runner-init-BDQtAGwQ.mjs.map +0 -1
  88. package/dist/pool-runner-init-CvnB0-iN.d.mts.map +0 -1
  89. package/dist/resolve-config-DhZ4lOSK.mjs.map +0 -1
  90. package/dist/types-D0nprJo1.d.mts.map +0 -1
  91. package/dist/vitest-file-tasks-Bn9CrWt_.mjs +0 -61
  92. package/dist/vitest-file-tasks-Bn9CrWt_.mjs.map +0 -1
  93. package/dist/vitest-tasks--ow4pacR.mjs.map +0 -1
@@ -1,61 +1,321 @@
1
- function arrayEquals<T extends ArrayLike<unknown>, U extends ArrayLike<unknown>>(actual: T, expected: U): bool {
1
+ import { stringifyValue } from './utils';
2
+
3
+ /**
4
+ * Byte offset from an object pointer to the rtId field in the AS managed object header.
5
+ * Every managed object has a 20-byte header preceding the payload; rtId is a u32 at offset -8.
6
+ * See: https://www.assemblyscript.org/runtime.html#memory-layout
7
+ */
8
+ const MANAGED_OBJECT_RTID_BYTE_OFFSET: usize = 8;
9
+
10
+ /**
11
+ * Cycle detection for deep equality comparisons. Tracks which (actual, expected) reference
12
+ * pairs are currently being compared to prevent infinite recursion on self-referential or
13
+ * mutually-referential object graphs.
14
+ *
15
+ * Pairs are packed as u64 keys: (u64(actualPtr) << 32) | u64(expectedPtr).
16
+ * Entries are added when a reference comparison starts and never individually removed —
17
+ * if a pair was previously Equal, revisiting returns Equal (correct); if NotEqual, we
18
+ * already returned and won't revisit. Cleared at the start of each toEqual/toStrictEqual call.
19
+ */
20
+ const equalsRefPairs = new Set<u64>();
21
+
22
+ function equalsRefPairSeen(actualPtr: usize, expectedPtr: usize): bool {
23
+ const key: u64 = (u64(actualPtr) << 32) | u64(expectedPtr);
24
+ return equalsRefPairs.has(key);
25
+ }
26
+
27
+ function equalsRefPairMark(actualPtr: usize, expectedPtr: usize): void {
28
+ const key: u64 = (u64(actualPtr) << 32) | u64(expectedPtr);
29
+ equalsRefPairs.add(key);
30
+ }
31
+
32
+ export function equalsRefPairsClear(): void {
33
+ equalsRefPairs.clear();
34
+ }
35
+
36
+ /**
37
+ * Comparison path tracking for deep equality. Accumulates path segments (e.g. "[0]",
38
+ * "['key']", "{Set}") as equals() recurses into containers, building a path like
39
+ * "[2].name" from root to the mismatch point.
40
+ *
41
+ * Uses push/pop discipline: pop only on Equal, return-without-pop on non-Equal.
42
+ * As non-Equal propagates up the call stack, the path naturally accumulates to the
43
+ * deepest mismatch point. Cleared at the start of each toEqual/toStrictEqual call.
44
+ */
45
+ const equalsPath: string[] = [];
46
+
47
+ function equalsPathPush(segment: string): void {
48
+ equalsPath.push(segment);
49
+ }
50
+
51
+ function equalsPathPop(): void {
52
+ equalsPath.pop();
53
+ }
54
+
55
+ export function equalsPathString(): string {
56
+ let result = "";
57
+ for (let i = 0; i < equalsPath.length; i++) {
58
+ result += equalsPath[i];
59
+ }
60
+ return result;
61
+ }
62
+
63
+ export function equalsPathClear(): void {
64
+ equalsPath.length = 0;
65
+ }
66
+
67
+ export function equalsPathLength(): i32 {
68
+ return equalsPath.length;
69
+ }
70
+
71
+ /**
72
+ * Runtime type mismatch name tracking. When equals() detects a runtime type mismatch
73
+ * (different rtIds on managed objects), it captures the actual and expected runtime type
74
+ * names via the transform-injected __vitest_assemblyscript_typename method. These are
75
+ * read by toEqual/toStrictEqual to include type names in the assertion suffix
76
+ * (e.g. "runtime type mismatch: Circle vs Square").
77
+ *
78
+ * Cleared at the start of each toEqual/toStrictEqual call alongside path and visited set.
79
+ */
80
+ let equalsRtmActualName: string = "";
81
+ let equalsRtmExpectedName: string = "";
82
+
83
+ export function equalsRtmNamesSuffix(): string {
84
+ if (equalsRtmActualName != "" && equalsRtmExpectedName != "") {
85
+ return ": " + equalsRtmActualName + " vs " + equalsRtmExpectedName;
86
+ }
87
+ return "";
88
+ }
89
+
90
+ export function equalsRtmNamesClear(): void {
91
+ equalsRtmActualName = "";
92
+ equalsRtmExpectedName = "";
93
+ }
94
+
95
+ /**
96
+ * Global bridge for the deep-equals compiler transform — push a path segment.
97
+ *
98
+ * Transform-injected deep equality methods call this to record which field is
99
+ * being compared, enabling path context like ".shape" or ".members" in error messages.
100
+ * Declared global to make it available in all source files without import.
101
+ */
102
+ // @ts-ignore: AS-specific global decorator
103
+ @global
104
+ function __vitest_assemblyscript_equals_path_push(segment: string): void {
105
+ equalsPathPush(segment);
106
+ }
107
+
108
+ /**
109
+ * Global bridge for the deep-equals compiler transform — pop a path segment.
110
+ *
111
+ * Called only when a field comparison returns Equal (push/pop discipline).
112
+ * On non-Equal, the segment is left on the stack so the path accumulates
113
+ * to the deepest mismatch point.
114
+ */
115
+ // @ts-ignore: AS-specific global decorator
116
+ @global
117
+ function __vitest_assemblyscript_equals_path_pop(): void {
118
+ equalsPathPop();
119
+ }
120
+
121
+ /** Returns " at <path>" or " within <path>" if the path is non-empty, otherwise empty string. */
122
+ function equalsPathAtSuffix(): string {
123
+ if (equalsPath.length == 0) return "";
124
+
125
+ // Scan for "Set" anywhere in the path stack. Set elements have no meaningful
126
+ // identifier, so segments pushed after "Set" (e.g. array indices from inner
127
+ // comparisons during the set scan) are discarded — they represent aborted
128
+ // comparison attempts whose segments couldn't be cleaned up because a throw
129
+ // halted execution. Build the path only up to and including "Set".
130
+ for (let i = equalsPath.length - 1; i >= 0; i--) {
131
+ if (equalsPath[i] == "Set") {
132
+ let path = "";
133
+ for (let j = 0; j <= i; j++) {
134
+ path += equalsPath[j];
135
+ }
136
+ return " within " + path;
137
+ }
138
+ }
139
+
140
+ return " at " + equalsPathString();
141
+ }
142
+
143
+ /**
144
+ * Result of a deep equality comparison.
145
+ * Declared global so it is available in all source files without import —
146
+ * including transform-injected deep equality methods in user classes.
147
+ */
148
+ // @ts-ignore: AS-specific global decorator
149
+ @global
150
+ export enum __vitest_assemblyscript_EqualityResult {
151
+ Equal,
152
+ NotEqual,
153
+ RuntimeTypeMismatch,
154
+ }
155
+
156
+ function arrayEquals<T extends ArrayLike<unknown>, U extends ArrayLike<unknown>>(actual: T, expected: U): __vitest_assemblyscript_EqualityResult {
2
157
  if (actual.length != expected.length) {
3
- return false;
158
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
4
159
  }
5
160
 
6
161
  for (let i = 0; i < expected.length; i++) {
7
- if (!equals(actual[i], expected[i])) {
8
- return false;
162
+ // Context-aware format: "[N]" composes with field paths (e.g. ".members[2]"),
163
+ // "index [N]" reads well standalone (e.g. "differs at index [2]")
164
+ const segment = equalsPathLength() > 0
165
+ ? "[" + i.toString() + "]"
166
+ : "index [" + i.toString() + "]";
167
+ equalsPathPush(segment);
168
+ const result = equals(actual[i], expected[i]);
169
+ if (result != __vitest_assemblyscript_EqualityResult.Equal) {
170
+ return result;
9
171
  }
172
+ equalsPathPop();
10
173
  }
11
174
 
12
- return true;
175
+ return __vitest_assemblyscript_EqualityResult.Equal;
13
176
  }
14
177
 
15
- function setEquals<T, U>(actual: T, expected: U): bool {
178
+ function setEquals<T, U>(actual: T, expected: U): __vitest_assemblyscript_EqualityResult {
16
179
  if (actual instanceof Set && expected instanceof Set) {
180
+ // Exception to push/pop discipline: always pop before returning, regardless of result.
181
+ // Set elements have no meaningful identifier (no index, no key), so "Set" is only
182
+ // useful as a terminal path segment — it should not compose with deeper segments
183
+ // from recursive comparisons inside elements. equalsPathAtSuffix() formats this
184
+ // as "within Set" instead of "at Set".
185
+ equalsPathPush("Set");
186
+
17
187
  if (actual.size != expected.size) {
18
- return false;
188
+ equalsPathPop();
189
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
19
190
  }
20
191
 
192
+ const actualValues = actual.values();
21
193
  const expectedValues = expected.values();
22
194
 
195
+ // Track which actual elements have been matched to prevent double-counting.
196
+ // Without this, two expected elements could both match the same actual element.
197
+ const matched = new Array<bool>(actualValues.length);
198
+ for (let i = 0; i < matched.length; i++) {
199
+ matched[i] = false;
200
+ }
201
+
23
202
  for (let i = 0; i < expectedValues.length; i++) {
24
- if (!actual.has(expectedValues[i])) {
25
- return false;
203
+ let found = false;
204
+ for (let j = 0; j < actualValues.length; j++) {
205
+ if (!matched[j]) {
206
+ // Save path stack depth before each scan attempt. Failed comparisons
207
+ // leave stale segments (e.g. ".x" from a Point field) that would corrupt
208
+ // the pop discipline — restoring ensures "Set" remains the top segment.
209
+ const pathDepth = equalsPathLength();
210
+ if (equals(actualValues[j], expectedValues[i]) == __vitest_assemblyscript_EqualityResult.Equal) {
211
+ matched[j] = true;
212
+ found = true;
213
+ break;
214
+ }
215
+ equalsPath.length = pathDepth;
216
+ }
217
+ }
218
+ if (!found) {
219
+ equalsPathPop();
220
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
26
221
  }
27
222
  }
28
223
 
29
- return true;
224
+ equalsPathPop();
225
+ return __vitest_assemblyscript_EqualityResult.Equal;
30
226
  }
31
227
 
32
- return false;
228
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
33
229
  }
34
230
 
35
- function mapEquals<T, U>(actual: T, expected: U): bool {
231
+ function mapEquals<T, U>(actual: T, expected: U): __vitest_assemblyscript_EqualityResult {
36
232
  if (actual instanceof Map && expected instanceof Map) {
37
- if (actual.size != expected.size) {
38
- return false;
233
+ // Key types must match exactly — cross-type key comparison is not safe because
234
+ // .has() and .get() depend on the key's hash and equality semantics, which differ
235
+ // across types (e.g. string vs i32 keys have incompatible hash/lookup behavior).
236
+ if (nameof<indexof<T>>() != nameof<indexof<U>>()) {
237
+ throw new Error("Map key types must match for deep equality comparison: "
238
+ + nameof<T>() + " and " + nameof<U>()
239
+ + equalsPathAtSuffix()
240
+ );
39
241
  }
40
242
 
41
- const expectedKeys = expected.keys();
243
+ if (actual.size != expected.size) {
244
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
245
+ }
42
246
 
43
- for (let i = 0; i < expectedKeys.length; i++) {
44
- const key = expectedKeys[i];
247
+ // Cast actual to use expected's key type (verified equal above) while preserving
248
+ // actual's native value type. This lets us iterate expected's keys and look them
249
+ // up in actual, while equals() handles cross-type value comparison naturally
250
+ // (e.g. valueof<T>=i32 vs valueof<U>=f64).
251
+ const castActual = changetype<Map<indexof<U>, valueof<T>>>(actual);
252
+
253
+ // instanceof needed after changetype for the compiler to resolve Map methods
254
+ if (castActual instanceof Map) {
255
+ const expectedKeys = expected.keys();
256
+
257
+ for (let i = 0; i < expectedKeys.length; i++) {
258
+ const key = expectedKeys[i];
259
+ // Context-aware format: "[key]" composes with field paths (e.g. ".registry[\"x\"]"),
260
+ // "key [key]" reads well standalone (e.g. "differs at key [\"x\"]")
261
+ const segment = equalsPathLength() > 0
262
+ ? "[" + stringifyValue(key) + "]"
263
+ : "key [" + stringifyValue(key) + "]";
264
+ equalsPathPush(segment);
265
+
266
+ if (!castActual.has(key)) {
267
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
268
+ }
45
269
 
46
- if (!actual.has(key)) {
47
- return false;
270
+ // Cross-type value comparison delegates to equals(), which handles
271
+ // compatible numeric types, incomparable types, and precision-loss cases
272
+ const result = equals(castActual.get(key), expected.get(key));
273
+ if (result != __vitest_assemblyscript_EqualityResult.Equal) {
274
+ return result;
275
+ }
276
+ equalsPathPop();
48
277
  }
49
278
 
50
- if (!equals(actual.get(key), expected.get(key))) {
51
- return false;
52
- }
279
+ return __vitest_assemblyscript_EqualityResult.Equal;
280
+ } else {
281
+ // will never happen — changetype preserves the underlying Map instance
282
+ unreachable();
283
+ }
284
+ }
285
+
286
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
287
+ }
288
+
289
+ function arrayBufferEquals<T, U>(actual: T, expected: U): __vitest_assemblyscript_EqualityResult {
290
+ if (!(actual instanceof ArrayBuffer) || !(expected instanceof ArrayBuffer)) {
291
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
292
+ }
293
+
294
+ if (actual.byteLength != expected.byteLength) {
295
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
296
+ }
297
+
298
+ const actualPtr = changetype<usize>(actual);
299
+ const expectedPtr = changetype<usize>(expected);
300
+ const wordCount: usize = actual.byteLength / 8;
301
+ const remainder: usize = actual.byteLength % 8;
302
+
303
+ // compare 8 bytes at a time (u64 word-sized comparison)
304
+ for (let i: usize = 0; i < wordCount; i++) {
305
+ if (load<u64>(actualPtr + i * 8) != load<u64>(expectedPtr + i * 8)) {
306
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
53
307
  }
308
+ }
54
309
 
55
- return true;
310
+ // compare remaining 0-7 bytes individually
311
+ const remainderOffset = wordCount * 8;
312
+ for (let i: usize = 0; i < remainder; i++) {
313
+ if (load<u8>(actualPtr + remainderOffset + i) != load<u8>(expectedPtr + remainderOffset + i)) {
314
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
315
+ }
56
316
  }
57
317
 
58
- return false;
318
+ return __vitest_assemblyscript_EqualityResult.Equal;
59
319
  }
60
320
 
61
321
  /**
@@ -66,9 +326,11 @@ export function identical<T, U>(actual: T, expected: U): bool {
66
326
  // both refs
67
327
  if (isReference<T>() && isReference<U>()) {
68
328
  const actualIsNullable = isNullable<T>();
69
- const actualIsNull = actualIsNullable && actual == null;
329
+ // Use changetype pointer check instead of `== null` to avoid invoking
330
+ // user-defined @operator("==") overloads, which reject null arguments
331
+ const actualIsNull = actualIsNullable && changetype<usize>(actual) == 0;
70
332
  const expectedIsNullable = isNullable<U>();
71
- const expectedIsNull = expectedIsNullable && expected == null;
333
+ const expectedIsNull = expectedIsNullable && changetype<usize>(expected) == 0;
72
334
 
73
335
  // null refs
74
336
  if (actualIsNull && expectedIsNull) {
@@ -84,12 +346,26 @@ export function identical<T, U>(actual: T, expected: U): bool {
84
346
  // object refs
85
347
  return changetype<usize>(actual) == changetype<usize>(expected);
86
348
  }
87
- } else if (isReference<T>() && !isReference<U>()) {
88
- // actual is null ref, expected bare null
89
- return changetype<usize>(actual) == usize(0) && expected == usize(0);
90
- } else if (!isReference<T>() && isReference<U>()) {
91
- // actual is bare null, expected is null ref
92
- return actual == usize(0) && changetype<usize>(expected) == usize(0);
349
+ } else if (isReference<T>() && !isReference<U>()) {
350
+ // Both null/zero: null reference matches bare null (usize(0))
351
+ if (changetype<usize>(actual) == 0 && expected == usize(0)) {
352
+ return true;
353
+ }
354
+ // Non-null reference vs value type: fundamentally incomparable
355
+ throw new Error(
356
+ "Cannot compare " + nameof<T>() + " with " + nameof<U>()
357
+ + equalsPathAtSuffix() + ": reference and value types are not comparable."
358
+ );
359
+ } else if (!isReference<T>() && isReference<U>()) {
360
+ // Both null/zero: bare null (usize(0)) matches null reference
361
+ if (actual == usize(0) && changetype<usize>(expected) == 0) {
362
+ return true;
363
+ }
364
+ // Value type vs non-null reference: fundamentally incomparable
365
+ throw new Error(
366
+ "Cannot compare " + nameof<T>() + " with " + nameof<U>()
367
+ + equalsPathAtSuffix() + ": reference and value types are not comparable."
368
+ );
93
369
  } else { // both primitives
94
370
  if ( (isBoolean<T>() && !isBoolean<U>()) || (!isBoolean<T>() && isBoolean<U>())
95
371
  ) {
@@ -121,6 +397,7 @@ export function identical<T, U>(actual: T, expected: U): bool {
121
397
  if (sizeof<U>() >= sizeof<T>()) {
122
398
  throw new Error(
123
399
  "Cannot compare " + nameof<T>() + " with " + nameof<U>()
400
+ + equalsPathAtSuffix()
124
401
  + ": float precision is insufficient for the integer type's range."
125
402
  + " Cast both values to f64 before comparing, e.g. expect(f64(a)).toBe(f64(b))."
126
403
  + " Note: large integer values may lose precision when cast to f64, which could cause false positives."
@@ -130,18 +407,24 @@ export function identical<T, U>(actual: T, expected: U): bool {
130
407
  if (sizeof<T>() >= sizeof<U>()) {
131
408
  throw new Error(
132
409
  "Cannot compare " + nameof<T>() + " with " + nameof<U>()
410
+ + equalsPathAtSuffix()
133
411
  + ": float precision is insufficient for the integer type's range."
134
412
  + " Cast both values to f64 before comparing, e.g. expect(f64(a)).toBe(f64(b))."
135
413
  + " Note: large integer values may lose precision when cast to f64, which could cause false positives."
136
414
  );
137
415
  }
138
416
  }
417
+
418
+ // if we got here, cast to f64 is safe without precision loss - cast to compare
139
419
  return f64(actual) === f64(expected);
140
420
  } else if (isVector<T>() && isVector<U>()) {
141
421
  return <v128>actual == <v128>expected;
142
422
  } else {
143
- return false;
144
- }
423
+ throw new Error(
424
+ "Cannot compare " + nameof<T>() + " with " + nameof<U>()
425
+ + equalsPathAtSuffix() + ": incompatible types."
426
+ );
427
+ }
145
428
  }
146
429
  }
147
430
 
@@ -181,9 +464,13 @@ export function closeTo<T, U>(actual: T, expected: U, precision: i32 = 2): bool
181
464
 
182
465
  /**
183
466
  * Generic value equality comparison. Assumes comparable types for both values.
184
- * Does not yet support user-defined types.
467
+ * Supports primitives, strings, Arrays, Sets, Maps, ArrayBuffers, and user-defined
468
+ * types (via compiler transform-injected deep equality method).
469
+ *
470
+ * Returns an __vitest_assemblyscript_EqualityResult enum to distinguish between "not equal" and "type mismatch",
471
+ * enabling matchers to produce more informative assertion failure messages.
185
472
  */
186
- export function equals<T, U>(actual: T, expected: U): bool {
473
+ export function equals<T, U>(actual: T, expected: U): __vitest_assemblyscript_EqualityResult {
187
474
  let exactMatch: bool = false;
188
475
 
189
476
  // allow boolean-to-number comparisons here
@@ -197,22 +484,37 @@ export function equals<T, U>(actual: T, expected: U): bool {
197
484
 
198
485
  if (!isReference<T>() || isString<T>() || isVector<T>()) {
199
486
  // non-bool primitives or strings: return result of comparing
200
- return exactMatch;
487
+ return exactMatch ? __vitest_assemblyscript_EqualityResult.Equal : __vitest_assemblyscript_EqualityResult.NotEqual;
201
488
  } else if (exactMatch) {
202
489
  // primitive / reference comparison passed already
203
- return true;
490
+ return __vitest_assemblyscript_EqualityResult.Equal;
204
491
  }
205
492
 
206
493
  if (isNullable<T>()) {
207
- if (actual == null && expected == null) {
208
- return true;
494
+ // Use changetype pointer checks instead of `== null` / `!= null` to avoid
495
+ // invoking user-defined @operator("==") overloads, which reject null arguments
496
+ const actualIsNull = changetype<usize>(actual) == 0;
497
+ const expectedIsNull = changetype<usize>(expected) == 0;
498
+
499
+ if (actualIsNull && expectedIsNull) {
500
+ return __vitest_assemblyscript_EqualityResult.Equal;
209
501
  }
210
502
 
211
- if ( (actual == null && expected != null) || (actual != null && expected == null) ) {
212
- return false;
503
+ if (actualIsNull != expectedIsNull) {
504
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
213
505
  }
214
506
  }
215
507
 
508
+ // Cycle detection: if this reference pair is already being compared further up
509
+ // the call stack, the cycle structure matches — any field-level differences would
510
+ // have been caught in the non-cyclic part before reaching the cycle.
511
+ const actualPtr = changetype<usize>(actual);
512
+ const expectedPtr = changetype<usize>(expected);
513
+ if (equalsRefPairSeen(actualPtr, expectedPtr)) {
514
+ return __vitest_assemblyscript_EqualityResult.Equal;
515
+ }
516
+ equalsRefPairMark(actualPtr, expectedPtr);
517
+
216
518
  if (isArrayLike<T>(actual) && isArrayLike<U>(expected)) {
217
519
  return arrayEquals(actual, expected);
218
520
  }
@@ -222,18 +524,135 @@ export function equals<T, U>(actual: T, expected: U): bool {
222
524
  if (actual instanceof Map && expected instanceof Map) {
223
525
  return mapEquals(actual, expected);
224
526
  }
527
+ if (actual instanceof ArrayBuffer && expected instanceof ArrayBuffer) {
528
+ return arrayBufferEquals(actual, expected);
529
+ }
225
530
 
226
- if (idof<T>() != idof<U>()) {
227
- throw new Error("Cannot compare equality between " + nameof<T>(actual)
228
- + " and " + nameof<U>(expected) + " - this comparison is undefined."
531
+ // Runtime type checking for reference types.
532
+ // Managed objects have a header with rtId (runtime type ID) that reflects the actual
533
+ // runtime type, even when the variable is declared as a base type. Unmanaged objects
534
+ // lack this header and fall back to compile-time idof checks.
535
+ if (isManaged<T>() != isManaged<U>()) {
536
+ // Managed vs unmanaged is a fundamental memory layout incompatibility
537
+ throw new Error("Cannot compare deep equality between managed and unmanaged types: "
538
+ + nameof<T>() + " and " + nameof<U>()
539
+ + equalsPathAtSuffix()
229
540
  );
230
541
  }
231
542
 
232
- // TODO value compare
233
- throw new Error("Deep equality comparison of user-defined reference types"
234
- + " is not yet implemented, and these references are not identical."
235
- + " Use toBe() for reference equality."
236
- );
543
+ if (isManaged<T>() && isManaged<U>()) {
544
+ // Both managed: read runtime type ID from the AS object header
545
+ const actualRtId = load<u32>(changetype<usize>(actual) - MANAGED_OBJECT_RTID_BYTE_OFFSET);
546
+ const expectedRtId = load<u32>(changetype<usize>(expected) - MANAGED_OBJECT_RTID_BYTE_OFFSET);
547
+
548
+ if (actualRtId != expectedRtId) {
549
+ // @ts-ignore
550
+ if (isDefined(actual.__vitest_assemblyscript_deep_equals)) {
551
+ // User-defined classes: return TypeMismatch so the matcher can produce an
552
+ // informative assertion failure message instead of an opaque error.
553
+ //
554
+ // We return here instead of throwing to support `.not.toEqual()` for
555
+ // polymorphic runtime type mismatches — e.g. a Shape-typed Circle vs
556
+ // Shape-typed Square, where asserting "not equal" is valid, not a
557
+ // programmer error. In `toEqual`, `equals()` is called BEFORE
558
+ // `assertComparison()`. If `equals()` throws, execution never reaches
559
+ // `assertComparison`, so `.not` inversion cannot run and the test
560
+ // crashes. By returning TypeMismatch, `toEqual` evaluates
561
+ // `result == __vitest_assemblyscript_EqualityResult.Equal` (false), passes that to
562
+ // `assertComparison`, and `.not` can invert it to a pass.
563
+
564
+ // Capture runtime type names via virtual dispatch for informative assertion suffix
565
+ // @ts-ignore
566
+ if (isDefined(actual.__vitest_assemblyscript_typename)) {
567
+ // @ts-ignore
568
+ equalsRtmActualName = (<NonNullable<T>>actual).__vitest_assemblyscript_typename();
569
+ }
570
+ // @ts-ignore
571
+ if (isDefined(expected.__vitest_assemblyscript_typename)) {
572
+ // @ts-ignore
573
+ equalsRtmExpectedName = (<NonNullable<U>>expected).__vitest_assemblyscript_typename();
574
+ }
575
+
576
+ return __vitest_assemblyscript_EqualityResult.RuntimeTypeMismatch;
577
+ }
578
+
579
+ // Non-user-defined managed types with mismatched rtIds: cross-container comparisons
580
+ // (e.g. Map vs Set, Array vs Map) that fell through the instanceof checks above
581
+ // (which require both operands to be the same container type), or stdlib types
582
+ throw new Error("Cannot compare deep equality between " + nameof<T>()
583
+ + " and " + nameof<U>()
584
+ + equalsPathAtSuffix()
585
+ );
586
+ }
587
+ } else {
588
+ // Both unmanaged: no object header or idof available, fall back to compile-time
589
+ // nameof check. This is acceptable because unmanaged types don't participate in
590
+ // virtual dispatch or polymorphic inheritance — the compile-time type is reliable.
591
+ if (nameof<T>() != nameof<U>()) {
592
+ // @ts-ignore
593
+ if (isDefined(actual.__vitest_assemblyscript_deep_equals)) {
594
+ // see both-managed case above: same reasoning here, just behind a different type check
595
+ // Unmanaged types don't have virtual dispatch, so typename (if present) returns
596
+ // compile-time names — consistent with how the type check itself works here.
597
+ // @ts-ignore
598
+ if (isDefined(actual.__vitest_assemblyscript_typename)) {
599
+ // @ts-ignore
600
+ equalsRtmActualName = (<NonNullable<T>>actual).__vitest_assemblyscript_typename();
601
+ }
602
+ // @ts-ignore
603
+ if (isDefined(expected.__vitest_assemblyscript_typename)) {
604
+ // @ts-ignore
605
+ equalsRtmExpectedName = (<NonNullable<U>>expected).__vitest_assemblyscript_typename();
606
+ }
607
+ return __vitest_assemblyscript_EqualityResult.RuntimeTypeMismatch;
608
+ }
609
+
610
+ // see both-managed case above: handle potential mismatched type fallthrough
611
+ // for unmanaged stdlib / container type mismatches
612
+ throw new Error("Cannot compare deep equality between " + nameof<T>()
613
+ + " and " + nameof<U>()
614
+ + equalsPathAtSuffix()
615
+ );
616
+ }
617
+ }
618
+
619
+ // @ts-ignore
620
+ // User-defined reference types: delegate to compiler transform-injected deep equality
621
+ // method. Uses hard-coded method name because using a variable like `actual[DEEP_EQ_FUNC]`
622
+ // requires the class to define an index signature.
623
+ // Cast to NonNullable<T> because AS doesn't narrow nullability from the changetype-based
624
+ // null checks above — it requires explicit type narrowing to call methods on nullable types.
625
+ // Safe because both-null and one-null cases return early above.
626
+ if (isDefined(actual.__vitest_assemblyscript_deep_equals)) {
627
+ const nonNullActual = <NonNullable<T>>actual;
628
+ // @ts-ignore
629
+ return nonNullActual.__vitest_assemblyscript_deep_equals(changetype<usize>(expected));
630
+ }
631
+
632
+ // Fall back to reference identity for types without deep equality method
633
+ return changetype<usize>(actual) == changetype<usize>(expected)
634
+ ? __vitest_assemblyscript_EqualityResult.Equal
635
+ : __vitest_assemblyscript_EqualityResult.NotEqual;
636
+ }
637
+
638
+ /**
639
+ * Global bridge for the deep-equals compiler transform.
640
+ *
641
+ * Injected deep equality methods in user classes call this function for per-field
642
+ * comparisons. Declared global to make it available in all source files without import,
643
+ * solving the afterParse import resolution limitation (injected import statements
644
+ * are not processed by the AS compiler).
645
+ *
646
+ * Returns __vitest_assemblyscript_EqualityResult so injected methods can propagate type mismatch information
647
+ * from nested comparisons back to the top-level matcher.
648
+ *
649
+ * Loaded into the compilation transitively: user test imports
650
+ * vitest-pool-assemblyscript/assembly → index.ts → compare.ts.
651
+ */
652
+ // @ts-ignore
653
+ @global
654
+ function __vitest_assemblyscript_compare_equals<T, U>(actual: T, expected: U): __vitest_assemblyscript_EqualityResult {
655
+ return equals<T, U>(actual, expected);
237
656
  }
238
657
 
239
658
  export enum InequalityOperation {
@@ -367,29 +786,3 @@ export function truthyOrFalsey<T>(actual: T, expected: bool): bool {
367
786
  return actual ? expected == true : expected == false;
368
787
  }
369
788
 
370
- export function isNull<T>(value: T): bool {
371
- if (isReference<T>()) {
372
- if (isNullable<T>()) {
373
- return value == null;
374
- } else {
375
- return false;
376
- }
377
- } else {
378
- if (isBoolean<T>()) {
379
- return false;
380
- } else if (isVector<T>()) {
381
- return false;
382
- } else {
383
- return nameof<T>(value) == 'usize' && value == 0;
384
- }
385
- }
386
- }
387
-
388
- export function nan<T>(value: T): bool {
389
- if (isFloat<T>()) {
390
- // @ts-ignore
391
- return isNaN<T>(value);
392
- } else {
393
- return false;
394
- }
395
- }