vitest-pool-assemblyscript 0.8.0 → 0.9.1

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 (95) hide show
  1. package/BINARYEN_VERSION +1 -1
  2. package/README.md +10 -13
  3. package/assembly/compare.ts +453 -95
  4. package/assembly/expect.ts +113 -62
  5. package/assembly/index.ts +0 -10
  6. package/assembly/utils.ts +168 -0
  7. package/binding.gyp +4 -4
  8. package/dist/{addon-interface-_pNXcbib.mjs → addon-interface-CYFXMbK7.mjs} +4 -3
  9. package/dist/addon-interface-CYFXMbK7.mjs.map +1 -0
  10. package/dist/{ast-visitor-C5gQqWD2.mjs → ast-visitor-CWEOd3UH.mjs} +15 -2
  11. package/dist/ast-visitor-CWEOd3UH.mjs.map +1 -0
  12. package/dist/{compile-runner-DOMhsLQE.mjs → compile-runner-BNFHRGZO.mjs} +8 -7
  13. package/dist/compile-runner-BNFHRGZO.mjs.map +1 -0
  14. package/dist/compiler/transforms/deep-equals.d.mts +14 -0
  15. package/dist/compiler/transforms/deep-equals.d.mts.map +1 -0
  16. package/dist/compiler/transforms/deep-equals.mjs +245 -0
  17. package/dist/compiler/transforms/deep-equals.mjs.map +1 -0
  18. package/dist/compiler/transforms/strip-inline.mjs +2 -2
  19. package/dist/{compiler-CIXpfKq0.mjs → compiler-Dqs-qd3I.mjs} +18 -8
  20. package/dist/compiler-Dqs-qd3I.mjs.map +1 -0
  21. package/dist/config/index-v3.d.mts +33 -3
  22. package/dist/config/index-v3.d.mts.map +1 -1
  23. package/dist/config/index-v3.mjs.map +1 -1
  24. package/dist/config/index.d.mts +29 -4
  25. package/dist/config/index.d.mts.map +1 -0
  26. package/dist/config/index.mjs +6 -6
  27. package/dist/{constants-DuBLuMjt.mjs → constants-DbxJ3hzg.mjs} +10 -97
  28. package/dist/constants-DbxJ3hzg.mjs.map +1 -0
  29. package/dist/coverage-provider/index.mjs +30 -10
  30. package/dist/coverage-provider/index.mjs.map +1 -1
  31. package/dist/{debug-Cm1VFmaz.mjs → debug-DtRAL4rM.mjs} +38 -4
  32. package/dist/debug-DtRAL4rM.mjs.map +1 -0
  33. package/dist/{feature-check-ELxw_Mji.mjs → feature-check-Bje3ntpV.mjs} +2 -2
  34. package/dist/{feature-check-ELxw_Mji.mjs.map → feature-check-Bje3ntpV.mjs.map} +1 -1
  35. package/dist/index-internal.d.mts +1 -1
  36. package/dist/index-internal.d.mts.map +1 -1
  37. package/dist/index-internal.mjs +4 -4
  38. package/dist/index-v3.d.mts.map +1 -1
  39. package/dist/index-v3.mjs +8 -7
  40. package/dist/index-v3.mjs.map +1 -1
  41. package/dist/index.d.mts +1 -2
  42. package/dist/index.mjs +6 -6
  43. package/dist/{load-user-imports-B3Iy_K8k.mjs → load-user-imports-Bx5ZlhSm.mjs} +9 -9
  44. package/dist/load-user-imports-Bx5ZlhSm.mjs.map +1 -0
  45. package/dist/{pool-runner-init-BDQtAGwQ.mjs → pool-runner-init-BqkwQ2tk.mjs} +7 -7
  46. package/dist/pool-runner-init-BqkwQ2tk.mjs.map +1 -0
  47. package/dist/{pool-runner-init-CvnB0-iN.d.mts → pool-runner-init-CNpRdA5u.d.mts} +2 -2
  48. package/dist/pool-runner-init-CNpRdA5u.d.mts.map +1 -0
  49. package/dist/pool-thread/compile-worker-thread.d.mts +1 -1
  50. package/dist/pool-thread/compile-worker-thread.d.mts.map +1 -1
  51. package/dist/pool-thread/compile-worker-thread.mjs +10 -13
  52. package/dist/pool-thread/compile-worker-thread.mjs.map +1 -1
  53. package/dist/pool-thread/test-worker-thread.d.mts +1 -1
  54. package/dist/pool-thread/test-worker-thread.d.mts.map +1 -1
  55. package/dist/pool-thread/test-worker-thread.mjs +8 -11
  56. package/dist/pool-thread/test-worker-thread.mjs.map +1 -1
  57. package/dist/pool-thread/v3-tinypool-thread.d.mts +1 -1
  58. package/dist/pool-thread/v3-tinypool-thread.d.mts.map +1 -1
  59. package/dist/pool-thread/v3-tinypool-thread.mjs +12 -18
  60. package/dist/pool-thread/v3-tinypool-thread.mjs.map +1 -1
  61. package/dist/{resolve-config-DhZ4lOSK.mjs → resolve-config-s9gSJSMc.mjs} +14 -5
  62. package/dist/resolve-config-s9gSJSMc.mjs.map +1 -0
  63. package/dist/{test-runner-C4I9VknR.mjs → test-runner-BGqc9uCK.mjs} +4 -4
  64. package/dist/{test-runner-C4I9VknR.mjs.map → test-runner-BGqc9uCK.mjs.map} +1 -1
  65. package/dist/{types-D0nprJo1.d.mts → types-DHVk5iAx.d.mts} +17 -11
  66. package/dist/types-DHVk5iAx.d.mts.map +1 -0
  67. package/dist/vitest-file-tasks-D8sOClGX.mjs +149 -0
  68. package/dist/vitest-file-tasks-D8sOClGX.mjs.map +1 -0
  69. package/dist/{vitest-tasks--ow4pacR.mjs → vitest-tasks-BZ24sghI.mjs} +6 -4
  70. package/dist/vitest-tasks-BZ24sghI.mjs.map +1 -0
  71. package/package.json +11 -14
  72. package/prebuilds/darwin-arm64/vitest-pool-assemblyscript.glibc.node +0 -0
  73. package/prebuilds/darwin-x64/vitest-pool-assemblyscript.glibc.node +0 -0
  74. package/prebuilds/linux-arm64/vitest-pool-assemblyscript.glibc.node +0 -0
  75. package/prebuilds/linux-x64/vitest-pool-assemblyscript.glibc.node +0 -0
  76. package/prebuilds/linux-x64/vitest-pool-assemblyscript.musl.node +0 -0
  77. package/prebuilds/win32-arm64/vitest-pool-assemblyscript.glibc.node +0 -0
  78. package/prebuilds/win32-x64/vitest-pool-assemblyscript.glibc.node +0 -0
  79. package/src/instrumentation/native/addon.cpp +29 -3
  80. package/dist/addon-interface-_pNXcbib.mjs.map +0 -1
  81. package/dist/ast-visitor-C5gQqWD2.mjs.map +0 -1
  82. package/dist/compile-runner-DOMhsLQE.mjs.map +0 -1
  83. package/dist/compiler-CIXpfKq0.mjs.map +0 -1
  84. package/dist/constants-DuBLuMjt.mjs.map +0 -1
  85. package/dist/custom-provider-options-YTk1m7At.d.mts +0 -26
  86. package/dist/custom-provider-options-YTk1m7At.d.mts.map +0 -1
  87. package/dist/debug-Cm1VFmaz.mjs.map +0 -1
  88. package/dist/load-user-imports-B3Iy_K8k.mjs.map +0 -1
  89. package/dist/pool-runner-init-BDQtAGwQ.mjs.map +0 -1
  90. package/dist/pool-runner-init-CvnB0-iN.d.mts.map +0 -1
  91. package/dist/resolve-config-DhZ4lOSK.mjs.map +0 -1
  92. package/dist/types-D0nprJo1.d.mts.map +0 -1
  93. package/dist/vitest-file-tasks-Bn9CrWt_.mjs +0 -61
  94. package/dist/vitest-file-tasks-Bn9CrWt_.mjs.map +0 -1
  95. package/dist/vitest-tasks--ow4pacR.mjs.map +0 -1
@@ -1,44 +1,298 @@
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;
229
+ }
230
+
231
+ function mapEquals<T, U>(actual: T, expected: U): __vitest_assemblyscript_EqualityResult {
232
+ if (actual instanceof Map && expected instanceof Map) {
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
+ );
241
+ }
242
+
243
+ if (actual.size != expected.size) {
244
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
245
+ }
246
+
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
+ }
269
+
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();
277
+ }
278
+
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;
33
287
  }
34
288
 
35
- function arrayBufferEquals<T, U>(actual: T, expected: U): bool {
289
+ function arrayBufferEquals<T, U>(actual: T, expected: U): __vitest_assemblyscript_EqualityResult {
36
290
  if (!(actual instanceof ArrayBuffer) || !(expected instanceof ArrayBuffer)) {
37
- return false;
291
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
38
292
  }
39
293
 
40
294
  if (actual.byteLength != expected.byteLength) {
41
- return false;
295
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
42
296
  }
43
297
 
44
298
  const actualPtr = changetype<usize>(actual);
@@ -49,7 +303,7 @@ function arrayBufferEquals<T, U>(actual: T, expected: U): bool {
49
303
  // compare 8 bytes at a time (u64 word-sized comparison)
50
304
  for (let i: usize = 0; i < wordCount; i++) {
51
305
  if (load<u64>(actualPtr + i * 8) != load<u64>(expectedPtr + i * 8)) {
52
- return false;
306
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
53
307
  }
54
308
  }
55
309
 
@@ -57,37 +311,11 @@ function arrayBufferEquals<T, U>(actual: T, expected: U): bool {
57
311
  const remainderOffset = wordCount * 8;
58
312
  for (let i: usize = 0; i < remainder; i++) {
59
313
  if (load<u8>(actualPtr + remainderOffset + i) != load<u8>(expectedPtr + remainderOffset + i)) {
60
- return false;
314
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
61
315
  }
62
316
  }
63
317
 
64
- return true;
65
- }
66
-
67
- function mapEquals<T, U>(actual: T, expected: U): bool {
68
- if (actual instanceof Map && expected instanceof Map) {
69
- if (actual.size != expected.size) {
70
- return false;
71
- }
72
-
73
- const expectedKeys = expected.keys();
74
-
75
- for (let i = 0; i < expectedKeys.length; i++) {
76
- const key = expectedKeys[i];
77
-
78
- if (!actual.has(key)) {
79
- return false;
80
- }
81
-
82
- if (!equals(actual.get(key), expected.get(key))) {
83
- return false;
84
- }
85
- }
86
-
87
- return true;
88
- }
89
-
90
- return false;
318
+ return __vitest_assemblyscript_EqualityResult.Equal;
91
319
  }
92
320
 
93
321
  /**
@@ -98,9 +326,11 @@ export function identical<T, U>(actual: T, expected: U): bool {
98
326
  // both refs
99
327
  if (isReference<T>() && isReference<U>()) {
100
328
  const actualIsNullable = isNullable<T>();
101
- 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;
102
332
  const expectedIsNullable = isNullable<U>();
103
- const expectedIsNull = expectedIsNullable && expected == null;
333
+ const expectedIsNull = expectedIsNullable && changetype<usize>(expected) == 0;
104
334
 
105
335
  // null refs
106
336
  if (actualIsNull && expectedIsNull) {
@@ -116,12 +346,26 @@ export function identical<T, U>(actual: T, expected: U): bool {
116
346
  // object refs
117
347
  return changetype<usize>(actual) == changetype<usize>(expected);
118
348
  }
119
- } else if (isReference<T>() && !isReference<U>()) {
120
- // actual is null ref, expected bare null
121
- return changetype<usize>(actual) == usize(0) && expected == usize(0);
122
- } else if (!isReference<T>() && isReference<U>()) {
123
- // actual is bare null, expected is null ref
124
- 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
+ );
125
369
  } else { // both primitives
126
370
  if ( (isBoolean<T>() && !isBoolean<U>()) || (!isBoolean<T>() && isBoolean<U>())
127
371
  ) {
@@ -153,6 +397,7 @@ export function identical<T, U>(actual: T, expected: U): bool {
153
397
  if (sizeof<U>() >= sizeof<T>()) {
154
398
  throw new Error(
155
399
  "Cannot compare " + nameof<T>() + " with " + nameof<U>()
400
+ + equalsPathAtSuffix()
156
401
  + ": float precision is insufficient for the integer type's range."
157
402
  + " Cast both values to f64 before comparing, e.g. expect(f64(a)).toBe(f64(b))."
158
403
  + " Note: large integer values may lose precision when cast to f64, which could cause false positives."
@@ -162,18 +407,24 @@ export function identical<T, U>(actual: T, expected: U): bool {
162
407
  if (sizeof<T>() >= sizeof<U>()) {
163
408
  throw new Error(
164
409
  "Cannot compare " + nameof<T>() + " with " + nameof<U>()
410
+ + equalsPathAtSuffix()
165
411
  + ": float precision is insufficient for the integer type's range."
166
412
  + " Cast both values to f64 before comparing, e.g. expect(f64(a)).toBe(f64(b))."
167
413
  + " Note: large integer values may lose precision when cast to f64, which could cause false positives."
168
414
  );
169
415
  }
170
416
  }
417
+
418
+ // if we got here, cast to f64 is safe without precision loss - cast to compare
171
419
  return f64(actual) === f64(expected);
172
420
  } else if (isVector<T>() && isVector<U>()) {
173
421
  return <v128>actual == <v128>expected;
174
422
  } else {
175
- return false;
176
- }
423
+ throw new Error(
424
+ "Cannot compare " + nameof<T>() + " with " + nameof<U>()
425
+ + equalsPathAtSuffix() + ": incompatible types."
426
+ );
427
+ }
177
428
  }
178
429
  }
179
430
 
@@ -213,9 +464,13 @@ export function closeTo<T, U>(actual: T, expected: U, precision: i32 = 2): bool
213
464
 
214
465
  /**
215
466
  * Generic value equality comparison. Assumes comparable types for both values.
216
- * 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.
217
472
  */
218
- export function equals<T, U>(actual: T, expected: U): bool {
473
+ export function equals<T, U>(actual: T, expected: U): __vitest_assemblyscript_EqualityResult {
219
474
  let exactMatch: bool = false;
220
475
 
221
476
  // allow boolean-to-number comparisons here
@@ -229,22 +484,37 @@ export function equals<T, U>(actual: T, expected: U): bool {
229
484
 
230
485
  if (!isReference<T>() || isString<T>() || isVector<T>()) {
231
486
  // non-bool primitives or strings: return result of comparing
232
- return exactMatch;
487
+ return exactMatch ? __vitest_assemblyscript_EqualityResult.Equal : __vitest_assemblyscript_EqualityResult.NotEqual;
233
488
  } else if (exactMatch) {
234
489
  // primitive / reference comparison passed already
235
- return true;
490
+ return __vitest_assemblyscript_EqualityResult.Equal;
236
491
  }
237
492
 
238
493
  if (isNullable<T>()) {
239
- if (actual == null && expected == null) {
240
- 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;
241
501
  }
242
502
 
243
- if ( (actual == null && expected != null) || (actual != null && expected == null) ) {
244
- return false;
503
+ if (actualIsNull != expectedIsNull) {
504
+ return __vitest_assemblyscript_EqualityResult.NotEqual;
245
505
  }
246
506
  }
247
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
+
248
518
  if (isArrayLike<T>(actual) && isArrayLike<U>(expected)) {
249
519
  return arrayEquals(actual, expected);
250
520
  }
@@ -258,17 +528,131 @@ export function equals<T, U>(actual: T, expected: U): bool {
258
528
  return arrayBufferEquals(actual, expected);
259
529
  }
260
530
 
261
- if (idof<T>() != idof<U>()) {
262
- throw new Error("Cannot compare equality between " + nameof<T>(actual)
263
- + " 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()
264
540
  );
265
541
  }
266
542
 
267
- // TODO value compare
268
- throw new Error("Deep equality comparison of user-defined reference types"
269
- + " is not yet implemented, and these references are not identical."
270
- + " Use toBe() for reference equality."
271
- );
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);
272
656
  }
273
657
 
274
658
  export enum InequalityOperation {
@@ -402,29 +786,3 @@ export function truthyOrFalsey<T>(actual: T, expected: bool): bool {
402
786
  return actual ? expected == true : expected == false;
403
787
  }
404
788
 
405
- export function isNull<T>(value: T): bool {
406
- if (isReference<T>()) {
407
- if (isNullable<T>()) {
408
- return value == null;
409
- } else {
410
- return false;
411
- }
412
- } else {
413
- if (isBoolean<T>()) {
414
- return false;
415
- } else if (isVector<T>()) {
416
- return false;
417
- } else {
418
- return nameof<T>(value) == 'usize' && value == 0;
419
- }
420
- }
421
- }
422
-
423
- export function nan<T>(value: T): bool {
424
- if (isFloat<T>()) {
425
- // @ts-ignore
426
- return isNaN<T>(value);
427
- } else {
428
- return false;
429
- }
430
- }