@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,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);
|