codeflash 0.0.1 → 0.1.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.
- package/README.md +104 -0
- package/bin/codeflash-setup.js +13 -0
- package/bin/codeflash.js +131 -0
- package/package.json +71 -6
- package/runtime/capture.js +707 -0
- package/runtime/comparator.js +406 -0
- package/runtime/compare-results.js +329 -0
- package/runtime/index.js +79 -0
- package/runtime/serializer.js +851 -0
- package/scripts/postinstall.js +265 -0
- package/index.js +0 -7
|
@@ -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
|
+
};
|