@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.
- package/CHANGELOG.md +113 -0
- package/LICENSE +674 -0
- package/README.md +266 -0
- package/dist/browser.cjs +1352 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +49 -0
- package/dist/browser.d.ts +49 -0
- package/dist/browser.js +1317 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.cjs +70 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +70 -0
- package/dist/cli.js.map +1 -0
- package/dist/core.cjs +286 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +69 -0
- package/dist/core.d.ts +69 -0
- package/dist/core.js +254 -0
- package/dist/core.js.map +1 -0
- package/dist/index.cjs +2603 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2538 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/browser.cjs +647 -0
- package/dist/internal/browser.cjs.map +1 -0
- package/dist/internal/browser.d.cts +143 -0
- package/dist/internal/browser.d.ts +143 -0
- package/dist/internal/browser.js +617 -0
- package/dist/internal/browser.js.map +1 -0
- package/dist/javascript.cjs +549 -0
- package/dist/javascript.cjs.map +1 -0
- package/dist/javascript.d.cts +11 -0
- package/dist/javascript.d.ts +11 -0
- package/dist/javascript.js +518 -0
- package/dist/javascript.js.map +1 -0
- package/dist/python.cjs +744 -0
- package/dist/python.cjs.map +1 -0
- package/dist/python.d.cts +97 -0
- package/dist/python.d.ts +97 -0
- package/dist/python.js +698 -0
- package/dist/python.js.map +1 -0
- package/dist/runtime-types-C7d1LFbx.d.ts +85 -0
- package/dist/runtime-types-Dvgn07z9.d.cts +85 -0
- package/dist/types-Bzr1Ohcf.d.cts +96 -0
- package/dist/types-Bzr1Ohcf.d.ts +96 -0
- package/package.json +89 -0
- package/workers/javascript/javascript-worker.js +2918 -0
- package/workers/python/generated-python-harness-snippets.js +20 -0
- package/workers/python/pyodide-worker.js +1197 -0
- package/workers/python/runtime-core.js +1529 -0
- 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
|
+
}
|