codeflash 0.0.1 → 0.2.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.
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Codeflash Comparator - Deep equality comparison for JavaScript values
3
+ *
4
+ * This module provides a robust comparator function for comparing JavaScript
5
+ * values to determine behavioral equivalence between original and optimized code.
6
+ *
7
+ * Features:
8
+ * - Handles all JavaScript primitive types
9
+ * - Floating point comparison with relative tolerance (like Python's math.isclose)
10
+ * - Deep comparison of objects, arrays, Maps, Sets
11
+ * - Handles special values: NaN, Infinity, -Infinity, undefined, null
12
+ * - Handles TypedArrays, Date, RegExp, Error objects
13
+ * - Circular reference detection
14
+ * - Superset mode: allows new object to have additional keys
15
+ *
16
+ * Usage:
17
+ * const { comparator } = require('./codeflash-comparator');
18
+ * comparator(original, optimized); // Exact comparison
19
+ * comparator(original, optimized, { supersetObj: true }); // Allow extra keys
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ /**
25
+ * Default options for the comparator.
26
+ */
27
+ const DEFAULT_OPTIONS = {
28
+ // Relative tolerance for floating point comparison (like Python's rtol)
29
+ rtol: 1e-9,
30
+ // Absolute tolerance for floating point comparison (like Python's atol)
31
+ atol: 0,
32
+ // If true, the new object is allowed to have more keys than the original
33
+ supersetObj: false,
34
+ // Maximum recursion depth to prevent stack overflow
35
+ maxDepth: 1000,
36
+ };
37
+
38
+ /**
39
+ * Check if two floating point numbers are close within tolerance.
40
+ * Equivalent to Python's math.isclose(a, b, rel_tol, abs_tol).
41
+ *
42
+ * @param {number} a - First number
43
+ * @param {number} b - Second number
44
+ * @param {number} rtol - Relative tolerance (default: 1e-9)
45
+ * @param {number} atol - Absolute tolerance (default: 0)
46
+ * @returns {boolean} - True if numbers are close
47
+ */
48
+ function isClose(a, b, rtol = 1e-9, atol = 0) {
49
+ // Handle identical values (including both being 0)
50
+ if (a === b) return true;
51
+
52
+ // Handle NaN
53
+ if (Number.isNaN(a) && Number.isNaN(b)) return true;
54
+ if (Number.isNaN(a) || Number.isNaN(b)) return false;
55
+
56
+ // Handle Infinity
57
+ if (!Number.isFinite(a) || !Number.isFinite(b)) {
58
+ return a === b; // Both must be same infinity
59
+ }
60
+
61
+ // Use the same formula as Python's math.isclose
62
+ // abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
63
+ const diff = Math.abs(a - b);
64
+ const maxAbs = Math.max(Math.abs(a), Math.abs(b));
65
+ return diff <= Math.max(rtol * maxAbs, atol);
66
+ }
67
+
68
+ /**
69
+ * Get the precise type of a value for comparison.
70
+ *
71
+ * @param {any} value - The value to get the type of
72
+ * @returns {string} - The type name
73
+ */
74
+ function getType(value) {
75
+ if (value === null) return 'null';
76
+ if (value === undefined) return 'undefined';
77
+
78
+ const type = typeof value;
79
+ if (type !== 'object') return type;
80
+
81
+ // Get the constructor name for objects
82
+ const constructorName = value.constructor?.name;
83
+ if (constructorName) return constructorName;
84
+
85
+ // Fallback to Object.prototype.toString
86
+ return Object.prototype.toString.call(value).slice(8, -1);
87
+ }
88
+
89
+ /**
90
+ * Check if a value is a TypedArray.
91
+ *
92
+ * @param {any} value - The value to check
93
+ * @returns {boolean} - True if TypedArray
94
+ */
95
+ function isTypedArray(value) {
96
+ return ArrayBuffer.isView(value) && !(value instanceof DataView);
97
+ }
98
+
99
+ /**
100
+ * Compare two values for deep equality.
101
+ *
102
+ * @param {any} orig - Original value
103
+ * @param {any} newVal - New value to compare
104
+ * @param {Object} options - Comparison options
105
+ * @param {number} options.rtol - Relative tolerance for floats
106
+ * @param {number} options.atol - Absolute tolerance for floats
107
+ * @param {boolean} options.supersetObj - Allow new object to have extra keys
108
+ * @param {number} options.maxDepth - Maximum recursion depth
109
+ * @returns {boolean} - True if values are equivalent
110
+ */
111
+ function comparator(orig, newVal, options = {}) {
112
+ const opts = { ...DEFAULT_OPTIONS, ...options };
113
+
114
+ // Track visited objects to handle circular references
115
+ const visited = new WeakMap();
116
+
117
+ function compare(a, b, depth) {
118
+ // Check recursion depth
119
+ if (depth > opts.maxDepth) {
120
+ console.warn('[comparator] Maximum recursion depth exceeded');
121
+ return false;
122
+ }
123
+
124
+ // === Identical references ===
125
+ if (a === b) return true;
126
+
127
+ // === Handle null and undefined ===
128
+ if (a === null || a === undefined || b === null || b === undefined) {
129
+ return a === b;
130
+ }
131
+
132
+ // === Type checking ===
133
+ const typeA = typeof a;
134
+ const typeB = typeof b;
135
+
136
+ if (typeA !== typeB) {
137
+ // Special case: comparing number with BigInt
138
+ // In JavaScript, 1n !== 1, but we might want to consider them equal
139
+ // For strict behavioral comparison, we'll say they're different
140
+ return false;
141
+ }
142
+
143
+ // === Primitives ===
144
+
145
+ // Numbers (including NaN and Infinity)
146
+ if (typeA === 'number') {
147
+ return isClose(a, b, opts.rtol, opts.atol);
148
+ }
149
+
150
+ // Strings, booleans
151
+ if (typeA === 'string' || typeA === 'boolean') {
152
+ return a === b;
153
+ }
154
+
155
+ // BigInt
156
+ if (typeA === 'bigint') {
157
+ return a === b;
158
+ }
159
+
160
+ // Symbols - compare by description since Symbol() always creates unique
161
+ if (typeA === 'symbol') {
162
+ return a.description === b.description;
163
+ }
164
+
165
+ // Functions - compare by reference (same function)
166
+ if (typeA === 'function') {
167
+ // Functions are equal if they're the same reference
168
+ // or if they have the same name and source code
169
+ if (a === b) return true;
170
+ // For bound functions or native functions, we can only compare by reference
171
+ try {
172
+ return a.name === b.name && a.toString() === b.toString();
173
+ } catch (e) {
174
+ return false;
175
+ }
176
+ }
177
+
178
+ // === Objects (typeA === 'object') ===
179
+
180
+ // Check for circular references
181
+ if (visited.has(a)) {
182
+ // If we've seen 'a' before, check if 'b' was the corresponding value
183
+ return visited.get(a) === b;
184
+ }
185
+
186
+ // Get constructor names for type comparison
187
+ const constructorA = a.constructor?.name || 'Object';
188
+ const constructorB = b.constructor?.name || 'Object';
189
+
190
+ // Different constructors means different types
191
+ // Exception: plain objects might have different constructors due to different realms
192
+ if (constructorA !== constructorB) {
193
+ // Allow comparison between plain objects from different realms
194
+ if (!(constructorA === 'Object' && constructorB === 'Object')) {
195
+ return false;
196
+ }
197
+ }
198
+
199
+ // Mark as visited before recursing
200
+ visited.set(a, b);
201
+
202
+ try {
203
+ // === Arrays ===
204
+ if (Array.isArray(a)) {
205
+ if (!Array.isArray(b)) return false;
206
+ if (a.length !== b.length) return false;
207
+ return a.every((elem, i) => compare(elem, b[i], depth + 1));
208
+ }
209
+
210
+ // === TypedArrays (Int8Array, Uint8Array, Float32Array, etc.) ===
211
+ if (isTypedArray(a)) {
212
+ if (!isTypedArray(b)) return false;
213
+ if (a.constructor !== b.constructor) return false;
214
+ if (a.length !== b.length) return false;
215
+
216
+ // For float arrays, use tolerance comparison
217
+ if (a instanceof Float32Array || a instanceof Float64Array) {
218
+ for (let i = 0; i < a.length; i++) {
219
+ if (!isClose(a[i], b[i], opts.rtol, opts.atol)) return false;
220
+ }
221
+ return true;
222
+ }
223
+
224
+ // For integer arrays, use exact comparison
225
+ for (let i = 0; i < a.length; i++) {
226
+ if (a[i] !== b[i]) return false;
227
+ }
228
+ return true;
229
+ }
230
+
231
+ // === ArrayBuffer ===
232
+ if (a instanceof ArrayBuffer) {
233
+ if (!(b instanceof ArrayBuffer)) return false;
234
+ if (a.byteLength !== b.byteLength) return false;
235
+ const viewA = new Uint8Array(a);
236
+ const viewB = new Uint8Array(b);
237
+ for (let i = 0; i < viewA.length; i++) {
238
+ if (viewA[i] !== viewB[i]) return false;
239
+ }
240
+ return true;
241
+ }
242
+
243
+ // === DataView ===
244
+ if (a instanceof DataView) {
245
+ if (!(b instanceof DataView)) return false;
246
+ if (a.byteLength !== b.byteLength) return false;
247
+ for (let i = 0; i < a.byteLength; i++) {
248
+ if (a.getUint8(i) !== b.getUint8(i)) return false;
249
+ }
250
+ return true;
251
+ }
252
+
253
+ // === Date ===
254
+ if (a instanceof Date) {
255
+ if (!(b instanceof Date)) return false;
256
+ // Handle Invalid Date (NaN time)
257
+ const timeA = a.getTime();
258
+ const timeB = b.getTime();
259
+ if (Number.isNaN(timeA) && Number.isNaN(timeB)) return true;
260
+ return timeA === timeB;
261
+ }
262
+
263
+ // === RegExp ===
264
+ if (a instanceof RegExp) {
265
+ if (!(b instanceof RegExp)) return false;
266
+ return a.source === b.source && a.flags === b.flags;
267
+ }
268
+
269
+ // === Error ===
270
+ if (a instanceof Error) {
271
+ if (!(b instanceof Error)) return false;
272
+ // Compare error name and message
273
+ if (a.name !== b.name) return false;
274
+ if (a.message !== b.message) return false;
275
+ // Optionally compare stack traces (usually not, as they differ)
276
+ return true;
277
+ }
278
+
279
+ // === Map ===
280
+ if (a instanceof Map) {
281
+ if (!(b instanceof Map)) return false;
282
+ if (a.size !== b.size) return false;
283
+ for (const [key, val] of a) {
284
+ if (!b.has(key)) return false;
285
+ if (!compare(val, b.get(key), depth + 1)) return false;
286
+ }
287
+ return true;
288
+ }
289
+
290
+ // === Set ===
291
+ if (a instanceof Set) {
292
+ if (!(b instanceof Set)) return false;
293
+ if (a.size !== b.size) return false;
294
+ // For Sets, we need to find matching elements
295
+ // This is O(n^2) but necessary for deep comparison
296
+ const bArray = Array.from(b);
297
+ for (const valA of a) {
298
+ let found = false;
299
+ for (let i = 0; i < bArray.length; i++) {
300
+ if (compare(valA, bArray[i], depth + 1)) {
301
+ found = true;
302
+ bArray.splice(i, 1); // Remove matched element
303
+ break;
304
+ }
305
+ }
306
+ if (!found) return false;
307
+ }
308
+ return true;
309
+ }
310
+
311
+ // === WeakMap / WeakSet ===
312
+ // Cannot iterate over these, so we can only compare by reference
313
+ if (a instanceof WeakMap || a instanceof WeakSet) {
314
+ return a === b;
315
+ }
316
+
317
+ // === Promise ===
318
+ // Promises can only be compared by reference
319
+ if (a instanceof Promise) {
320
+ return a === b;
321
+ }
322
+
323
+ // === URL ===
324
+ if (typeof URL !== 'undefined' && a instanceof URL) {
325
+ if (!(b instanceof URL)) return false;
326
+ return a.href === b.href;
327
+ }
328
+
329
+ // === URLSearchParams ===
330
+ if (typeof URLSearchParams !== 'undefined' && a instanceof URLSearchParams) {
331
+ if (!(b instanceof URLSearchParams)) return false;
332
+ return a.toString() === b.toString();
333
+ }
334
+
335
+ // === Plain Objects ===
336
+ // This includes class instances
337
+
338
+ const keysA = Object.keys(a);
339
+ const keysB = Object.keys(b);
340
+
341
+ if (opts.supersetObj) {
342
+ // In superset mode, all keys from original must exist in new
343
+ // but new can have additional keys
344
+ for (const key of keysA) {
345
+ if (!(key in b)) return false;
346
+ if (!compare(a[key], b[key], depth + 1)) return false;
347
+ }
348
+ return true;
349
+ } else {
350
+ // Exact key matching
351
+ if (keysA.length !== keysB.length) return false;
352
+
353
+ for (const key of keysA) {
354
+ if (!(key in b)) return false;
355
+ if (!compare(a[key], b[key], depth + 1)) return false;
356
+ }
357
+ return true;
358
+ }
359
+ } finally {
360
+ // Clean up visited tracking
361
+ // Note: We don't delete from visited because the same object
362
+ // might appear multiple times in the structure
363
+ }
364
+ }
365
+
366
+ try {
367
+ return compare(orig, newVal, 0);
368
+ } catch (e) {
369
+ console.error('[comparator] Error during comparison:', e);
370
+ return false;
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Create a comparator with custom default options.
376
+ *
377
+ * @param {Object} defaultOptions - Default options for all comparisons
378
+ * @returns {Function} - Comparator function with bound defaults
379
+ */
380
+ function createComparator(defaultOptions = {}) {
381
+ const opts = { ...DEFAULT_OPTIONS, ...defaultOptions };
382
+ return (orig, newVal, overrideOptions = {}) => {
383
+ return comparator(orig, newVal, { ...opts, ...overrideOptions });
384
+ };
385
+ }
386
+
387
+ /**
388
+ * Strict comparator that requires exact equality (no tolerance).
389
+ */
390
+ const strictComparator = createComparator({ rtol: 0, atol: 0 });
391
+
392
+ /**
393
+ * Loose comparator with larger tolerance for floating point.
394
+ */
395
+ const looseComparator = createComparator({ rtol: 1e-6, atol: 1e-9 });
396
+
397
+ // Export public API
398
+ module.exports = {
399
+ comparator,
400
+ createComparator,
401
+ strictComparator,
402
+ looseComparator,
403
+ isClose,
404
+ getType,
405
+ DEFAULT_OPTIONS,
406
+ };