@tracecode/harness 0.4.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 (55) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/LICENSE +674 -0
  3. package/README.md +266 -0
  4. package/dist/browser.cjs +1352 -0
  5. package/dist/browser.cjs.map +1 -0
  6. package/dist/browser.d.cts +49 -0
  7. package/dist/browser.d.ts +49 -0
  8. package/dist/browser.js +1317 -0
  9. package/dist/browser.js.map +1 -0
  10. package/dist/cli.cjs +70 -0
  11. package/dist/cli.cjs.map +1 -0
  12. package/dist/cli.d.cts +1 -0
  13. package/dist/cli.d.ts +1 -0
  14. package/dist/cli.js +70 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/core.cjs +286 -0
  17. package/dist/core.cjs.map +1 -0
  18. package/dist/core.d.cts +69 -0
  19. package/dist/core.d.ts +69 -0
  20. package/dist/core.js +254 -0
  21. package/dist/core.js.map +1 -0
  22. package/dist/index.cjs +2603 -0
  23. package/dist/index.cjs.map +1 -0
  24. package/dist/index.d.cts +6 -0
  25. package/dist/index.d.ts +6 -0
  26. package/dist/index.js +2538 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/internal/browser.cjs +647 -0
  29. package/dist/internal/browser.cjs.map +1 -0
  30. package/dist/internal/browser.d.cts +143 -0
  31. package/dist/internal/browser.d.ts +143 -0
  32. package/dist/internal/browser.js +617 -0
  33. package/dist/internal/browser.js.map +1 -0
  34. package/dist/javascript.cjs +549 -0
  35. package/dist/javascript.cjs.map +1 -0
  36. package/dist/javascript.d.cts +11 -0
  37. package/dist/javascript.d.ts +11 -0
  38. package/dist/javascript.js +518 -0
  39. package/dist/javascript.js.map +1 -0
  40. package/dist/python.cjs +744 -0
  41. package/dist/python.cjs.map +1 -0
  42. package/dist/python.d.cts +97 -0
  43. package/dist/python.d.ts +97 -0
  44. package/dist/python.js +698 -0
  45. package/dist/python.js.map +1 -0
  46. package/dist/runtime-types-C7d1LFbx.d.ts +85 -0
  47. package/dist/runtime-types-Dvgn07z9.d.cts +85 -0
  48. package/dist/types-Bzr1Ohcf.d.cts +96 -0
  49. package/dist/types-Bzr1Ohcf.d.ts +96 -0
  50. package/package.json +89 -0
  51. package/workers/javascript/javascript-worker.js +2918 -0
  52. package/workers/python/generated-python-harness-snippets.js +20 -0
  53. package/workers/python/pyodide-worker.js +1197 -0
  54. package/workers/python/runtime-core.js +1529 -0
  55. package/workers/vendor/typescript.js +200276 -0
@@ -0,0 +1,1197 @@
1
+ /**
2
+ * Pyodide Web Worker
3
+ *
4
+ * Runs Python code execution in a separate thread to avoid blocking the UI.
5
+ * This worker handles loading Pyodide, executing code, and returning traces.
6
+ *
7
+ * This is the canonical worker implementation for the browser Python runtime.
8
+ * The legacy lib/execution/pyodide.ts path is deprecated and should not be used.
9
+ *
10
+ * IMPORTANT: Shared harness snippets are defined in:
11
+ * - packages/harness-python/src/python-harness-template.ts
12
+ * and generated into:
13
+ * - packages/harness-python/src/generated/python-harness-snippets.ts
14
+ * - workers/python/generated-python-harness-snippets.js
15
+ *
16
+ * Runtime trace/execute builders now live in:
17
+ * - workers/python/runtime-core.js
18
+ *
19
+ * Keep worker/runtime-core split aligned with generated shared snippets and
20
+ * validate with:
21
+ * pnpm test:python-regression-gate
22
+ *
23
+ * Version: 4 (raises exception to abort infinite loops)
24
+ */
25
+
26
+ // Worker version: 4
27
+
28
+ // Pyodide index URLs in fallback order
29
+ const PYODIDE_INDEX_URLS = [
30
+ 'https://cdn.jsdelivr.net/pyodide/v0.29.0/full/',
31
+ 'https://unpkg.com/pyodide@0.29.0/',
32
+ ];
33
+ const GENERATED_HARNESS_SNIPPETS_PATHS = [
34
+ './generated-python-harness-snippets.js',
35
+ ];
36
+
37
+ let pyodide = null;
38
+ let isLoading = false;
39
+ let loadPromise = null;
40
+ const WORKER_DEBUG = (() => {
41
+ try {
42
+ return typeof self !== 'undefined' && typeof self.location?.search === 'string' && self.location.search.includes('dev=');
43
+ } catch {
44
+ return false;
45
+ }
46
+ })();
47
+
48
+ // Interview mode runtime guard defaults. These are intentionally coarse
49
+ // safeguards to stop runaway executions without exposing internals.
50
+ const INTERVIEW_GUARD_DEFAULTS = Object.freeze({
51
+ maxLineEvents: 400000,
52
+ maxSingleLineHits: 150000,
53
+ maxCallDepth: 2000,
54
+ maxMemoryBytes: 96 * 1024 * 1024, // 96 MB
55
+ memoryCheckEvery: 200,
56
+ });
57
+
58
+ // Load generated shared harness snippets when available. Keep worker startup
59
+ // resilient by falling back to embedded implementations if this import fails.
60
+ if (typeof importScripts === 'function') {
61
+ for (const scriptPath of GENERATED_HARNESS_SNIPPETS_PATHS) {
62
+ try {
63
+ importScripts(scriptPath);
64
+ if (WORKER_DEBUG) {
65
+ console.log('[PyodideWorker] Loaded generated harness snippets from', scriptPath);
66
+ }
67
+ break;
68
+ } catch (error) {
69
+ if (WORKER_DEBUG) {
70
+ const message = error instanceof Error ? error.message : String(error);
71
+ console.warn('[PyodideWorker] Failed to load generated harness snippets from', scriptPath, message);
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Convert a JavaScript value to a Python literal string.
79
+ * Prefer the generated shared implementation when available.
80
+ */
81
+ function fallbackToPythonLiteral(value) {
82
+ if (value === null || value === undefined) {
83
+ return 'None';
84
+ }
85
+ if (typeof value === 'boolean') {
86
+ return value ? 'True' : 'False';
87
+ }
88
+ if (typeof value === 'number') {
89
+ return String(value);
90
+ }
91
+ if (typeof value === 'string') {
92
+ return JSON.stringify(value);
93
+ }
94
+ if (Array.isArray(value)) {
95
+ return '[' + value.map(fallbackToPythonLiteral).join(', ') + ']';
96
+ }
97
+ if (typeof value === 'object') {
98
+ const entries = Object.entries(value)
99
+ .map(([k, v]) => `${JSON.stringify(k)}: ${fallbackToPythonLiteral(v)}`)
100
+ .join(', ');
101
+ return '{' + entries + '}';
102
+ }
103
+ return JSON.stringify(value);
104
+ }
105
+
106
+ const toPythonLiteralImpl =
107
+ typeof self !== 'undefined' && typeof self.__TRACECODE_toPythonLiteral === 'function'
108
+ ? self.__TRACECODE_toPythonLiteral
109
+ : fallbackToPythonLiteral;
110
+
111
+ function toPythonLiteral(value) {
112
+ return toPythonLiteralImpl(value);
113
+ }
114
+
115
+ const sharedHarnessSnippets =
116
+ typeof self !== 'undefined' &&
117
+ self.__TRACECODE_PYTHON_HARNESS__ &&
118
+ typeof self.__TRACECODE_PYTHON_HARNESS__ === 'object'
119
+ ? self.__TRACECODE_PYTHON_HARNESS__
120
+ : null;
121
+
122
+ function resolveSharedPythonSnippet(key, fallback) {
123
+ if (!sharedHarnessSnippets) return fallback;
124
+ const candidate = sharedHarnessSnippets[key];
125
+ return typeof candidate === 'string' ? candidate : fallback;
126
+ }
127
+
128
+ const PYTHON_CLASS_DEFINITIONS_SNIPPET = resolveSharedPythonSnippet(
129
+ 'PYTHON_CLASS_DEFINITIONS',
130
+ `
131
+ class TreeNode:
132
+ def __init__(self, val=0, left=None, right=None):
133
+ self.val = val
134
+ self.value = val
135
+ self.left = left
136
+ self.right = right
137
+ def __getitem__(self, key):
138
+ if key == 'val': return getattr(self, 'val', getattr(self, 'value', None))
139
+ if key == 'value': return getattr(self, 'value', getattr(self, 'val', None))
140
+ if key == 'left': return self.left
141
+ if key == 'right': return self.right
142
+ raise KeyError(key)
143
+ def get(self, key, default=None):
144
+ if key == 'val': return getattr(self, 'val', getattr(self, 'value', default))
145
+ if key == 'value': return getattr(self, 'value', getattr(self, 'val', default))
146
+ if key == 'left': return self.left
147
+ if key == 'right': return self.right
148
+ return default
149
+ def __repr__(self):
150
+ return f"TreeNode({getattr(self, 'val', getattr(self, 'value', None))})"
151
+
152
+ class ListNode:
153
+ def __init__(self, val=0, next=None):
154
+ self.val = val
155
+ self.value = val
156
+ self.next = next
157
+ def __getitem__(self, key):
158
+ if key == 'val': return getattr(self, 'val', getattr(self, 'value', None))
159
+ if key == 'value': return getattr(self, 'value', getattr(self, 'val', None))
160
+ if key == 'next': return self.next
161
+ raise KeyError(key)
162
+ def get(self, key, default=None):
163
+ if key == 'val': return getattr(self, 'val', getattr(self, 'value', default))
164
+ if key == 'value': return getattr(self, 'value', getattr(self, 'val', default))
165
+ if key == 'next': return self.next
166
+ return default
167
+ def __repr__(self):
168
+ return f"ListNode({getattr(self, 'val', getattr(self, 'value', None))})"
169
+ `
170
+ );
171
+
172
+ const PYTHON_CONVERSION_HELPERS_SNIPPET = resolveSharedPythonSnippet(
173
+ 'PYTHON_CONVERSION_HELPERS',
174
+ `
175
+ def _ensure_node_value_aliases(node):
176
+ if node is None:
177
+ return node
178
+ try:
179
+ has_val = hasattr(node, 'val')
180
+ has_value = hasattr(node, 'value')
181
+ if has_value and not has_val:
182
+ try:
183
+ setattr(node, 'val', getattr(node, 'value'))
184
+ except Exception:
185
+ pass
186
+ elif has_val and not has_value:
187
+ try:
188
+ setattr(node, 'value', getattr(node, 'val'))
189
+ except Exception:
190
+ pass
191
+ except Exception:
192
+ pass
193
+ return node
194
+
195
+ def _dict_to_tree(d):
196
+ if d is None:
197
+ return None
198
+ if not isinstance(d, dict):
199
+ return d
200
+ if 'val' not in d and 'value' not in d:
201
+ return d
202
+ node = TreeNode(d.get('val', d.get('value', 0)))
203
+ _ensure_node_value_aliases(node)
204
+ node.left = _dict_to_tree(d.get('left'))
205
+ node.right = _dict_to_tree(d.get('right'))
206
+ return node
207
+
208
+ def _dict_to_list(d, _refs=None):
209
+ if _refs is None:
210
+ _refs = {}
211
+ if d is None:
212
+ return None
213
+ if not isinstance(d, dict):
214
+ return d
215
+ if '__ref__' in d:
216
+ return _refs.get(d.get('__ref__'))
217
+ if 'val' not in d and 'value' not in d:
218
+ return d
219
+ node = ListNode(d.get('val', d.get('value', 0)))
220
+ _ensure_node_value_aliases(node)
221
+ node_id = d.get('__id__')
222
+ if isinstance(node_id, str) and node_id:
223
+ _refs[node_id] = node
224
+ node.next = _dict_to_list(d.get('next'), _refs)
225
+ return node
226
+ `
227
+ );
228
+
229
+ const PYTHON_TRACE_SERIALIZE_FUNCTION_SNIPPET = resolveSharedPythonSnippet(
230
+ 'PYTHON_TRACE_SERIALIZE_FUNCTION',
231
+ `
232
+ # Sentinel to mark skipped values (functions, etc.) - distinct from None
233
+ _SKIP_SENTINEL = "__TRACECODE_SKIP__"
234
+ _MAX_SERIALIZE_DEPTH = 48
235
+
236
+ def _serialize(obj, depth=0, node_refs=None):
237
+ if node_refs is None:
238
+ node_refs = {}
239
+ if isinstance(obj, (bool, int, str, type(None))):
240
+ return obj
241
+ elif isinstance(obj, float):
242
+ if not math.isfinite(obj):
243
+ if math.isnan(obj):
244
+ return "NaN"
245
+ return "Infinity" if obj > 0 else "-Infinity"
246
+ return obj
247
+ if depth > _MAX_SERIALIZE_DEPTH:
248
+ return "<max depth>"
249
+ elif isinstance(obj, (list, tuple)):
250
+ return [_serialize(x, depth + 1, node_refs) for x in obj]
251
+ elif getattr(obj, '__class__', None) and getattr(obj.__class__, '__name__', '') == 'deque':
252
+ return [_serialize(x, depth + 1, node_refs) for x in obj]
253
+ elif isinstance(obj, dict):
254
+ return {str(k): _serialize(v, depth + 1, node_refs) for k, v in obj.items()}
255
+ elif isinstance(obj, set):
256
+ # Use try/except for sorting to handle heterogeneous sets
257
+ try:
258
+ sorted_vals = sorted([_serialize(x, depth + 1, node_refs) for x in obj])
259
+ except TypeError:
260
+ sorted_vals = [_serialize(x, depth + 1, node_refs) for x in obj]
261
+ return {"__type__": "set", "values": sorted_vals}
262
+ elif (hasattr(obj, 'val') or hasattr(obj, 'value')) and (hasattr(obj, 'left') or hasattr(obj, 'right')):
263
+ obj_ref = id(obj)
264
+ if obj_ref in node_refs:
265
+ return {"__ref__": node_refs[obj_ref]}
266
+ node_id = f"tree-{obj_ref}"
267
+ node_refs[obj_ref] = node_id
268
+ result = {
269
+ "__type__": "TreeNode",
270
+ "__id__": node_id,
271
+ "val": _serialize(getattr(obj, 'val', getattr(obj, 'value', None)), depth + 1, node_refs),
272
+ }
273
+ if hasattr(obj, 'left'):
274
+ result["left"] = _serialize(obj.left, depth + 1, node_refs)
275
+ if hasattr(obj, 'right'):
276
+ result["right"] = _serialize(obj.right, depth + 1, node_refs)
277
+ return result
278
+ elif (hasattr(obj, 'val') or hasattr(obj, 'value')) and hasattr(obj, 'next'):
279
+ obj_ref = id(obj)
280
+ if obj_ref in node_refs:
281
+ return {"__ref__": node_refs[obj_ref]}
282
+ node_id = f"list-{obj_ref}"
283
+ node_refs[obj_ref] = node_id
284
+ result = {
285
+ "__type__": "ListNode",
286
+ "__id__": node_id,
287
+ "val": _serialize(getattr(obj, 'val', getattr(obj, 'value', None)), depth + 1, node_refs),
288
+ }
289
+ result["next"] = _serialize(obj.next, depth + 1, node_refs)
290
+ return result
291
+ elif callable(obj):
292
+ # Skip functions entirely - return sentinel
293
+ return _SKIP_SENTINEL
294
+ else:
295
+ repr_str = repr(obj)
296
+ # Filter out function-like representations (e.g., <function foo at 0x...>)
297
+ if repr_str.startswith('<') and repr_str.endswith('>'):
298
+ return _SKIP_SENTINEL
299
+ return repr_str
300
+ `
301
+ );
302
+
303
+ const PYTHON_EXECUTE_SERIALIZE_FUNCTION_SNIPPET = resolveSharedPythonSnippet(
304
+ 'PYTHON_EXECUTE_SERIALIZE_FUNCTION',
305
+ `
306
+ _MAX_SERIALIZE_DEPTH = 48
307
+
308
+ def _serialize(obj, depth=0):
309
+ if isinstance(obj, (bool, int, str, type(None))):
310
+ return obj
311
+ elif isinstance(obj, float):
312
+ if not math.isfinite(obj):
313
+ if math.isnan(obj):
314
+ return "NaN"
315
+ return "Infinity" if obj > 0 else "-Infinity"
316
+ return obj
317
+ if depth > _MAX_SERIALIZE_DEPTH:
318
+ return "<max depth>"
319
+ elif isinstance(obj, (list, tuple)):
320
+ return [_serialize(x, depth + 1) for x in obj]
321
+ elif getattr(obj, '__class__', None) and getattr(obj.__class__, '__name__', '') == 'deque':
322
+ return [_serialize(x, depth + 1) for x in obj]
323
+ elif isinstance(obj, dict):
324
+ return {str(k): _serialize(v, depth + 1) for k, v in obj.items()}
325
+ elif isinstance(obj, set):
326
+ try:
327
+ return {"__type__": "set", "values": sorted([_serialize(x, depth + 1) for x in obj])}
328
+ except TypeError:
329
+ return {"__type__": "set", "values": [_serialize(x, depth + 1) for x in obj]}
330
+ elif (hasattr(obj, 'val') or hasattr(obj, 'value')) and (hasattr(obj, 'left') or hasattr(obj, 'right')):
331
+ result = {"__type__": "TreeNode", "val": _serialize(getattr(obj, 'val', getattr(obj, 'value', None)), depth + 1)}
332
+ if hasattr(obj, 'left'):
333
+ result["left"] = _serialize(obj.left, depth + 1)
334
+ if hasattr(obj, 'right'):
335
+ result["right"] = _serialize(obj.right, depth + 1)
336
+ return result
337
+ elif (hasattr(obj, 'val') or hasattr(obj, 'value')) and hasattr(obj, 'next'):
338
+ result = {"__type__": "ListNode", "val": _serialize(getattr(obj, 'val', getattr(obj, 'value', None)), depth + 1)}
339
+ result["next"] = _serialize(obj.next, depth + 1)
340
+ return result
341
+ elif callable(obj):
342
+ return None
343
+ else:
344
+ repr_str = repr(obj)
345
+ if repr_str.startswith('<') and repr_str.endswith('>'):
346
+ return None
347
+ return repr_str
348
+ `
349
+ );
350
+
351
+ /**
352
+ * Load Pyodide
353
+ */
354
+ async function loadPyodideInstance() {
355
+ if (pyodide) return pyodide;
356
+ if (loadPromise) return loadPromise;
357
+
358
+ isLoading = true;
359
+
360
+ loadPromise = (async () => {
361
+ try {
362
+ const bootstrapErrors = [];
363
+
364
+ if (typeof self.loadPyodide !== 'function') {
365
+ let loadedBootstrap = false;
366
+
367
+ for (const indexURL of PYODIDE_INDEX_URLS) {
368
+ try {
369
+ importScripts(`${indexURL}pyodide.js`);
370
+ loadedBootstrap = true;
371
+ if (WORKER_DEBUG) {
372
+ console.log('[PyodideWorker] Loaded bootstrap script from', indexURL);
373
+ }
374
+ break;
375
+ } catch (error) {
376
+ const message = error instanceof Error ? error.message : String(error);
377
+ bootstrapErrors.push(`${indexURL}pyodide.js (${message})`);
378
+ }
379
+ }
380
+
381
+ if (!loadedBootstrap || typeof self.loadPyodide !== 'function') {
382
+ throw new Error(
383
+ `Unable to load Pyodide bootstrap script. Tried: ${bootstrapErrors.join(' | ')}`
384
+ );
385
+ }
386
+ }
387
+
388
+ const initErrors = [];
389
+ for (const indexURL of PYODIDE_INDEX_URLS) {
390
+ try {
391
+ pyodide = await self.loadPyodide({ indexURL });
392
+ if (WORKER_DEBUG) {
393
+ console.log('[PyodideWorker] Initialized runtime from', indexURL);
394
+ }
395
+ return pyodide;
396
+ } catch (error) {
397
+ const message = error instanceof Error ? error.message : String(error);
398
+ initErrors.push(`${indexURL} (${message})`);
399
+ }
400
+ }
401
+
402
+ throw new Error(`Unable to initialize Pyodide runtime. Tried: ${initErrors.join(' | ')}`);
403
+ } catch (error) {
404
+ loadPromise = null;
405
+ throw error;
406
+ } finally {
407
+ isLoading = false;
408
+ }
409
+ })();
410
+
411
+ return loadPromise;
412
+ }
413
+
414
+
415
+ const PYODIDE_RUNTIME_CORE_PATHS = [
416
+ './pyodide/runtime-core.js',
417
+ ];
418
+
419
+ let pyodideRuntimeCore = null;
420
+ let pyodideRuntimeCoreLoadAttempted = false;
421
+
422
+ function loadPyodideRuntimeCore() {
423
+ if (pyodideRuntimeCore) return pyodideRuntimeCore;
424
+
425
+ if (!pyodideRuntimeCoreLoadAttempted) {
426
+ pyodideRuntimeCoreLoadAttempted = true;
427
+
428
+ if (typeof importScripts === 'function') {
429
+ for (const scriptPath of PYODIDE_RUNTIME_CORE_PATHS) {
430
+ try {
431
+ importScripts(scriptPath);
432
+ if (WORKER_DEBUG) {
433
+ console.log('[PyodideWorker] Loaded runtime core from', scriptPath);
434
+ }
435
+ break;
436
+ } catch (error) {
437
+ if (WORKER_DEBUG) {
438
+ const message = error instanceof Error ? error.message : String(error);
439
+ console.warn('[PyodideWorker] Failed to load runtime core from', scriptPath, message);
440
+ }
441
+ }
442
+ }
443
+ }
444
+ }
445
+
446
+ const runtime =
447
+ typeof self !== 'undefined' &&
448
+ self.__TRACECODE_PYODIDE_RUNTIME__ &&
449
+ typeof self.__TRACECODE_PYODIDE_RUNTIME__ === 'object'
450
+ ? self.__TRACECODE_PYODIDE_RUNTIME__
451
+ : null;
452
+
453
+ if (!runtime) {
454
+ throw new Error('Pyodide runtime core failed to load');
455
+ }
456
+
457
+ pyodideRuntimeCore = runtime;
458
+ return pyodideRuntimeCore;
459
+ }
460
+
461
+ function buildRuntimeDeps() {
462
+ return {
463
+ toPythonLiteral,
464
+ PYTHON_CLASS_DEFINITIONS_SNIPPET,
465
+ PYTHON_CONVERSION_HELPERS_SNIPPET,
466
+ PYTHON_TRACE_SERIALIZE_FUNCTION_SNIPPET,
467
+ PYTHON_EXECUTE_SERIALIZE_FUNCTION_SNIPPET,
468
+ INTERVIEW_GUARD_DEFAULTS,
469
+ loadPyodideInstance,
470
+ getPyodide: () => pyodide,
471
+ performanceNow: () => performance.now(),
472
+ };
473
+ }
474
+
475
+ /**
476
+ * Generate the tracing wrapper code for step-by-step execution.
477
+ * Delegates to the runtime core module.
478
+ */
479
+ function generateTracingCode(userCode, functionName, inputs, executionStyle = 'function', options = {}) {
480
+ return loadPyodideRuntimeCore().generateTracingCode(
481
+ buildRuntimeDeps(),
482
+ userCode,
483
+ functionName,
484
+ inputs,
485
+ executionStyle,
486
+ options
487
+ );
488
+ }
489
+
490
+ /**
491
+ * Parse Python error message.
492
+ * Delegates to the runtime core module.
493
+ */
494
+ function parsePythonError(rawError, userCodeStartLine, userCodeLineCount) {
495
+ return loadPyodideRuntimeCore().parsePythonError(rawError, userCodeStartLine, userCodeLineCount);
496
+ }
497
+
498
+ /**
499
+ * Execute Python code with tracing.
500
+ * Delegates to the runtime core module.
501
+ */
502
+ async function executeWithTracing(code, functionName, inputs, executionStyle = 'function', options = {}) {
503
+ return loadPyodideRuntimeCore().executeWithTracing(
504
+ buildRuntimeDeps(),
505
+ code,
506
+ functionName,
507
+ inputs,
508
+ executionStyle,
509
+ options
510
+ );
511
+ }
512
+
513
+ /**
514
+ * Execute Python code without tracing (for running tests).
515
+ * Delegates to the runtime core module.
516
+ */
517
+ async function executeCode(code, functionName, inputs, executionStyle = 'function', options = {}) {
518
+ return loadPyodideRuntimeCore().executeCode(
519
+ buildRuntimeDeps(),
520
+ code,
521
+ functionName,
522
+ inputs,
523
+ executionStyle,
524
+ options
525
+ );
526
+ }
527
+
528
+ async function processMessage(data) {
529
+ const { id, type, payload } = data;
530
+ try {
531
+ switch (type) {
532
+ case 'init': {
533
+ const startTime = performance.now();
534
+ await loadPyodideInstance();
535
+ const loadTimeMs = performance.now() - startTime;
536
+ self.postMessage({ id, type: 'init-result', payload: { success: true, loadTimeMs } });
537
+ break;
538
+ }
539
+
540
+ case 'execute-with-tracing': {
541
+ const { code, functionName, inputs, executionStyle, options } = payload;
542
+ const result = await executeWithTracing(code, functionName, inputs, executionStyle ?? 'function', options);
543
+ analyzerInitialized = false;
544
+ self.postMessage({ id, type: 'execute-result', payload: result });
545
+ break;
546
+ }
547
+
548
+ case 'execute-code': {
549
+ const { code, functionName, inputs, executionStyle } = payload;
550
+ const result = await executeCode(code, functionName, inputs, executionStyle ?? 'function');
551
+ analyzerInitialized = false;
552
+ self.postMessage({ id, type: 'execute-result', payload: result });
553
+ break;
554
+ }
555
+
556
+ case 'execute-code-interview': {
557
+ const { code, functionName, inputs, executionStyle } = payload;
558
+ const result = await executeCode(code, functionName, inputs, executionStyle ?? 'function', {
559
+ interviewGuard: true,
560
+ });
561
+ analyzerInitialized = false;
562
+ self.postMessage({ id, type: 'execute-result', payload: result });
563
+ break;
564
+ }
565
+
566
+ case 'status': {
567
+ self.postMessage({
568
+ id,
569
+ type: 'status-result',
570
+ payload: {
571
+ isReady: pyodide !== null,
572
+ isLoading,
573
+ },
574
+ });
575
+ break;
576
+ }
577
+
578
+ case 'analyze-code': {
579
+ const { code } = payload;
580
+ const result = await analyzeCodeAST(code);
581
+ self.postMessage({ id, type: 'analyze-result', payload: result });
582
+ break;
583
+ }
584
+
585
+ default:
586
+ self.postMessage({
587
+ id,
588
+ type: 'error',
589
+ payload: { error: `Unknown message type: ${type}` },
590
+ });
591
+ }
592
+ } catch (error) {
593
+ self.postMessage({
594
+ id,
595
+ type: 'error',
596
+ payload: { error: error instanceof Error ? error.message : String(error) },
597
+ });
598
+ }
599
+ }
600
+
601
+ let messageQueue = Promise.resolve();
602
+
603
+ // Message handler
604
+ self.onmessage = function(event) {
605
+ const messageData = event.data;
606
+ messageQueue = messageQueue
607
+ .then(() => processMessage(messageData))
608
+ .catch((error) => {
609
+ const { id } = messageData;
610
+ self.postMessage({
611
+ id,
612
+ type: 'error',
613
+ payload: { error: error instanceof Error ? error.message : String(error) },
614
+ });
615
+ });
616
+ };
617
+
618
+ // Notify that worker is ready
619
+ self.postMessage({ type: 'worker-ready' });
620
+
621
+ // ═══════════════════════════════════════════════════════════════════════════
622
+ // AST CODE ANALYSIS
623
+ // ═══════════════════════════════════════════════════════════════════════════
624
+
625
+ /**
626
+ * Whether the AST analyzer has been initialized in Pyodide
627
+ */
628
+ let analyzerInitialized = false;
629
+
630
+ function isAnalyzeNameError(message) {
631
+ if (!message || typeof message !== 'string') return false;
632
+ return /NameError/.test(message) && /name ['"]analyze['"] is not defined/.test(message);
633
+ }
634
+
635
+ /**
636
+ * Initialize the AST analyzer (define the analyze_code function in Pyodide)
637
+ */
638
+ async function initAnalyzer() {
639
+ if (analyzerInitialized) return;
640
+
641
+ await loadPyodideInstance();
642
+
643
+ // The AST analyzer Python code - must match lib/analysis/ast-analyzer.ts
644
+ const analyzerCode = `
645
+ import ast
646
+ import json
647
+ ${PYTHON_CLASS_DEFINITIONS_SNIPPET}
648
+
649
+ TRACKED_BUILTINS = frozenset([
650
+ 'max', 'min', 'len', 'sum', 'abs', 'sorted', 'reversed',
651
+ 'enumerate', 'range', 'zip', 'map', 'filter', 'any', 'all',
652
+ 'int', 'float', 'str', 'bool', 'list', 'dict', 'set', 'tuple',
653
+ 'ord', 'chr', 'print', 'input', 'open', 'type', 'isinstance',
654
+ 'hasattr', 'getattr', 'setattr', 'delattr',
655
+ ])
656
+
657
+ DICT_METHODS = frozenset([
658
+ 'get', 'keys', 'values', 'items', 'pop', 'setdefault',
659
+ 'update', 'clear', 'copy', 'fromkeys',
660
+ ])
661
+
662
+ LIST_METHODS = frozenset([
663
+ 'append', 'pop', 'extend', 'insert', 'remove', 'clear',
664
+ 'index', 'count', 'sort', 'reverse', 'copy',
665
+ ])
666
+
667
+ STRING_METHODS = frozenset([
668
+ 'split', 'join', 'strip', 'lstrip', 'rstrip', 'lower', 'upper',
669
+ 'replace', 'find', 'rfind', 'index', 'rindex', 'count',
670
+ 'startswith', 'endswith', 'isalpha', 'isdigit', 'isalnum',
671
+ 'format', 'encode', 'decode',
672
+ ])
673
+
674
+ HEAP_FUNCS = frozenset([
675
+ 'heappush', 'heappop', 'heapify', 'heappushpop', 'heapreplace',
676
+ 'nlargest', 'nsmallest',
677
+ ])
678
+
679
+ def analyze_code(code):
680
+ facts = {
681
+ 'valid': True,
682
+ 'syntaxError': None,
683
+ 'hasFunctionDef': False,
684
+ 'functionNames': [],
685
+ 'hasForLoop': False,
686
+ 'hasWhileLoop': False,
687
+ 'hasNestedLoop': False,
688
+ 'hasConditional': False,
689
+ 'hasRecursion': False,
690
+ 'usesDict': False,
691
+ 'usesList': False,
692
+ 'usesSet': False,
693
+ 'usesHeap': False,
694
+ 'usesDeque': False,
695
+ 'builtinsUsed': [],
696
+ 'augmentedAssignOps': [],
697
+ 'comparisonOps': [],
698
+ 'dictOps': [],
699
+ 'listOps': [],
700
+ 'stringOps': [],
701
+ 'hasReturn': False,
702
+ 'returnCount': 0,
703
+ 'hasEarlyReturn': False,
704
+ 'indexAccesses': False,
705
+ 'sliceAccesses': False,
706
+ 'slidingWindowPattern': None,
707
+ 'indexExpressions': [],
708
+ 'windowPatterns': [],
709
+ 'variablesAssigned': [],
710
+ 'functionParams': [],
711
+ }
712
+
713
+ try:
714
+ tree = ast.parse(code)
715
+ except SyntaxError as e:
716
+ facts['valid'] = False
717
+ facts['syntaxError'] = f"Line {e.lineno}: {e.msg}" if e.lineno else str(e.msg)
718
+ return facts
719
+
720
+ function_names = set()
721
+ builtins_used = set()
722
+ aug_assign_ops = set()
723
+ comparison_ops = set()
724
+ dict_ops = set()
725
+ list_ops = set()
726
+ string_ops = set()
727
+ variables_assigned = set()
728
+ function_params = set()
729
+
730
+ loop_depth = 0
731
+ in_conditional = False
732
+ deque_imported = False
733
+ current_loop_var = None
734
+ canonical_index_expressions = []
735
+
736
+ AUG_OP_MAP = {
737
+ ast.Add: '+=', ast.Sub: '-=', ast.Mult: '*=', ast.Div: '/=',
738
+ ast.FloorDiv: '//=', ast.Mod: '%=', ast.Pow: '**=',
739
+ ast.BitOr: '|=', ast.BitAnd: '&=', ast.BitXor: '^=',
740
+ ast.LShift: '<<=', ast.RShift: '>>=',
741
+ }
742
+
743
+ CMP_OP_MAP = {
744
+ ast.Lt: '<', ast.LtE: '<=', ast.Gt: '>', ast.GtE: '>=',
745
+ ast.Eq: '==', ast.NotEq: '!=', ast.In: 'in', ast.NotIn: 'not in',
746
+ ast.Is: 'is', ast.IsNot: 'is not',
747
+ }
748
+
749
+ def _merge_coeffs(left_coeffs, right_coeffs):
750
+ merged = dict(left_coeffs)
751
+ for key, value in right_coeffs.items():
752
+ merged[key] = merged.get(key, 0) + value
753
+ if merged[key] == 0:
754
+ del merged[key]
755
+ return merged
756
+
757
+ def _linearize_index_expr(node):
758
+ if isinstance(node, ast.Name):
759
+ return (0, {node.id: 1}, [node.id])
760
+
761
+ if isinstance(node, ast.Constant):
762
+ if isinstance(node.value, bool):
763
+ return None
764
+ if isinstance(node.value, int):
765
+ return (int(node.value), {}, [])
766
+ return None
767
+
768
+ if isinstance(node, ast.UnaryOp):
769
+ child = _linearize_index_expr(node.operand)
770
+ if child is None:
771
+ return None
772
+ child_const, child_coeffs, child_order = child
773
+ if isinstance(node.op, ast.UAdd):
774
+ return (child_const, child_coeffs, child_order)
775
+ if isinstance(node.op, ast.USub):
776
+ neg_coeffs = {key: -value for key, value in child_coeffs.items()}
777
+ return (-child_const, neg_coeffs, child_order)
778
+ return None
779
+
780
+ if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Sub)):
781
+ left = _linearize_index_expr(node.left)
782
+ right = _linearize_index_expr(node.right)
783
+ if left is None or right is None:
784
+ return None
785
+ left_const, left_coeffs, left_order = left
786
+ right_const, right_coeffs, right_order = right
787
+ if isinstance(node.op, ast.Sub):
788
+ right_const = -right_const
789
+ right_coeffs = {key: -value for key, value in right_coeffs.items()}
790
+ const_delta = left_const + right_const
791
+ coeffs = _merge_coeffs(left_coeffs, right_coeffs)
792
+ order = list(left_order)
793
+ for key in right_order:
794
+ if key not in order:
795
+ order.append(key)
796
+ return (const_delta, coeffs, order)
797
+
798
+ return None
799
+
800
+ def _to_canonical_index_expr(array_name, expr_node, preferred_base_var=None):
801
+ linear = _linearize_index_expr(expr_node)
802
+ if linear is None:
803
+ return None
804
+
805
+ const_delta, coeffs, order = linear
806
+ if not coeffs:
807
+ return None
808
+
809
+ mutable_coeffs = dict(coeffs)
810
+ base_var = None
811
+ if preferred_base_var and mutable_coeffs.get(preferred_base_var) == 1:
812
+ base_var = preferred_base_var
813
+ else:
814
+ for var_name in order:
815
+ if mutable_coeffs.get(var_name) == 1:
816
+ base_var = var_name
817
+ break
818
+
819
+ if base_var is None:
820
+ return None
821
+
822
+ mutable_coeffs[base_var] = mutable_coeffs.get(base_var, 0) - 1
823
+ if mutable_coeffs[base_var] == 0:
824
+ del mutable_coeffs[base_var]
825
+
826
+ variable_delta_name = None
827
+ variable_delta_sign = 0
828
+ if len(mutable_coeffs) > 1:
829
+ return None
830
+ if len(mutable_coeffs) == 1:
831
+ variable_delta_name, coeff = next(iter(mutable_coeffs.items()))
832
+ if coeff not in (-1, 1):
833
+ return None
834
+ variable_delta_sign = coeff
835
+
836
+ return {
837
+ 'arrayVar': array_name,
838
+ 'baseVar': base_var,
839
+ 'constantDelta': int(const_delta),
840
+ 'variableDeltaName': variable_delta_name,
841
+ 'variableDeltaSign': variable_delta_sign,
842
+ }
843
+
844
+ def _canonical_expr_key(expr):
845
+ return (
846
+ expr['arrayVar'],
847
+ expr['baseVar'],
848
+ int(expr.get('constantDelta', 0)),
849
+ expr.get('variableDeltaName') or '',
850
+ int(expr.get('variableDeltaSign') or 0),
851
+ )
852
+
853
+ def _is_plain_base_expr(expr):
854
+ return (
855
+ int(expr.get('constantDelta', 0)) == 0
856
+ and int(expr.get('variableDeltaSign') or 0) == 0
857
+ and not expr.get('variableDeltaName')
858
+ )
859
+
860
+ def _build_window_patterns(index_exprs):
861
+ grouped = {}
862
+ for expr in index_exprs:
863
+ group_key = (expr['arrayVar'], expr['baseVar'])
864
+ grouped.setdefault(group_key, []).append(expr)
865
+
866
+ patterns = []
867
+ for (array_var, base_var), expressions in grouped.items():
868
+ unique = []
869
+ seen = set()
870
+ for expr in expressions:
871
+ key = _canonical_expr_key(expr)
872
+ if key in seen:
873
+ continue
874
+ seen.add(key)
875
+ unique.append(expr)
876
+
877
+ if len(unique) < 2:
878
+ continue
879
+
880
+ plain = next((expr for expr in unique if _is_plain_base_expr(expr)), None)
881
+ if plain is not None:
882
+ shifted = next(
883
+ (
884
+ expr for expr in unique
885
+ if _canonical_expr_key(expr) != _canonical_expr_key(plain)
886
+ and (
887
+ int(expr.get('constantDelta', 0)) != 0
888
+ or int(expr.get('variableDeltaSign') or 0) != 0
889
+ )
890
+ ),
891
+ None
892
+ )
893
+ if shifted is not None:
894
+ patterns.append({
895
+ 'arrayVar': array_var,
896
+ 'baseVar': base_var,
897
+ 'leftExpr': shifted,
898
+ 'rightExpr': plain,
899
+ })
900
+ continue
901
+
902
+ patterns.append({
903
+ 'arrayVar': array_var,
904
+ 'baseVar': base_var,
905
+ 'leftExpr': unique[0],
906
+ 'rightExpr': unique[1],
907
+ })
908
+
909
+ return patterns
910
+
911
+ def _project_legacy_sliding_window(window_patterns):
912
+ for pattern in window_patterns:
913
+ left = pattern.get('leftExpr') or {}
914
+ right = pattern.get('rightExpr') or {}
915
+ plain = None
916
+ shifted = None
917
+ if _is_plain_base_expr(left):
918
+ plain = left
919
+ shifted = right
920
+ elif _is_plain_base_expr(right):
921
+ plain = right
922
+ shifted = left
923
+ else:
924
+ continue
925
+
926
+ offset_name = shifted.get('variableDeltaName')
927
+ offset_sign = int(shifted.get('variableDeltaSign') or 0)
928
+ offset_constant = int(shifted.get('constantDelta', 0))
929
+
930
+ if offset_name and offset_sign in (-1, 1) and offset_constant == 0:
931
+ return {
932
+ 'loopVar': plain['baseVar'],
933
+ 'offsetVar': offset_name,
934
+ 'arrayVar': pattern['arrayVar'],
935
+ 'offsetDirection': 'subtract' if offset_sign < 0 else 'add',
936
+ }
937
+
938
+ if not offset_name and offset_constant != 0:
939
+ return {
940
+ 'loopVar': plain['baseVar'],
941
+ 'offsetVar': str(abs(offset_constant)),
942
+ 'arrayVar': pattern['arrayVar'],
943
+ 'offsetDirection': 'subtract' if offset_constant < 0 else 'add',
944
+ }
945
+
946
+ return None
947
+
948
+ class FactExtractor(ast.NodeVisitor):
949
+ def visit_FunctionDef(self, node):
950
+ nonlocal function_names, function_params
951
+ facts['hasFunctionDef'] = True
952
+ function_names.add(node.name)
953
+ for arg in node.args.args:
954
+ function_params.add(arg.arg)
955
+ self.generic_visit(node)
956
+
957
+ def visit_AsyncFunctionDef(self, node):
958
+ self.visit_FunctionDef(node)
959
+
960
+ def visit_For(self, node):
961
+ nonlocal loop_depth, current_loop_var
962
+ facts['hasForLoop'] = True
963
+ loop_depth += 1
964
+ if loop_depth > 1:
965
+ facts['hasNestedLoop'] = True
966
+ old_loop_var = current_loop_var
967
+ if isinstance(node.target, ast.Name):
968
+ current_loop_var = node.target.id
969
+ self.generic_visit(node)
970
+ current_loop_var = old_loop_var
971
+ loop_depth -= 1
972
+
973
+ def visit_While(self, node):
974
+ nonlocal loop_depth
975
+ facts['hasWhileLoop'] = True
976
+ loop_depth += 1
977
+ if loop_depth > 1:
978
+ facts['hasNestedLoop'] = True
979
+ self.generic_visit(node)
980
+ loop_depth -= 1
981
+
982
+ def visit_If(self, node):
983
+ nonlocal in_conditional
984
+ facts['hasConditional'] = True
985
+ was_in_conditional = in_conditional
986
+ in_conditional = True
987
+ self.generic_visit(node)
988
+ in_conditional = was_in_conditional
989
+
990
+ def visit_Call(self, node):
991
+ nonlocal builtins_used, dict_ops, list_ops, string_ops
992
+ if isinstance(node.func, ast.Name):
993
+ name = node.func.id
994
+ if name in TRACKED_BUILTINS:
995
+ builtins_used.add(name)
996
+ if name == 'dict':
997
+ facts['usesDict'] = True
998
+ elif name == 'list':
999
+ facts['usesList'] = True
1000
+ elif name == 'set':
1001
+ facts['usesSet'] = True
1002
+ if name in HEAP_FUNCS:
1003
+ facts['usesHeap'] = True
1004
+ if name in function_names:
1005
+ facts['hasRecursion'] = True
1006
+ elif isinstance(node.func, ast.Attribute):
1007
+ method = node.func.attr
1008
+ if method in DICT_METHODS:
1009
+ dict_ops.add(method)
1010
+ if method in LIST_METHODS:
1011
+ list_ops.add(method)
1012
+ if method in STRING_METHODS:
1013
+ string_ops.add(method)
1014
+ if method in ('appendleft', 'popleft'):
1015
+ facts['usesDeque'] = True
1016
+ elif method in ('append', 'pop') and deque_imported:
1017
+ facts['usesDeque'] = True
1018
+ if method in HEAP_FUNCS:
1019
+ facts['usesHeap'] = True
1020
+ self.generic_visit(node)
1021
+
1022
+ def visit_Dict(self, node):
1023
+ facts['usesDict'] = True
1024
+ self.generic_visit(node)
1025
+
1026
+ def visit_List(self, node):
1027
+ facts['usesList'] = True
1028
+ self.generic_visit(node)
1029
+
1030
+ def visit_Set(self, node):
1031
+ facts['usesSet'] = True
1032
+ self.generic_visit(node)
1033
+
1034
+ def visit_ListComp(self, node):
1035
+ facts['usesList'] = True
1036
+ self.generic_visit(node)
1037
+
1038
+ def visit_DictComp(self, node):
1039
+ facts['usesDict'] = True
1040
+ self.generic_visit(node)
1041
+
1042
+ def visit_SetComp(self, node):
1043
+ facts['usesSet'] = True
1044
+ self.generic_visit(node)
1045
+
1046
+ def visit_AugAssign(self, node):
1047
+ nonlocal aug_assign_ops
1048
+ op_type = type(node.op)
1049
+ if op_type in AUG_OP_MAP:
1050
+ aug_assign_ops.add(AUG_OP_MAP[op_type])
1051
+ self.generic_visit(node)
1052
+
1053
+ def visit_Compare(self, node):
1054
+ nonlocal comparison_ops, dict_ops
1055
+ for op in node.ops:
1056
+ op_type = type(op)
1057
+ if op_type in CMP_OP_MAP:
1058
+ op_str = CMP_OP_MAP[op_type]
1059
+ comparison_ops.add(op_str)
1060
+ if op_str == 'in' or op_str == 'not in':
1061
+ dict_ops.add(op_str)
1062
+ self.generic_visit(node)
1063
+
1064
+ def visit_Assign(self, node):
1065
+ nonlocal variables_assigned
1066
+ for target in node.targets:
1067
+ if isinstance(target, ast.Name):
1068
+ variables_assigned.add(target.id)
1069
+ elif isinstance(target, (ast.Tuple, ast.List)):
1070
+ for elt in target.elts:
1071
+ if isinstance(elt, ast.Name):
1072
+ variables_assigned.add(elt.id)
1073
+ self.generic_visit(node)
1074
+
1075
+ def visit_AnnAssign(self, node):
1076
+ nonlocal variables_assigned
1077
+ if isinstance(node.target, ast.Name):
1078
+ variables_assigned.add(node.target.id)
1079
+ self.generic_visit(node)
1080
+
1081
+ def visit_Subscript(self, node):
1082
+ nonlocal canonical_index_expressions, current_loop_var
1083
+ if isinstance(node.slice, ast.Slice):
1084
+ facts['sliceAccesses'] = True
1085
+ else:
1086
+ facts['indexAccesses'] = True
1087
+ if isinstance(node.value, ast.Name):
1088
+ array_name = node.value.id
1089
+ canonical_expr = _to_canonical_index_expr(
1090
+ array_name,
1091
+ node.slice,
1092
+ current_loop_var
1093
+ )
1094
+ if canonical_expr:
1095
+ canonical_index_expressions.append(canonical_expr)
1096
+ self.generic_visit(node)
1097
+
1098
+ def visit_Return(self, node):
1099
+ nonlocal in_conditional
1100
+ facts['hasReturn'] = True
1101
+ facts['returnCount'] += 1
1102
+ if in_conditional:
1103
+ facts['hasEarlyReturn'] = True
1104
+ self.generic_visit(node)
1105
+
1106
+ def visit_Import(self, node):
1107
+ for alias in node.names:
1108
+ if alias.name == 'heapq':
1109
+ facts['usesHeap'] = True
1110
+ self.generic_visit(node)
1111
+
1112
+ def visit_ImportFrom(self, node):
1113
+ nonlocal deque_imported
1114
+ if node.module == 'heapq':
1115
+ facts['usesHeap'] = True
1116
+ elif node.module == 'collections':
1117
+ for alias in node.names:
1118
+ if alias.name == 'deque':
1119
+ facts['usesDeque'] = True
1120
+ deque_imported = True
1121
+ self.generic_visit(node)
1122
+
1123
+ extractor = FactExtractor()
1124
+ extractor.visit(tree)
1125
+
1126
+ facts['functionNames'] = sorted(function_names)
1127
+ facts['builtinsUsed'] = sorted(builtins_used)
1128
+ facts['augmentedAssignOps'] = sorted(aug_assign_ops)
1129
+ facts['comparisonOps'] = sorted(comparison_ops)
1130
+ facts['dictOps'] = sorted(dict_ops)
1131
+ facts['listOps'] = sorted(list_ops)
1132
+ facts['stringOps'] = sorted(string_ops)
1133
+ facts['variablesAssigned'] = sorted(variables_assigned)
1134
+ facts['functionParams'] = sorted(function_params)
1135
+
1136
+ deduped_index_exprs = []
1137
+ seen_expr_keys = set()
1138
+ for expr in canonical_index_expressions:
1139
+ key = _canonical_expr_key(expr)
1140
+ if key in seen_expr_keys:
1141
+ continue
1142
+ seen_expr_keys.add(key)
1143
+ deduped_index_exprs.append(expr)
1144
+
1145
+ facts['indexExpressions'] = deduped_index_exprs
1146
+ facts['windowPatterns'] = _build_window_patterns(deduped_index_exprs)
1147
+ facts['slidingWindowPattern'] = _project_legacy_sliding_window(facts['windowPatterns'])
1148
+
1149
+ return facts
1150
+
1151
+ def analyze(code):
1152
+ return json.dumps(analyze_code(code))
1153
+ `;
1154
+
1155
+ // The analyzer code defines `analyze()` in Pyodide's default globals.
1156
+ // We mark initialization done so we don't redefine each time.
1157
+ await pyodide.runPythonAsync(analyzerCode);
1158
+ analyzerInitialized = true;
1159
+ }
1160
+
1161
+ /**
1162
+ * Analyze Python code using the AST analyzer
1163
+ */
1164
+ async function analyzeCodeAST(code) {
1165
+ // Escape the code for embedding in a Python string
1166
+ const escaped = code
1167
+ .replace(/\\/g, '\\\\')
1168
+ .replace(/'/g, "\\'")
1169
+ .replace(/\n/g, '\\n')
1170
+ .replace(/\r/g, '\\r')
1171
+ .replace(/\t/g, '\\t');
1172
+
1173
+ const analyzeCall = `analyze('${escaped}')`;
1174
+ let lastError = null;
1175
+
1176
+ for (let attempt = 0; attempt < 2; attempt++) {
1177
+ try {
1178
+ await initAnalyzer();
1179
+ const resultJson = await pyodide.runPythonAsync(analyzeCall);
1180
+ return JSON.parse(resultJson);
1181
+ } catch (error) {
1182
+ const message = error instanceof Error ? error.message : String(error);
1183
+ lastError = error;
1184
+
1185
+ if (!isAnalyzeNameError(message) || attempt === 1) {
1186
+ throw error;
1187
+ }
1188
+
1189
+ if (WORKER_DEBUG) {
1190
+ console.warn('[Pyodide Worker] analyze() missing; reinitializing AST analyzer');
1191
+ }
1192
+ analyzerInitialized = false;
1193
+ }
1194
+ }
1195
+
1196
+ throw lastError || new Error('AST analysis failed');
1197
+ }