@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,1529 @@
1
+ /**
2
+ * Pyodide runtime core helpers loaded by pyodide-worker.js.
3
+ *
4
+ * Exposes runtime helpers behind a dependency-injected surface so
5
+ * the top-level worker can stay focused on loading + message dispatch.
6
+ */
7
+
8
+ (function initPyodideRuntimeCore(globalScope) {
9
+ function generateTracingCode(deps, userCode, functionName, inputs, executionStyle = 'function', options = {}) {
10
+ const inputSetup = Object.entries(inputs)
11
+ .map(([key, value]) => `${key} = ${deps.toPythonLiteral(value)}`)
12
+ .join('\n');
13
+
14
+ const escapedCode = userCode.replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"');
15
+ const targetFunction = functionName || '';
16
+
17
+ // Configurable limits
18
+ const maxTraceSteps = options.maxTraceSteps || 2000;
19
+ const maxLineEvents = options.maxLineEvents || 10000;
20
+ const maxSingleLineHits = options.maxSingleLineHits || 500;
21
+ const minimalTrace = options.minimalTrace === true;
22
+ // Keep stdout capture deterministic for the app UI; worker-console mirroring
23
+ // can cause recursive print chains across mixed runs in dev.
24
+ const mirrorPrintToConsole = false;
25
+
26
+ // Python harness code - all at column 0, using 4-space indentation
27
+ const harnessPrefix = `
28
+ import sys
29
+ import json
30
+ import math
31
+ import ast
32
+ import builtins as _builtins
33
+ ${deps.PYTHON_CLASS_DEFINITIONS_SNIPPET}
34
+
35
+ _trace_data = []
36
+ _console_output = []
37
+ _original_print = _builtins.print
38
+ _target_function = "${targetFunction}"
39
+ _MIRROR_PRINT_TO_WORKER_CONSOLE = ${mirrorPrintToConsole ? 'True' : 'False'}
40
+ _MINIMAL_TRACE = ${minimalTrace ? 'True' : 'False'}
41
+
42
+ class _InfiniteLoopDetected(Exception):
43
+ pass
44
+
45
+ def _custom_print(*args, **kwargs):
46
+ output = " ".join(str(arg) for arg in args)
47
+ _console_output.append(output)
48
+ try:
49
+ _frame = sys._getframe(1)
50
+ _trace_data.append({
51
+ 'line': _frame.f_lineno,
52
+ 'event': 'stdout',
53
+ 'variables': {'output': output},
54
+ 'function': _frame.f_code.co_name,
55
+ 'callStack': [] if _MINIMAL_TRACE else [f.copy() for f in _call_stack],
56
+ 'stdoutLineCount': len(_console_output)
57
+ })
58
+ except Exception:
59
+ pass
60
+ # Do not mirror to worker console; app UI owns stdout rendering.
61
+
62
+ print = _custom_print
63
+
64
+ ${deps.PYTHON_TRACE_SERIALIZE_FUNCTION_SNIPPET}
65
+
66
+ _call_stack = []
67
+ _pending_accesses = {}
68
+ _prev_hashmap_snapshots = {}
69
+ _TRACE_MUTATING_METHODS = {'append', 'appendleft', 'pop', 'popleft', 'extend', 'insert'}
70
+ _internal_funcs = {'_serialize', '_tracer', '_custom_print', '_dict_to_tree', '_dict_to_list', '_is_structural_constructor_frame', '_snapshot_call_stack', '_snapshot_locals', '_stable_token', '_looks_like_adjacency_list', '_looks_like_indexed_adjacency_list', '_extract_hashmap_snapshot', '_classify_runtime_object_kind', '_infer_hashmap_delta', '_clear_frame_hashmap_snapshots', '_build_runtime_visualization', '_resolve_inplace_result', '__tracecode_record_access', '__tracecode_flush_accesses', '__tracecode_normalize_indices', '__tracecode_make_access_event', '__tracecode_read_value', '__tracecode_write_value', '__tracecode_apply_augmented_value', '_tracecode_read_index', '_tracecode_write_index', '_tracecode_augassign_index', '_tracecode_mutating_call', '__tracecode_attach_parents', '_tracecode_extract_named_subscript', '__TracecodeAccessTransformer', '__tracecode_compile_user_code', '<listcomp>', '<dictcomp>', '<setcomp>', '<genexpr>'}
71
+ _internal_locals = {
72
+ '_trace_data', '_console_output', '_original_print', '_target_function',
73
+ '_MIRROR_PRINT_TO_WORKER_CONSOLE', '_MINIMAL_TRACE', '_SKIP_SENTINEL',
74
+ '_call_stack', '_pending_accesses', '_prev_hashmap_snapshots', '_TRACE_MUTATING_METHODS', '_internal_funcs', '_internal_locals', '_max_trace_steps',
75
+ '_trace_limit_exceeded', '_timeout_reason', '_total_line_events', '_max_line_events',
76
+ '_line_hit_count', '_max_single_line_hits', '_infinite_loop_line',
77
+ '_MAX_SERIALIZE_DEPTH', '_trace_failed', '_inplace',
78
+ '_custom_print', '_tracer', '_serialize', '_dict_to_tree', '_dict_to_list',
79
+ '_is_structural_constructor_frame', '_snapshot_call_stack', '_snapshot_locals', '_stable_token',
80
+ '_looks_like_adjacency_list', '_looks_like_indexed_adjacency_list', '_extract_hashmap_snapshot', '_classify_runtime_object_kind', '_infer_hashmap_delta',
81
+ '_clear_frame_hashmap_snapshots', '_build_runtime_visualization', '_resolve_inplace_result',
82
+ '__tracecode_record_access', '__tracecode_flush_accesses', '__tracecode_normalize_indices',
83
+ '__tracecode_make_access_event', '__tracecode_read_value', '__tracecode_write_value',
84
+ '__tracecode_apply_augmented_value', '_tracecode_read_index', '_tracecode_write_index',
85
+ '_tracecode_augassign_index', '_tracecode_mutating_call', '__tracecode_attach_parents',
86
+ '_tracecode_extract_named_subscript', '__TracecodeAccessTransformer', '__tracecode_compile_user_code',
87
+ '_InfiniteLoopDetected', '_tb', '_result', '_exc_type', '_exc_msg', '_exc_tb',
88
+ '_error_line', '_solver', '_ops', '_args', '_cls', '_instance', '_out',
89
+ '_i', '_op', '_call_args', '_method', '_user_code_str', '_textwrap',
90
+ '_globals_dict', '_k', '_preserve', '_real_globals', '_real_list',
91
+ '__tracecode_tree', '__tracecode_compiled'
92
+ }
93
+ _max_trace_steps = ${maxTraceSteps}
94
+ _trace_limit_exceeded = False
95
+ _timeout_reason = None
96
+ _total_line_events = 0
97
+ _max_line_events = ${maxLineEvents}
98
+ _line_hit_count = {}
99
+ _max_single_line_hits = ${maxSingleLineHits}
100
+ _infinite_loop_line = -1
101
+
102
+ def _is_structural_constructor_frame(frame):
103
+ if frame.f_code.co_name != '__init__':
104
+ return False
105
+ try:
106
+ arg_count = frame.f_code.co_argcount
107
+ arg_names = frame.f_code.co_varnames[:arg_count]
108
+ # Detect node constructors by signature so we can skip at call-time
109
+ # before self.left/self.right/self.next are initialized.
110
+ if arg_names and arg_names[0] == 'self':
111
+ has_val_param = ('val' in arg_names) or ('value' in arg_names)
112
+ has_tree_param = ('left' in arg_names) or ('right' in arg_names)
113
+ has_list_param = ('next' in arg_names) or ('prev' in arg_names)
114
+ if has_val_param and (has_tree_param or has_list_param):
115
+ return True
116
+ except Exception:
117
+ pass
118
+ try:
119
+ self_obj = frame.f_locals.get('self')
120
+ except Exception:
121
+ return False
122
+ if self_obj is None:
123
+ return False
124
+ try:
125
+ has_val_like = hasattr(self_obj, 'val') or hasattr(self_obj, 'value')
126
+ has_tree_links = hasattr(self_obj, 'left') or hasattr(self_obj, 'right')
127
+ has_list_links = hasattr(self_obj, 'next') or hasattr(self_obj, 'prev')
128
+ return has_val_like and (has_tree_links or has_list_links)
129
+ except Exception:
130
+ return False
131
+
132
+ def _snapshot_call_stack():
133
+ if _MINIMAL_TRACE:
134
+ return []
135
+ return [f.copy() for f in _call_stack]
136
+
137
+ def _snapshot_locals(frame):
138
+ if _MINIMAL_TRACE:
139
+ return {}
140
+ try:
141
+ _node_refs = {}
142
+ return {
143
+ k: v
144
+ for k, v in ((k, _serialize(v, 0, _node_refs)) for k, v in frame.f_locals.items() if k not in _internal_locals and k != '_' and not k.startswith('__'))
145
+ if v != _SKIP_SENTINEL
146
+ }
147
+ except Exception:
148
+ return {}
149
+
150
+ def __tracecode_record_access(frame, event):
151
+ if frame is None or not isinstance(event, dict):
152
+ return
153
+ frame_key = id(frame)
154
+ _pending_accesses.setdefault(frame_key, []).append(event)
155
+
156
+ def __tracecode_flush_accesses(frame):
157
+ if frame is None:
158
+ return []
159
+ return _pending_accesses.pop(id(frame), [])
160
+
161
+ def __tracecode_normalize_indices(indices, max_depth=2):
162
+ if not isinstance(indices, (list, tuple)) or len(indices) == 0 or len(indices) > max_depth:
163
+ return None
164
+ normalized = []
165
+ for index in indices:
166
+ if not isinstance(index, int):
167
+ return None
168
+ normalized.append(int(index))
169
+ return normalized
170
+
171
+ def __tracecode_make_access_event(var_name, kind, indices=None, method_name=None):
172
+ event = {
173
+ 'variable': var_name,
174
+ 'kind': kind,
175
+ }
176
+ if indices is not None:
177
+ event['indices'] = list(indices)
178
+ event['pathDepth'] = len(indices)
179
+ if method_name is not None:
180
+ event['method'] = method_name
181
+ return event
182
+
183
+ def __tracecode_read_value(container, indices):
184
+ current = container
185
+ for index in indices:
186
+ current = current[index]
187
+ return current
188
+
189
+ def __tracecode_write_value(container, indices, value):
190
+ if len(indices) == 1:
191
+ container[indices[0]] = value
192
+ return value
193
+ parent = container
194
+ for index in indices[:-1]:
195
+ parent = parent[index]
196
+ parent[indices[-1]] = value
197
+ return value
198
+
199
+ def __tracecode_apply_augmented_value(current, op_name, rhs):
200
+ if op_name == 'add':
201
+ return current + rhs
202
+ if op_name == 'sub':
203
+ return current - rhs
204
+ if op_name == 'mul':
205
+ return current * rhs
206
+ if op_name == 'div':
207
+ return current / rhs
208
+ if op_name == 'floordiv':
209
+ return current // rhs
210
+ if op_name == 'mod':
211
+ return current % rhs
212
+ if op_name == 'pow':
213
+ return current ** rhs
214
+ if op_name == 'lshift':
215
+ return current << rhs
216
+ if op_name == 'rshift':
217
+ return current >> rhs
218
+ if op_name == 'bitand':
219
+ return current & rhs
220
+ if op_name == 'bitor':
221
+ return current | rhs
222
+ if op_name == 'bitxor':
223
+ return current ^ rhs
224
+ return rhs
225
+
226
+ def _tracecode_read_index(var_name, container, indices):
227
+ normalized = __tracecode_normalize_indices(indices)
228
+ if normalized is not None:
229
+ __tracecode_record_access(
230
+ sys._getframe(1),
231
+ __tracecode_make_access_event(
232
+ var_name,
233
+ 'cell-read' if len(normalized) == 2 else 'indexed-read',
234
+ normalized,
235
+ ),
236
+ )
237
+ return __tracecode_read_value(container, list(indices))
238
+
239
+ def _tracecode_write_index(var_name, container, indices, value):
240
+ effective_indices = list(indices)
241
+ result = __tracecode_write_value(container, effective_indices, value)
242
+ normalized = __tracecode_normalize_indices(effective_indices)
243
+ if normalized is not None:
244
+ __tracecode_record_access(
245
+ sys._getframe(1),
246
+ __tracecode_make_access_event(
247
+ var_name,
248
+ 'cell-write' if len(normalized) == 2 else 'indexed-write',
249
+ normalized,
250
+ ),
251
+ )
252
+ return result
253
+
254
+ def _tracecode_augassign_index(var_name, container, indices, op_name, rhs):
255
+ effective_indices = list(indices)
256
+ current = __tracecode_read_value(container, effective_indices)
257
+ normalized = __tracecode_normalize_indices(effective_indices)
258
+ if normalized is not None:
259
+ __tracecode_record_access(
260
+ sys._getframe(1),
261
+ __tracecode_make_access_event(
262
+ var_name,
263
+ 'cell-read' if len(normalized) == 2 else 'indexed-read',
264
+ normalized,
265
+ ),
266
+ )
267
+ next_value = __tracecode_apply_augmented_value(current, op_name, rhs)
268
+ __tracecode_write_value(container, effective_indices, next_value)
269
+ if normalized is not None:
270
+ __tracecode_record_access(
271
+ sys._getframe(1),
272
+ __tracecode_make_access_event(
273
+ var_name,
274
+ 'cell-write' if len(normalized) == 2 else 'indexed-write',
275
+ normalized,
276
+ ),
277
+ )
278
+ return next_value
279
+
280
+ def _tracecode_mutating_call(var_name, container, method_name, *args, **kwargs):
281
+ result = getattr(container, method_name)(*args, **kwargs)
282
+ if method_name in _TRACE_MUTATING_METHODS:
283
+ __tracecode_record_access(
284
+ sys._getframe(1),
285
+ __tracecode_make_access_event(var_name, 'mutating-call', method_name=method_name),
286
+ )
287
+ return result
288
+
289
+ def __tracecode_attach_parents(node, parent=None):
290
+ for child in ast.iter_child_nodes(node):
291
+ setattr(child, '__trace_parent__', node)
292
+ __tracecode_attach_parents(child, node)
293
+
294
+ def _tracecode_extract_named_subscript(node):
295
+ indices = []
296
+ current = node
297
+ while isinstance(current, ast.Subscript) and len(indices) < 3:
298
+ indices.insert(0, current.slice)
299
+ current = current.value
300
+ if not isinstance(current, ast.Name) or len(indices) == 0 or len(indices) > 2:
301
+ return None
302
+ return current.id, indices
303
+
304
+ class __TracecodeAccessTransformer(ast.NodeTransformer):
305
+ def visit_Subscript(self, node):
306
+ parent = getattr(node, '__trace_parent__', None)
307
+ if isinstance(parent, ast.Subscript) and getattr(parent, 'value', None) is node:
308
+ return self.generic_visit(node)
309
+ if isinstance(parent, ast.Assign) and node in getattr(parent, 'targets', []):
310
+ return self.generic_visit(node)
311
+ if isinstance(parent, ast.AugAssign) and getattr(parent, 'target', None) is node:
312
+ return self.generic_visit(node)
313
+
314
+ node = self.generic_visit(node)
315
+ extracted = _tracecode_extract_named_subscript(node)
316
+ if extracted is None or not isinstance(node.ctx, ast.Load):
317
+ return node
318
+
319
+ var_name, indices = extracted
320
+ call = ast.Call(
321
+ func=ast.Name(id='_tracecode_read_index', ctx=ast.Load()),
322
+ args=[
323
+ ast.Constant(value=var_name),
324
+ ast.Name(id=var_name, ctx=ast.Load()),
325
+ ast.List(elts=indices, ctx=ast.Load()),
326
+ ],
327
+ keywords=[],
328
+ )
329
+ return ast.copy_location(call, node)
330
+
331
+ def visit_Assign(self, node):
332
+ if len(node.targets) == 1:
333
+ extracted = _tracecode_extract_named_subscript(node.targets[0])
334
+ if extracted is not None:
335
+ var_name, indices = extracted
336
+ value = self.visit(node.value)
337
+ call = ast.Call(
338
+ func=ast.Name(id='_tracecode_write_index', ctx=ast.Load()),
339
+ args=[
340
+ ast.Constant(value=var_name),
341
+ ast.Name(id=var_name, ctx=ast.Load()),
342
+ ast.List(elts=[self.visit(index) for index in indices], ctx=ast.Load()),
343
+ value,
344
+ ],
345
+ keywords=[],
346
+ )
347
+ return ast.copy_location(ast.Expr(value=call), node)
348
+ return self.generic_visit(node)
349
+
350
+ def visit_AugAssign(self, node):
351
+ extracted = _tracecode_extract_named_subscript(node.target)
352
+ if extracted is None:
353
+ return self.generic_visit(node)
354
+
355
+ op_names = {
356
+ ast.Add: 'add',
357
+ ast.Sub: 'sub',
358
+ ast.Mult: 'mul',
359
+ ast.Div: 'div',
360
+ ast.FloorDiv: 'floordiv',
361
+ ast.Mod: 'mod',
362
+ ast.Pow: 'pow',
363
+ ast.LShift: 'lshift',
364
+ ast.RShift: 'rshift',
365
+ ast.BitAnd: 'bitand',
366
+ ast.BitOr: 'bitor',
367
+ ast.BitXor: 'bitxor',
368
+ }
369
+ op_name = op_names.get(type(node.op))
370
+ if op_name is None:
371
+ return self.generic_visit(node)
372
+
373
+ var_name, indices = extracted
374
+ rhs = self.visit(node.value)
375
+ call = ast.Call(
376
+ func=ast.Name(id='_tracecode_augassign_index', ctx=ast.Load()),
377
+ args=[
378
+ ast.Constant(value=var_name),
379
+ ast.Name(id=var_name, ctx=ast.Load()),
380
+ ast.List(elts=[self.visit(index) for index in indices], ctx=ast.Load()),
381
+ ast.Constant(value=op_name),
382
+ rhs,
383
+ ],
384
+ keywords=[],
385
+ )
386
+ return ast.copy_location(ast.Expr(value=call), node)
387
+
388
+ def visit_Call(self, node):
389
+ node = self.generic_visit(node)
390
+ if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
391
+ method_name = node.func.attr
392
+ if method_name in _TRACE_MUTATING_METHODS:
393
+ call = ast.Call(
394
+ func=ast.Name(id='_tracecode_mutating_call', ctx=ast.Load()),
395
+ args=[
396
+ ast.Constant(value=node.func.value.id),
397
+ ast.Name(id=node.func.value.id, ctx=ast.Load()),
398
+ ast.Constant(value=method_name),
399
+ *node.args,
400
+ ],
401
+ keywords=node.keywords,
402
+ )
403
+ return ast.copy_location(call, node)
404
+ return node
405
+
406
+ def __tracecode_compile_user_code(source):
407
+ tree = ast.parse(source, filename='<user_code>', mode='exec')
408
+ __tracecode_attach_parents(tree)
409
+ tree = __TracecodeAccessTransformer().visit(tree)
410
+ ast.fix_missing_locations(tree)
411
+ return compile(tree, '<user_code>', 'exec')
412
+
413
+ def _stable_token(value):
414
+ try:
415
+ return json.dumps(value, sort_keys=True)
416
+ except Exception:
417
+ return repr(value)
418
+
419
+ def _looks_like_adjacency_list(value):
420
+ if not isinstance(value, dict) or len(value) == 0:
421
+ return False
422
+ if not all(isinstance(v, list) for v in value.values()):
423
+ return False
424
+ key_set = {str(k) for k in value.keys()}
425
+ has_valid_neighbor = False
426
+ for neighbors in value.values():
427
+ for neighbor in neighbors:
428
+ if isinstance(neighbor, (str, int, float)) and str(neighbor) in key_set:
429
+ has_valid_neighbor = True
430
+ break
431
+ if has_valid_neighbor:
432
+ break
433
+ return has_valid_neighbor
434
+
435
+ def _looks_like_indexed_adjacency_list(value):
436
+ if not isinstance(value, list) or len(value) == 0:
437
+ return False
438
+ if not all(isinstance(row, list) for row in value):
439
+ return False
440
+
441
+ node_count = len(value)
442
+ edge_count = 0
443
+ for neighbors in value:
444
+ for neighbor in neighbors:
445
+ if not isinstance(neighbor, int):
446
+ return False
447
+ if neighbor < 0 or neighbor >= node_count:
448
+ return False
449
+ edge_count += 1
450
+
451
+ if edge_count == 0:
452
+ return False
453
+
454
+ looks_like_adjacency_matrix = all(
455
+ len(row) == node_count and all(cell in (0, 1) for cell in row)
456
+ for row in value
457
+ )
458
+ if looks_like_adjacency_matrix:
459
+ return False
460
+
461
+ return True
462
+
463
+ def _extract_hashmap_snapshot(value):
464
+ if not isinstance(value, dict):
465
+ return None
466
+
467
+ value_type = value.get('__type__')
468
+
469
+ if value_type == 'set' and isinstance(value.get('values'), list):
470
+ _values = value.get('values') or []
471
+ return {
472
+ 'kind': 'set',
473
+ 'entries': [{'key': item, 'value': True} for item in _values],
474
+ 'setValues': {_stable_token(item): item for item in _values},
475
+ }
476
+
477
+ if value_type in ('TreeNode', 'ListNode'):
478
+ return None
479
+
480
+ if value_type == 'map' and isinstance(value.get('entries'), list):
481
+ _entries = []
482
+ _map_values = {}
483
+ for entry in value.get('entries') or []:
484
+ if isinstance(entry, (list, tuple)) and len(entry) >= 2:
485
+ _key = entry[0]
486
+ _value = entry[1]
487
+ _entries.append({'key': _key, 'value': _value})
488
+ _map_values[str(_key)] = _value
489
+ return {
490
+ 'kind': 'map',
491
+ 'entries': _entries,
492
+ 'mapValues': _map_values,
493
+ }
494
+
495
+ if value_type in ('map',):
496
+ return None
497
+
498
+ if '__ref__' in value and len(value) == 1:
499
+ return None
500
+
501
+ if _looks_like_adjacency_list(value):
502
+ return None
503
+
504
+ return {
505
+ 'kind': 'hashmap',
506
+ 'entries': [{'key': key, 'value': val} for key, val in value.items()],
507
+ 'mapValues': {str(key): val for key, val in value.items()},
508
+ }
509
+
510
+ def _classify_runtime_object_kind(value):
511
+ if isinstance(value, list):
512
+ if _looks_like_indexed_adjacency_list(value):
513
+ return 'graph-adjacency'
514
+ return None
515
+
516
+ if not isinstance(value, dict):
517
+ return None
518
+
519
+ value_type = value.get('__type__')
520
+ if value_type == 'set' and isinstance(value.get('values'), list):
521
+ return 'set'
522
+ if value_type == 'map' and isinstance(value.get('entries'), list):
523
+ return 'map'
524
+ if value_type == 'TreeNode':
525
+ return 'tree'
526
+ if value_type == 'ListNode':
527
+ return 'linked-list'
528
+ if '__ref__' in value and len(value) == 1:
529
+ return None
530
+ if _looks_like_adjacency_list(value):
531
+ return 'graph-adjacency'
532
+ return 'hashmap'
533
+
534
+ def _infer_hashmap_delta(previous_snapshot, current_snapshot):
535
+ if not previous_snapshot or not current_snapshot:
536
+ return (None, None)
537
+
538
+ if previous_snapshot.get('kind') != current_snapshot.get('kind'):
539
+ return (None, None)
540
+
541
+ highlighted_key = None
542
+ deleted_key = None
543
+
544
+ if current_snapshot.get('kind') in ('hashmap', 'map'):
545
+ previous_map = previous_snapshot.get('mapValues') or {}
546
+ current_map = current_snapshot.get('mapValues') or {}
547
+
548
+ previous_keys = set(previous_map.keys())
549
+ current_keys = set(current_map.keys())
550
+
551
+ new_keys = [key for key in current_keys if key not in previous_keys]
552
+ removed_keys = [key for key in previous_keys if key not in current_keys]
553
+ changed_keys = [
554
+ key for key in current_keys
555
+ if key in previous_map and previous_map.get(key) != current_map.get(key)
556
+ ]
557
+
558
+ if len(new_keys) == 1:
559
+ highlighted_key = new_keys[0]
560
+ elif len(changed_keys) == 1:
561
+ highlighted_key = changed_keys[0]
562
+
563
+ if len(removed_keys) == 1:
564
+ deleted_key = removed_keys[0]
565
+
566
+ return (highlighted_key, deleted_key)
567
+
568
+ if current_snapshot.get('kind') == 'set':
569
+ previous_values = previous_snapshot.get('setValues') or {}
570
+ current_values = current_snapshot.get('setValues') or {}
571
+
572
+ added_tokens = [token for token in current_values.keys() if token not in previous_values]
573
+ removed_tokens = [token for token in previous_values.keys() if token not in current_values]
574
+
575
+ if len(added_tokens) == 1:
576
+ highlighted_key = current_values.get(added_tokens[0])
577
+ if len(removed_tokens) == 1:
578
+ deleted_key = previous_values.get(removed_tokens[0])
579
+
580
+ return (highlighted_key, deleted_key)
581
+
582
+ def _clear_frame_hashmap_snapshots(frame):
583
+ frame_prefix = f"{id(frame)}::"
584
+ stale_keys = [
585
+ key for key in list(_prev_hashmap_snapshots.keys())
586
+ if key.startswith(frame_prefix)
587
+ ]
588
+ for key in stale_keys:
589
+ _prev_hashmap_snapshots.pop(key, None)
590
+
591
+ def _build_runtime_visualization(local_vars, frame):
592
+ try:
593
+ hash_maps = []
594
+ object_kinds = {}
595
+ active_snapshot_keys = set()
596
+ frame_prefix = f"{id(frame)}::"
597
+
598
+ for name, value in local_vars.items():
599
+ kind = _classify_runtime_object_kind(value)
600
+ if kind is not None:
601
+ object_kinds[name] = kind
602
+
603
+ snapshot = _extract_hashmap_snapshot(value)
604
+ if snapshot is None:
605
+ continue
606
+
607
+ snapshot_key = f"{frame_prefix}{name}"
608
+ active_snapshot_keys.add(snapshot_key)
609
+ previous_snapshot = _prev_hashmap_snapshots.get(snapshot_key)
610
+ highlighted_key, deleted_key = _infer_hashmap_delta(previous_snapshot, snapshot)
611
+
612
+ payload = {
613
+ 'name': name,
614
+ 'kind': snapshot.get('kind', 'hashmap'),
615
+ 'entries': snapshot.get('entries', []),
616
+ }
617
+ if highlighted_key is not None:
618
+ payload['highlightedKey'] = highlighted_key
619
+ if deleted_key is not None:
620
+ payload['deletedKey'] = deleted_key
621
+
622
+ hash_maps.append(payload)
623
+ _prev_hashmap_snapshots[snapshot_key] = snapshot
624
+
625
+ stale_keys = [
626
+ key for key in list(_prev_hashmap_snapshots.keys())
627
+ if key.startswith(frame_prefix) and key not in active_snapshot_keys
628
+ ]
629
+ for key in stale_keys:
630
+ _prev_hashmap_snapshots.pop(key, None)
631
+
632
+ if len(hash_maps) > 0 or len(object_kinds) > 0:
633
+ payload = {}
634
+ if len(hash_maps) > 0:
635
+ payload['hashMaps'] = hash_maps
636
+ if len(object_kinds) > 0:
637
+ payload['objectKinds'] = object_kinds
638
+ return payload
639
+ return {}
640
+ except Exception:
641
+ return {}
642
+
643
+ def _tracer(frame, event, arg):
644
+ global _trace_limit_exceeded, _timeout_reason, _total_line_events, _line_hit_count, _infinite_loop_line
645
+ func_name = frame.f_code.co_name
646
+
647
+ if func_name in _internal_funcs:
648
+ return _tracer
649
+
650
+ # Skip visual noise from node constructors used only to build data structures.
651
+ if _is_structural_constructor_frame(frame):
652
+ return _tracer
653
+
654
+ # Fast counter for any loops
655
+ if event == 'line':
656
+ _total_line_events += 1
657
+
658
+ # Check total line events
659
+ if _total_line_events >= _max_line_events:
660
+ if not _trace_limit_exceeded:
661
+ _trace_limit_exceeded = True
662
+ _timeout_reason = 'line-limit'
663
+ _infinite_loop_line = frame.f_lineno
664
+ _trace_data.append({
665
+ 'line': frame.f_lineno,
666
+ 'event': 'timeout',
667
+ 'variables': {'timeoutReason': _timeout_reason},
668
+ 'function': func_name,
669
+ 'callStack': _snapshot_call_stack(),
670
+ 'stdoutLineCount': len(_console_output),
671
+ 'accesses': __tracecode_flush_accesses(frame),
672
+ })
673
+ sys.settrace(None)
674
+ raise _InfiniteLoopDetected(f"Exceeded {_max_line_events} line events")
675
+
676
+ # Simple per-line counter (catches any line hit too many times)
677
+ line_key = (func_name, frame.f_lineno)
678
+ _line_hit_count[line_key] = _line_hit_count.get(line_key, 0) + 1
679
+ if _line_hit_count[line_key] >= _max_single_line_hits:
680
+ if not _trace_limit_exceeded:
681
+ _trace_limit_exceeded = True
682
+ _timeout_reason = 'single-line-limit'
683
+ _infinite_loop_line = frame.f_lineno
684
+ local_vars = _snapshot_locals(frame)
685
+ local_vars['timeoutReason'] = _timeout_reason
686
+ _trace_data.append({
687
+ 'line': frame.f_lineno,
688
+ 'event': 'timeout',
689
+ 'variables': local_vars,
690
+ 'function': func_name,
691
+ 'callStack': _snapshot_call_stack(),
692
+ 'stdoutLineCount': len(_console_output),
693
+ 'accesses': __tracecode_flush_accesses(frame),
694
+ 'visualization': _build_runtime_visualization(local_vars, frame)
695
+ })
696
+ sys.settrace(None)
697
+ raise _InfiniteLoopDetected(f"Line {frame.f_lineno} executed {_max_single_line_hits} times")
698
+
699
+ # Hard limit on recorded trace steps
700
+ if (not _MINIMAL_TRACE) and len(_trace_data) >= _max_trace_steps:
701
+ if not _trace_limit_exceeded:
702
+ _trace_limit_exceeded = True
703
+ _timeout_reason = 'trace-limit'
704
+ _infinite_loop_line = frame.f_lineno
705
+ _trace_data.append({
706
+ 'line': frame.f_lineno,
707
+ 'event': 'timeout',
708
+ 'variables': {'timeoutReason': _timeout_reason},
709
+ 'function': func_name,
710
+ 'callStack': _snapshot_call_stack(),
711
+ 'stdoutLineCount': len(_console_output),
712
+ 'accesses': __tracecode_flush_accesses(frame),
713
+ })
714
+ sys.settrace(None)
715
+ raise _InfiniteLoopDetected(f"Exceeded {_max_trace_steps} trace steps")
716
+
717
+ if event == 'call':
718
+ local_vars = _snapshot_locals(frame)
719
+ if func_name != '<module>':
720
+ _call_stack.append({
721
+ 'function': func_name,
722
+ 'args': local_vars.copy() if not _MINIMAL_TRACE else {},
723
+ 'line': frame.f_lineno
724
+ })
725
+ if _MINIMAL_TRACE:
726
+ return _tracer
727
+ _trace_data.append({
728
+ 'line': frame.f_lineno,
729
+ 'event': 'call',
730
+ 'variables': local_vars,
731
+ 'function': func_name,
732
+ 'callStack': _snapshot_call_stack(),
733
+ 'stdoutLineCount': len(_console_output),
734
+ 'accesses': __tracecode_flush_accesses(frame),
735
+ 'visualization': _build_runtime_visualization(local_vars, frame)
736
+ })
737
+ elif event == 'line':
738
+ if _MINIMAL_TRACE:
739
+ return _tracer
740
+ local_vars = _snapshot_locals(frame)
741
+ _trace_data.append({
742
+ 'line': frame.f_lineno,
743
+ 'event': event,
744
+ 'variables': local_vars,
745
+ 'function': func_name,
746
+ 'callStack': _snapshot_call_stack(),
747
+ 'stdoutLineCount': len(_console_output),
748
+ 'accesses': __tracecode_flush_accesses(frame),
749
+ 'visualization': _build_runtime_visualization(local_vars, frame)
750
+ })
751
+ elif event == 'return':
752
+ if not _MINIMAL_TRACE:
753
+ local_vars = _snapshot_locals(frame)
754
+ _trace_data.append({
755
+ 'line': frame.f_lineno,
756
+ 'event': 'return',
757
+ 'variables': local_vars,
758
+ 'function': func_name,
759
+ 'returnValue': _serialize(arg),
760
+ 'callStack': _snapshot_call_stack(),
761
+ 'stdoutLineCount': len(_console_output),
762
+ 'accesses': __tracecode_flush_accesses(frame),
763
+ 'visualization': _build_runtime_visualization(local_vars, frame)
764
+ })
765
+ _clear_frame_hashmap_snapshots(frame)
766
+ _pending_accesses.pop(id(frame), None)
767
+ if _call_stack and _call_stack[-1]['function'] == func_name:
768
+ _call_stack.pop()
769
+
770
+ return _tracer
771
+
772
+ # Clear user-defined globals from previous runs
773
+ # Use __builtins__ to access real globals() and list() in case they were shadowed
774
+ _real_globals = __builtins__['globals'] if isinstance(__builtins__, dict) else getattr(__builtins__, 'globals')
775
+ _real_list = __builtins__['list'] if isinstance(__builtins__, dict) else getattr(__builtins__, 'list')
776
+ _globals_dict = _real_globals()
777
+ _preserve = {"TreeNode", "ListNode", 'sys', 'json', 'math', 'ast', 'print', '__builtins__', '__name__', '__doc__', '__package__', '__loader__', '__spec__'}
778
+ for _k in _real_list(_globals_dict.keys()):
779
+ if not _k.startswith('_') and _k not in _preserve:
780
+ _globals_dict.pop(_k, None)
781
+ del _preserve, _real_globals, _real_list
782
+
783
+ # Ensure print remains routed through the tracer harness after global cleanup
784
+ print = _custom_print
785
+
786
+ `;
787
+
788
+ const userCodeStartLine = 1;
789
+
790
+ // Separate tree inputs (have left/right) from list inputs (have next)
791
+ const treeInputKeys = [];
792
+ const listInputKeys = [];
793
+
794
+ Object.entries(inputs).forEach(([key, value]) => {
795
+ if (value && typeof value === 'object' && !Array.isArray(value) && ('val' in value || 'value' in value)) {
796
+ const hasLeft = 'left' in value;
797
+ const hasRight = 'right' in value;
798
+ const hasNext = 'next' in value;
799
+
800
+ if (hasLeft || hasRight) {
801
+ treeInputKeys.push(key);
802
+ } else if (hasNext) {
803
+ listInputKeys.push(key);
804
+ } else {
805
+ // Default to tree for backwards compatibility
806
+ treeInputKeys.push(key);
807
+ }
808
+ }
809
+ });
810
+
811
+ const treeConversions = treeInputKeys.length > 0
812
+ ? treeInputKeys.map(key => `${key} = _dict_to_tree(${key})`).join('\n')
813
+ : '';
814
+
815
+ const listConversions = listInputKeys.length > 0
816
+ ? listInputKeys.map(key => `${key} = _dict_to_list(${key})`).join('\n')
817
+ : '';
818
+
819
+ const argList = Object.keys(inputs)
820
+ .map((key) => `${key}=${key}`)
821
+ .join(', ');
822
+ const inplaceCandidates = ['nums1', 'nums', 'arr', 'array', 'matrix', 'board', 'grid', 'head']
823
+ .filter((key) => Object.prototype.hasOwnProperty.call(inputs, key));
824
+ const inplaceCandidatesLiteral = JSON.stringify(inplaceCandidates);
825
+ const executionCode = functionName
826
+ ? executionStyle === 'solution-method'
827
+ ? [
828
+ ` if 'Solution' in globals() and hasattr(Solution, '${functionName}'):`,
829
+ ` _solver = Solution()`,
830
+ ` _result = getattr(_solver, '${functionName}')(${argList})`,
831
+ ` elif '${functionName}' in globals() and callable(globals()['${functionName}']):`,
832
+ ` _result = globals()['${functionName}'](${argList})`,
833
+ ` else:`,
834
+ ` raise NameError(\"Implement Solution.${functionName}(...)\")`,
835
+ ].join('\n')
836
+ : executionStyle === 'ops-class'
837
+ ? [
838
+ ` _ops = operations if 'operations' in locals() else (ops if 'ops' in locals() else None)`,
839
+ ` _args = arguments if 'arguments' in locals() else (args if 'args' in locals() else None)`,
840
+ ` if _ops is None or _args is None:`,
841
+ ` raise ValueError(\"ops-class execution requires inputs.operations and inputs.arguments (or ops/args)\")`,
842
+ ` if len(_ops) != len(_args):`,
843
+ ` raise ValueError(\"operations and arguments must have the same length\")`,
844
+ ` _cls = ${functionName}`,
845
+ ` _instance = None`,
846
+ ` _out = []`,
847
+ ` for _i, _op in enumerate(_ops):`,
848
+ ` _call_args = _args[_i] if _i < len(_args) else []`,
849
+ ` if _call_args is None:`,
850
+ ` _call_args = []`,
851
+ ` if not isinstance(_call_args, (list, tuple)):`,
852
+ ` _call_args = [_call_args]`,
853
+ ` if _i == 0:`,
854
+ ` _instance = _cls(*_call_args)`,
855
+ ` _out.append(None)`,
856
+ ` else:`,
857
+ ` if not hasattr(_instance, _op):`,
858
+ ` raise AttributeError(f"Required method '{_op}' is not implemented on {_cls.__name__}")`,
859
+ ` _method = getattr(_instance, _op)`,
860
+ ` _out.append(_method(*_call_args))`,
861
+ ` _result = _out`,
862
+ ].join('\n')
863
+ : ` _result = ${functionName}(${argList})`
864
+ : [
865
+ ` exec(__tracecode_compiled, _globals_dict)`,
866
+ ` _result = _globals_dict.get('result', None)`,
867
+ ].join('\n');
868
+
869
+ const userCodeTraceSetup = [
870
+ `\n_user_code_str = """${escapedCode}"""`,
871
+ `import textwrap as _textwrap`,
872
+ `_user_code_str = _textwrap.dedent(_user_code_str.lstrip("\\n"))`,
873
+ `__tracecode_compiled = __tracecode_compile_user_code(_user_code_str)`,
874
+ ].join('\n');
875
+
876
+ const preloadUserDefinitions = functionName ? `exec(__tracecode_compiled, _globals_dict)\n` : '';
877
+
878
+ const harnessSuffix = `
879
+ ${userCodeTraceSetup}
880
+ ${deps.PYTHON_CONVERSION_HELPERS_SNIPPET}
881
+
882
+ def _resolve_inplace_result():
883
+ for _name in ${inplaceCandidatesLiteral}:
884
+ if _name in globals():
885
+ return globals().get(_name)
886
+ return None
887
+
888
+ ${inputSetup}
889
+
890
+ ${treeConversions}
891
+
892
+ ${listConversions}
893
+
894
+ ${preloadUserDefinitions}
895
+
896
+ sys.settrace(_tracer)
897
+ _trace_failed = False
898
+
899
+ try:
900
+ ${executionCode}
901
+ except _InfiniteLoopDetected as e:
902
+ _trace_failed = True
903
+ _result = None
904
+ # Infinite loop was detected - trace data already has the timeout event
905
+ except Exception as e:
906
+ _trace_failed = True
907
+ # Stop tracing immediately so error-handling internals are never traced.
908
+ sys.settrace(None)
909
+ _result = None
910
+ _exc_type = type(e).__name__
911
+ _exc_msg = str(e)
912
+ _error_line = -1
913
+ _exc_tb = getattr(e, '__traceback__', None)
914
+ while _exc_tb is not None:
915
+ if _exc_tb.tb_lineno is not None:
916
+ _error_line = _exc_tb.tb_lineno
917
+ _exc_tb = _exc_tb.tb_next
918
+ _trace_data.append({
919
+ 'line': _error_line,
920
+ 'event': 'exception',
921
+ 'variables': {
922
+ 'error': _exc_msg,
923
+ 'errorType': _exc_type,
924
+ 'errorLine': _error_line
925
+ },
926
+ 'function': 'error',
927
+ 'callStack': _snapshot_call_stack(),
928
+ 'stdoutLineCount': len(_console_output),
929
+ 'accesses': __tracecode_flush_accesses(None)
930
+ })
931
+
932
+ if (not _trace_failed) and _result is None:
933
+ _inplace = _resolve_inplace_result()
934
+ if _inplace is not None:
935
+ _result = _inplace
936
+
937
+ sys.settrace(None)
938
+
939
+ _builtins.print = _original_print
940
+ print = _original_print
941
+
942
+ json.dumps({
943
+ 'trace': _trace_data,
944
+ 'result': _serialize(_result),
945
+ 'console': _console_output,
946
+ 'userCodeStartLine': ${userCodeStartLine},
947
+ 'traceLimitExceeded': _trace_limit_exceeded,
948
+ 'timeoutReason': _timeout_reason,
949
+ 'lineEventCount': _total_line_events,
950
+ 'traceStepCount': len(_trace_data)
951
+ })
952
+ `;
953
+
954
+ const code = harnessPrefix + harnessSuffix;
955
+
956
+ return { code, userCodeStartLine };
957
+ }
958
+
959
+ /**
960
+ * Parse Python error message
961
+ */
962
+ function parsePythonError(rawError, userCodeStartLine, userCodeLineCount) {
963
+ const mapRawLineToUserLine = (rawLine, allowOutOfBounds = false) => {
964
+ const adjustedLine = rawLine - userCodeStartLine + 1;
965
+ if (adjustedLine <= 0) return undefined;
966
+ if (
967
+ !allowOutOfBounds &&
968
+ typeof userCodeLineCount === 'number' &&
969
+ userCodeLineCount > 0 &&
970
+ adjustedLine > userCodeLineCount
971
+ ) {
972
+ return undefined;
973
+ }
974
+ return adjustedLine;
975
+ };
976
+
977
+ const rewriteEmbeddedLineRefs = (message) =>
978
+ message.replace(/\b(on\s+)?line (\d+)\b/g, (fullMatch, onPrefix = '', lineNumText) => {
979
+ const rawLine = parseInt(lineNumText, 10);
980
+ const mappedLine =
981
+ mapRawLineToUserLine(rawLine, false) ??
982
+ mapRawLineToUserLine(rawLine, true);
983
+ if (!mappedLine) return fullMatch;
984
+ return `${onPrefix}line ${mappedLine}`;
985
+ });
986
+
987
+ // Prefer frame lines from user-compiled code, then fall back to generic "line N" matches.
988
+ const frameLineMatches = [
989
+ ...rawError.matchAll(/File "(?:<exec>|<string>|<user_code>)", line (\d+)/g),
990
+ ];
991
+ const frameRawLines = frameLineMatches.map((match) => parseInt(match[1], 10));
992
+ const genericLineMatches = [...rawError.matchAll(/line (\d+)/g)];
993
+ const genericRawLines = genericLineMatches.map((match) => parseInt(match[1], 10));
994
+ const syntaxLineMatch = rawError.match(/\bon line (\d+)/);
995
+
996
+ const orderedCandidates = [];
997
+ if (syntaxLineMatch) {
998
+ orderedCandidates.push(parseInt(syntaxLineMatch[1], 10));
999
+ }
1000
+
1001
+ // Tracebacks are outermost -> innermost, so reverse to prefer the innermost frame.
1002
+ for (let i = frameRawLines.length - 1; i >= 0; i -= 1) {
1003
+ orderedCandidates.push(frameRawLines[i]);
1004
+ }
1005
+ if (orderedCandidates.length === 0) {
1006
+ for (let i = genericRawLines.length - 1; i >= 0; i -= 1) {
1007
+ orderedCandidates.push(genericRawLines[i]);
1008
+ }
1009
+ }
1010
+
1011
+ let userCodeLine;
1012
+ let hasTrustedUserLine = false;
1013
+ for (const rawLine of orderedCandidates) {
1014
+ const adjustedLine = mapRawLineToUserLine(rawLine, false);
1015
+ if (!adjustedLine) continue;
1016
+ userCodeLine = adjustedLine;
1017
+ hasTrustedUserLine = true;
1018
+ break;
1019
+ }
1020
+
1021
+ if (userCodeLine === undefined) {
1022
+ for (const rawLine of orderedCandidates) {
1023
+ const adjustedLine = mapRawLineToUserLine(rawLine, true);
1024
+ if (adjustedLine) {
1025
+ userCodeLine = adjustedLine;
1026
+ break;
1027
+ }
1028
+ }
1029
+ }
1030
+
1031
+ const errorTypeMatch = rawError.match(/\b((?:\w+Error)|(?:\w+Exception)|KeyError|StopIteration|AssertionError):\s*([\s\S]+)/);
1032
+
1033
+ let formattedMessage;
1034
+
1035
+ if (errorTypeMatch) {
1036
+ const [, errorType, errorMsg] = errorTypeMatch;
1037
+ const cleanedMsg = rewriteEmbeddedLineRefs(errorMsg.trim().split('\n')[0]);
1038
+
1039
+ if (hasTrustedUserLine && userCodeLine !== undefined) {
1040
+ formattedMessage = `${errorType} on line ${userCodeLine}: ${cleanedMsg}`;
1041
+ } else {
1042
+ formattedMessage = `${errorType}: ${cleanedMsg}`;
1043
+ }
1044
+ } else {
1045
+ const lines = rawError.trim().split('\n');
1046
+ const lastLine = lines[lines.length - 1].trim();
1047
+
1048
+ if (hasTrustedUserLine && userCodeLine !== undefined) {
1049
+ formattedMessage = `Error on line ${userCodeLine}: ${lastLine}`;
1050
+ } else {
1051
+ formattedMessage = lastLine || rawError;
1052
+ }
1053
+ }
1054
+
1055
+ return {
1056
+ message: formattedMessage,
1057
+ line: hasTrustedUserLine ? userCodeLine : undefined,
1058
+ };
1059
+ }
1060
+
1061
+ /**
1062
+ * Execute Python code with tracing
1063
+ * @param {string} code - The user's Python code
1064
+ * @param {string} functionName - The function to call
1065
+ * @param {object} inputs - Input parameters
1066
+ * @param {object} options - Optional limits for tracing
1067
+ */
1068
+ async function executeWithTracing(deps, code, functionName, inputs, executionStyle = 'function', options = {}) {
1069
+ const startTime = deps.performanceNow();
1070
+ const userCodeLineCount = code.split('\n').length;
1071
+ const { code: tracingCode, userCodeStartLine } = generateTracingCode(deps,
1072
+ code,
1073
+ functionName,
1074
+ inputs,
1075
+ executionStyle,
1076
+ options
1077
+ );
1078
+
1079
+ try {
1080
+ await deps.loadPyodideInstance();
1081
+
1082
+ const resultJson = await deps.getPyodide().runPythonAsync(tracingCode);
1083
+ const result = JSON.parse(resultJson);
1084
+
1085
+ const executionTimeMs = deps.performanceNow() - startTime;
1086
+
1087
+ const errorStep = result.trace.find(step => step.event === 'exception');
1088
+ const timeoutStep = result.trace.find(step => step.event === 'timeout');
1089
+ const timeoutReason =
1090
+ result.timeoutReason ||
1091
+ timeoutStep?.variables?.timeoutReason ||
1092
+ undefined;
1093
+
1094
+ const adjustedTrace = result.trace.map(step => ({
1095
+ ...step,
1096
+ line: step.line > 0 ? step.line - userCodeStartLine + 1 : step.line,
1097
+ }));
1098
+ const filteredTrace = adjustedTrace.filter((step) => {
1099
+ if (!step || typeof step !== 'object') return false;
1100
+ const line = typeof step.line === 'number' ? step.line : Number(step.line);
1101
+ if (!Number.isFinite(line)) return false;
1102
+ // Keep only user-code line numbers; drop harness setup/teardown noise
1103
+ // such as module bootstrap locals and _resolve_inplace_result frames.
1104
+ return line >= 1 && line <= userCodeLineCount;
1105
+ });
1106
+
1107
+ let errorMessage;
1108
+ let errorLine;
1109
+
1110
+ const isTraceBudgetExceeded =
1111
+ timeoutReason === 'trace-limit' ||
1112
+ timeoutReason === 'line-limit' ||
1113
+ timeoutReason === 'single-line-limit' ||
1114
+ (result.traceLimitExceeded && timeoutReason !== 'client-timeout');
1115
+
1116
+ // Handle tracing guard stops and execution timeouts
1117
+ if (result.traceLimitExceeded || timeoutStep) {
1118
+ const lastStep = adjustedTrace[adjustedTrace.length - 1];
1119
+ errorLine = lastStep?.line;
1120
+ const lineSuffix = errorLine && errorLine > 0 ? ` on line ${errorLine}` : '';
1121
+
1122
+ if (timeoutReason === 'client-timeout') {
1123
+ errorMessage = `Execution timed out${lineSuffix}. This may indicate an infinite loop or very expensive execution.`;
1124
+ } else if (isTraceBudgetExceeded) {
1125
+ errorMessage = `Trace budget exceeded${lineSuffix}. Step-by-step visualization hit its safety limits before execution finished.`;
1126
+ } else {
1127
+ errorMessage = `Execution stopped${lineSuffix}.`;
1128
+ }
1129
+ } else if (errorStep) {
1130
+ const errorType = errorStep.variables?.errorType;
1131
+ const errorMsg = errorStep.variables?.error;
1132
+ const rawErrorLine = errorStep.variables?.errorLine;
1133
+
1134
+ if (rawErrorLine && rawErrorLine > 0) {
1135
+ const mappedLine = rawErrorLine - userCodeStartLine + 1;
1136
+ if (mappedLine > 0 && mappedLine <= userCodeLineCount) {
1137
+ errorLine = mappedLine;
1138
+ }
1139
+ }
1140
+
1141
+ if (errorType && errorMsg) {
1142
+ if (errorLine && errorLine > 0) {
1143
+ errorMessage = `${errorType} on line ${errorLine}: ${errorMsg}`;
1144
+ } else {
1145
+ errorMessage = `${errorType}: ${errorMsg}`;
1146
+ }
1147
+ } else {
1148
+ errorMessage = errorMsg || 'Unknown error';
1149
+ }
1150
+ }
1151
+
1152
+ return {
1153
+ success: !errorStep && !result.traceLimitExceeded && !timeoutStep,
1154
+ output: result.result,
1155
+ error: errorMessage,
1156
+ errorLine,
1157
+ trace: filteredTrace,
1158
+ executionTimeMs,
1159
+ consoleOutput: result.console,
1160
+ traceLimitExceeded: result.traceLimitExceeded,
1161
+ timeoutReason,
1162
+ lineEventCount: result.lineEventCount,
1163
+ traceStepCount: result.traceStepCount,
1164
+ };
1165
+ } catch (error) {
1166
+ const executionTimeMs = deps.performanceNow() - startTime;
1167
+ const rawError = error instanceof Error ? error.message : String(error);
1168
+
1169
+ const { message, line } = parsePythonError(rawError, userCodeStartLine, code.split('\n').length);
1170
+ const isClientTimeout = rawError.includes('timed out');
1171
+
1172
+ return {
1173
+ success: false,
1174
+ error: isClientTimeout
1175
+ ? 'Execution timed out. This may indicate an infinite loop or very expensive execution.'
1176
+ : message,
1177
+ errorLine: line,
1178
+ trace: [],
1179
+ executionTimeMs,
1180
+ consoleOutput: [],
1181
+ timeoutReason: isClientTimeout ? 'client-timeout' : undefined,
1182
+ traceLimitExceeded: isClientTimeout ? true : undefined,
1183
+ lineEventCount: 0,
1184
+ traceStepCount: 0,
1185
+ };
1186
+ }
1187
+ }
1188
+
1189
+ /**
1190
+ * Execute Python code without tracing (for running tests)
1191
+ */
1192
+ async function executeCode(deps, code, functionName, inputs, executionStyle = 'function', options = {}) {
1193
+ const userCodeLineCount = code.split('\n').length;
1194
+ let userCodeStartLine = 1;
1195
+ const interviewGuardEnabled = options.interviewGuard === true;
1196
+ const interviewGuardConfig = {
1197
+ maxLineEvents: Math.max(10000, options.maxLineEvents ?? deps.INTERVIEW_GUARD_DEFAULTS.maxLineEvents),
1198
+ maxSingleLineHits: Math.max(1000, options.maxSingleLineHits ?? deps.INTERVIEW_GUARD_DEFAULTS.maxSingleLineHits),
1199
+ maxCallDepth: Math.max(100, options.maxCallDepth ?? deps.INTERVIEW_GUARD_DEFAULTS.maxCallDepth),
1200
+ maxMemoryBytes: Math.max(8 * 1024 * 1024, options.maxMemoryBytes ?? deps.INTERVIEW_GUARD_DEFAULTS.maxMemoryBytes),
1201
+ memoryCheckEvery: Math.max(10, options.memoryCheckEvery ?? deps.INTERVIEW_GUARD_DEFAULTS.memoryCheckEvery),
1202
+ };
1203
+
1204
+ try {
1205
+ await deps.loadPyodideInstance();
1206
+
1207
+ const inputSetup = Object.entries(inputs)
1208
+ .map(([key, value]) => `${key} = ${deps.toPythonLiteral(value)}`)
1209
+ .join('\n');
1210
+
1211
+ // Separate tree inputs (have left/right) from list inputs (have next)
1212
+ const treeInputKeys = [];
1213
+ const listInputKeys = [];
1214
+
1215
+ Object.entries(inputs).forEach(([key, value]) => {
1216
+ if (value && typeof value === 'object' && !Array.isArray(value) && ('val' in value || 'value' in value)) {
1217
+ const hasLeft = 'left' in value;
1218
+ const hasRight = 'right' in value;
1219
+ const hasNext = 'next' in value;
1220
+
1221
+ if (hasLeft || hasRight) {
1222
+ treeInputKeys.push(key);
1223
+ } else if (hasNext) {
1224
+ listInputKeys.push(key);
1225
+ } else {
1226
+ treeInputKeys.push(key);
1227
+ }
1228
+ }
1229
+ });
1230
+
1231
+ const treeConversions = treeInputKeys.length > 0
1232
+ ? treeInputKeys.map(key => `${key} = _dict_to_tree(${key})`).join('\n')
1233
+ : '';
1234
+
1235
+ const listConversions = listInputKeys.length > 0
1236
+ ? listInputKeys.map(key => `${key} = _dict_to_list(${key})`).join('\n')
1237
+ : '';
1238
+
1239
+ const inputArgs = Object.keys(inputs)
1240
+ .map((key) => `${key}=${key}`)
1241
+ .join(', ');
1242
+ const inplaceCandidates = ['nums1', 'nums', 'arr', 'array', 'matrix', 'board', 'grid', 'head']
1243
+ .filter((key) => Object.prototype.hasOwnProperty.call(inputs, key));
1244
+ const inplaceCandidatesLiteral = JSON.stringify(inplaceCandidates);
1245
+ const executionCall = executionStyle === 'solution-method'
1246
+ ? `if 'Solution' in globals() and hasattr(Solution, '${functionName}'):
1247
+ _solver = Solution()
1248
+ _result = getattr(_solver, '${functionName}')(${inputArgs})
1249
+ elif '${functionName}' in globals() and callable(globals()['${functionName}']):
1250
+ _result = globals()['${functionName}'](${inputArgs})
1251
+ else:
1252
+ raise NameError("Implement Solution.${functionName}(...)")`
1253
+ : executionStyle === 'ops-class'
1254
+ ? `_ops = operations if 'operations' in locals() else (ops if 'ops' in locals() else None)
1255
+ _args = arguments if 'arguments' in locals() else (args if 'args' in locals() else None)
1256
+ if _ops is None or _args is None:
1257
+ raise ValueError("ops-class execution requires inputs.operations and inputs.arguments (or ops/args)")
1258
+ if len(_ops) != len(_args):
1259
+ raise ValueError("operations and arguments must have the same length")
1260
+ _cls = ${functionName}
1261
+ _instance = None
1262
+ _out = []
1263
+ for _i, _op in enumerate(_ops):
1264
+ _call_args = _args[_i] if _i < len(_args) else []
1265
+ if _call_args is None:
1266
+ _call_args = []
1267
+ if not isinstance(_call_args, (list, tuple)):
1268
+ _call_args = [_call_args]
1269
+ if _i == 0:
1270
+ _instance = _cls(*_call_args)
1271
+ _out.append(None)
1272
+ else:
1273
+ if not hasattr(_instance, _op):
1274
+ raise AttributeError(f"Required method '{_op}' is not implemented on {_cls.__name__}")
1275
+ _method = getattr(_instance, _op)
1276
+ _out.append(_method(*_call_args))
1277
+ _result = _out`
1278
+ : `_result = ${functionName}(${inputArgs})`;
1279
+ const executionCallInTry = executionCall
1280
+ .split('\n')
1281
+ .map((line) => (line ? ` ${line}` : line))
1282
+ .join('\n');
1283
+ const executionCallInNestedTry = executionCall
1284
+ .split('\n')
1285
+ .map((line) => (line ? ` ${line}` : line))
1286
+ .join('\n');
1287
+
1288
+ // Keep stdout capture deterministic for the app UI; worker-console mirroring
1289
+ // can cause recursive print chains across mixed runs in dev.
1290
+ const mirrorPrintToConsole = false;
1291
+ const execPrefix = `
1292
+ import json
1293
+ import math
1294
+ import sys
1295
+ import builtins as _builtins
1296
+ ${deps.PYTHON_CLASS_DEFINITIONS_SNIPPET}
1297
+
1298
+ _console_output = []
1299
+ _original_print = _builtins.print
1300
+ _MIRROR_PRINT_TO_WORKER_CONSOLE = ${mirrorPrintToConsole ? 'True' : 'False'}
1301
+
1302
+ def _custom_print(*args, **kwargs):
1303
+ output = " ".join(str(arg) for arg in args)
1304
+ _console_output.append(output)
1305
+ # Do not mirror to worker console; app UI owns stdout rendering.
1306
+
1307
+ print = _custom_print
1308
+
1309
+ ${deps.PYTHON_EXECUTE_SERIALIZE_FUNCTION_SNIPPET}
1310
+
1311
+ ${interviewGuardEnabled
1312
+ ? `
1313
+ class _InterviewGuardTriggered(Exception):
1314
+ pass
1315
+
1316
+ _interview_timeout_reason = None
1317
+ _interview_line_events = 0
1318
+ _interview_line_hits = {}
1319
+ _interview_call_depth = 0
1320
+ _interview_tracemalloc_started = False
1321
+
1322
+ _INTERVIEW_GUARD_INTERNAL_FUNCS = {
1323
+ '_custom_print', '_serialize', '_dict_to_tree', '_dict_to_list',
1324
+ '_interview_guard_tracer', '_interview_check_memory',
1325
+ '_interview_guard_start', '_interview_guard_stop'
1326
+ }
1327
+
1328
+ _INTERVIEW_GUARD_MAX_LINE_EVENTS = ${interviewGuardConfig.maxLineEvents}
1329
+ _INTERVIEW_GUARD_MAX_SINGLE_LINE_HITS = ${interviewGuardConfig.maxSingleLineHits}
1330
+ _INTERVIEW_GUARD_MAX_CALL_DEPTH = ${interviewGuardConfig.maxCallDepth}
1331
+ _INTERVIEW_GUARD_MAX_MEMORY_BYTES = ${interviewGuardConfig.maxMemoryBytes}
1332
+ _INTERVIEW_GUARD_MEMORY_CHECK_EVERY = ${interviewGuardConfig.memoryCheckEvery}
1333
+
1334
+ try:
1335
+ import tracemalloc as _interview_tracemalloc
1336
+ except Exception:
1337
+ _interview_tracemalloc = None
1338
+
1339
+ def _interview_check_memory():
1340
+ global _interview_timeout_reason
1341
+ if _interview_tracemalloc is None or _INTERVIEW_GUARD_MAX_MEMORY_BYTES <= 0:
1342
+ return
1343
+ try:
1344
+ _current, _peak = _interview_tracemalloc.get_traced_memory()
1345
+ except Exception:
1346
+ return
1347
+ if _current >= _INTERVIEW_GUARD_MAX_MEMORY_BYTES or _peak >= _INTERVIEW_GUARD_MAX_MEMORY_BYTES:
1348
+ _interview_timeout_reason = 'memory-limit'
1349
+ raise _InterviewGuardTriggered('INTERVIEW_GUARD_TRIGGERED:memory-limit')
1350
+
1351
+ def _interview_guard_tracer(frame, event, arg):
1352
+ global _interview_timeout_reason, _interview_line_events, _interview_line_hits, _interview_call_depth
1353
+ _func_name = frame.f_code.co_name
1354
+
1355
+ if _func_name in _INTERVIEW_GUARD_INTERNAL_FUNCS:
1356
+ return _interview_guard_tracer
1357
+
1358
+ if event == 'call':
1359
+ _interview_call_depth += 1
1360
+ if _interview_call_depth > _INTERVIEW_GUARD_MAX_CALL_DEPTH:
1361
+ _interview_timeout_reason = 'recursion-limit'
1362
+ raise _InterviewGuardTriggered('INTERVIEW_GUARD_TRIGGERED:recursion-limit')
1363
+ elif event == 'return':
1364
+ if _interview_call_depth > 0:
1365
+ _interview_call_depth -= 1
1366
+ elif event == 'line':
1367
+ _interview_line_events += 1
1368
+ if _interview_line_events >= _INTERVIEW_GUARD_MAX_LINE_EVENTS:
1369
+ _interview_timeout_reason = 'line-limit'
1370
+ raise _InterviewGuardTriggered('INTERVIEW_GUARD_TRIGGERED:line-limit')
1371
+
1372
+ _line_key = (_func_name, frame.f_lineno)
1373
+ _line_hits = _interview_line_hits.get(_line_key, 0) + 1
1374
+ _interview_line_hits[_line_key] = _line_hits
1375
+ if _line_hits >= _INTERVIEW_GUARD_MAX_SINGLE_LINE_HITS:
1376
+ _interview_timeout_reason = 'single-line-limit'
1377
+ raise _InterviewGuardTriggered('INTERVIEW_GUARD_TRIGGERED:single-line-limit')
1378
+
1379
+ if _INTERVIEW_GUARD_MEMORY_CHECK_EVERY > 0 and (_interview_line_events % _INTERVIEW_GUARD_MEMORY_CHECK_EVERY) == 0:
1380
+ _interview_check_memory()
1381
+
1382
+ return _interview_guard_tracer
1383
+
1384
+ def _interview_guard_start():
1385
+ global _interview_tracemalloc_started
1386
+ if _interview_tracemalloc is not None:
1387
+ try:
1388
+ if not _interview_tracemalloc.is_tracing():
1389
+ _interview_tracemalloc.start()
1390
+ _interview_tracemalloc_started = True
1391
+ except Exception:
1392
+ _interview_tracemalloc_started = False
1393
+ _interview_check_memory()
1394
+ sys.settrace(_interview_guard_tracer)
1395
+
1396
+ def _interview_guard_stop():
1397
+ sys.settrace(None)
1398
+ if _interview_tracemalloc is not None and _interview_tracemalloc_started:
1399
+ try:
1400
+ _interview_tracemalloc.stop()
1401
+ except Exception:
1402
+ pass
1403
+ `
1404
+ : ''}
1405
+ `;
1406
+ userCodeStartLine = execPrefix.split('\n').length;
1407
+ const execSuffix = interviewGuardEnabled
1408
+ ? `
1409
+ ${deps.PYTHON_CONVERSION_HELPERS_SNIPPET}
1410
+
1411
+ def _resolve_inplace_result():
1412
+ for _name in ${inplaceCandidatesLiteral}:
1413
+ if _name in globals():
1414
+ return globals().get(_name)
1415
+ return None
1416
+
1417
+ ${inputSetup}
1418
+
1419
+ ${treeConversions}
1420
+
1421
+ ${listConversions}
1422
+
1423
+ _result = None
1424
+ _interview_guard_triggered = False
1425
+ _interview_guard_reason = None
1426
+
1427
+ try:
1428
+ _interview_guard_start()
1429
+ try:
1430
+ ${executionCallInNestedTry}
1431
+ finally:
1432
+ _interview_guard_stop()
1433
+ except _InterviewGuardTriggered as _guard_error:
1434
+ _interview_guard_triggered = True
1435
+ _interview_guard_reason = _interview_timeout_reason or str(_guard_error)
1436
+ finally:
1437
+ _builtins.print = _original_print
1438
+ print = _original_print
1439
+
1440
+ if _interview_guard_triggered:
1441
+ _json_out = json.dumps({
1442
+ "guardTriggered": True,
1443
+ "timeoutReason": _interview_guard_reason,
1444
+ "console": _console_output,
1445
+ })
1446
+ else:
1447
+ if _result is None:
1448
+ _inplace = _resolve_inplace_result()
1449
+ if _inplace is not None:
1450
+ _result = _inplace
1451
+ _json_out = json.dumps({
1452
+ "guardTriggered": False,
1453
+ "output": _serialize(_result),
1454
+ "console": _console_output,
1455
+ })
1456
+
1457
+ _json_out
1458
+ `
1459
+ : `
1460
+ ${deps.PYTHON_CONVERSION_HELPERS_SNIPPET}
1461
+
1462
+ def _resolve_inplace_result():
1463
+ for _name in ${inplaceCandidatesLiteral}:
1464
+ if _name in globals():
1465
+ return globals().get(_name)
1466
+ return None
1467
+
1468
+ ${inputSetup}
1469
+
1470
+ ${treeConversions}
1471
+
1472
+ ${listConversions}
1473
+
1474
+ try:
1475
+ ${executionCallInTry}
1476
+ finally:
1477
+ _builtins.print = _original_print
1478
+ print = _original_print
1479
+
1480
+ if _result is None:
1481
+ _inplace = _resolve_inplace_result()
1482
+ if _inplace is not None:
1483
+ _result = _inplace
1484
+
1485
+ json.dumps({
1486
+ "output": _serialize(_result),
1487
+ "console": _console_output,
1488
+ })
1489
+ `;
1490
+ const execCode = execPrefix + code + execSuffix;
1491
+
1492
+ const resultJson = await deps.getPyodide().runPythonAsync(execCode);
1493
+ const result = JSON.parse(resultJson);
1494
+
1495
+ if (result.guardTriggered) {
1496
+ return {
1497
+ success: false,
1498
+ output: null,
1499
+ error: result.timeoutReason || 'INTERVIEW_GUARD_TRIGGERED:resource-limit',
1500
+ consoleOutput: Array.isArray(result.console) ? result.console : [],
1501
+ };
1502
+ }
1503
+
1504
+ return {
1505
+ success: true,
1506
+ output: result.output,
1507
+ consoleOutput: Array.isArray(result.console) ? result.console : [],
1508
+ };
1509
+ } catch (error) {
1510
+ const rawError = error instanceof Error ? error.message : String(error);
1511
+ const { message, line } = parsePythonError(rawError, userCodeStartLine, userCodeLineCount);
1512
+
1513
+ return {
1514
+ success: false,
1515
+ output: null,
1516
+ error: message,
1517
+ errorLine: line,
1518
+ consoleOutput: [],
1519
+ };
1520
+ }
1521
+ }
1522
+
1523
+ globalScope.__TRACECODE_PYODIDE_RUNTIME__ = {
1524
+ generateTracingCode,
1525
+ parsePythonError,
1526
+ executeWithTracing,
1527
+ executeCode,
1528
+ };
1529
+ })(typeof self !== 'undefined' ? self : globalThis);