Skip to content

Commit f170cc0

Browse files
Allow highlighting lines (#25)
1 parent 08978a9 commit f170cc0

2 files changed

Lines changed: 214 additions & 21 deletions

File tree

web/driver.py

Lines changed: 130 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@
77
import sys
88
import tokenize
99
import traceback
10+
import types
1011
from token import tok_name
12+
from typing import Any, Iterator
1113

1214
from _testinternalcapi import compiler_codegen, optimize_cfg
1315

1416

15-
def view_tokens(code: str) -> str:
16-
out = []
17+
def _as_view(rows: list[tuple[str, int | None]]) -> dict[str, Any]:
18+
text_lines = [row[0] for row in rows]
19+
src_lines = [row[1] for row in rows]
20+
return {"text": "\n".join(text_lines), "lines": src_lines}
21+
22+
23+
def view_tokens(code: str) -> dict[str, Any]:
24+
rows = []
1725
toks = tokenize.tokenize(io.BytesIO(code.encode("utf-8")).readline)
1826
current_line = 0
1927
for t in toks:
@@ -24,14 +32,77 @@ def view_tokens(code: str) -> str:
2432
marker = f"{line:4d}: "
2533
else:
2634
marker = " "
27-
out.append(f"{marker}{tok_name[t.exact_type]:10} {t.string!r}")
35+
rows.append(
36+
(
37+
f"{marker}{tok_name[t.exact_type]:10} {t.string!r}",
38+
line if line > 0 else None,
39+
)
40+
)
2841
current_line = line
29-
return "\n".join(out)
42+
return _as_view(rows)
43+
44+
45+
def _has_ast_children(node: ast.AST) -> bool:
46+
if isinstance(node, ast.Name):
47+
return False
48+
SENTINEL = object()
49+
for name in node._fields:
50+
value = getattr(node, name, SENTINEL)
51+
if isinstance(value, (list, ast.AST)):
52+
return True
53+
return False
54+
55+
56+
def _ast_attr_repr(node: ast.AST, attr: str) -> str:
57+
value = getattr(node, attr, ...)
58+
if isinstance(value, (ast.Load, ast.Store, ast.Del)):
59+
return value.__class__.__name__
60+
return repr(value)
61+
62+
63+
def _dump_ast(tree: ast.AST) -> Iterator[tuple[str, int | None]]:
64+
SENTINEL = object()
65+
indent = " "
66+
67+
def walk(
68+
node: Any, level: int = 0, last_line: int = 0, prepend: str = ""
69+
) -> Iterator[tuple[str, int]]:
70+
prefix = f"{indent * level}{prepend}"
71+
if isinstance(node, ast.AST):
72+
fields = node._fields
73+
start = getattr(node, "lineno", last_line) or last_line
74+
if not _has_ast_children(node):
75+
args = ", ".join(f"{n}={_ast_attr_repr(node, n)}" for n in fields)
76+
yield f"{prefix}{node.__class__.__name__}({args})", start
77+
else:
78+
yield f"{prefix}{node.__class__.__name__}()", start
79+
for name in fields:
80+
value = getattr(node, name, SENTINEL)
81+
if value is SENTINEL:
82+
continue
83+
yield from walk(value, level + 1, start, f"{name}=")
84+
elif isinstance(node, list):
85+
if len(node) == 1 and not _has_ast_children(node[0]):
86+
inner = list(walk(node[0], level, last_line, prepend + "["))
87+
if len(inner) == 1:
88+
text, line = inner[0]
89+
yield text + "]", line
90+
return
91+
yield from inner
92+
else:
93+
yield f"{prefix}[]", last_line
94+
for value in node:
95+
yield from walk(value, level + 1, last_line)
96+
else:
97+
yield f"{prefix}{node!r}", last_line
3098

99+
for text, line in walk(tree):
100+
yield text, (line if line and line > 0 else None)
31101

32-
def view_ast(code: str, *, optimize: bool = False) -> str:
102+
103+
def view_ast(code: str, *, optimize: bool = False) -> dict[str, Any]:
33104
tree = ast.parse(code, optimize=1) if optimize else ast.parse(code)
34-
return ast.dump(tree, indent=4)
105+
return _as_view(list(_dump_ast(tree)))
35106

36107

37108
class _PseudoArgResolver(dis.ArgResolver):
@@ -44,10 +115,13 @@ def offset_from_jump_arg(self, op, arg, offset):
44115
class _CaptureStream:
45116
def __init__(self):
46117
self.lines = []
118+
self.src_lines = []
119+
self.current_line = None
47120

48121
def write(self, line):
49122
if line.strip():
50123
self.lines.append(line)
124+
self.src_lines.append(self.current_line)
51125

52126

53127
def _iter_instructions(insts, resolver):
@@ -78,20 +152,28 @@ def _iter_instructions(insts, resolver):
78152
)
79153

80154

81-
def _disassemble(insts_list, co_consts) -> str:
155+
class _LineTrackingFormatter(dis.Formatter):
156+
def print_instruction(self, instr, mark_as_current=False):
157+
line = getattr(instr, "line_number", None)
158+
if line:
159+
self.file.current_line = line
160+
super().print_instruction(instr, mark_as_current=mark_as_current)
161+
162+
163+
def _disassemble(insts_list, co_consts) -> dict[str, Any]:
82164
stream = _CaptureStream()
83165
jump_targets = [
84166
t for op, t, *_ in insts_list if op in dis.hasjump or op in dis.hasexc
85167
]
86168
labels_map = {o: i for i, o in enumerate(jump_targets, start=1)}
87169
resolver = _PseudoArgResolver(co_consts=co_consts, labels_map=labels_map)
88-
fmt = dis.Formatter(
170+
fmt = _LineTrackingFormatter(
89171
file=stream,
90172
lineno_width=4,
91173
label_width=4 + len(str(len(labels_map))),
92174
)
93175
dis.print_instructions(_iter_instructions(insts_list, resolver), None, fmt)
94-
return "\n".join(stream.lines)
176+
return {"text": "\n".join(stream.lines), "lines": list(stream.src_lines)}
95177

96178

97179
class _ConstPlaceholder:
@@ -216,7 +298,35 @@ def _instruction_items(insts):
216298
return list(insts)
217299

218300

219-
def view_pseudo(code: str, *, optimize: bool = False) -> str:
301+
def _iter_nested_code_objects(co: types.CodeType) -> Iterator[types.CodeType]:
302+
for const in co.co_consts:
303+
if isinstance(const, types.CodeType):
304+
yield const
305+
yield from _iter_nested_code_objects(const)
306+
307+
308+
def _heading_view(co: types.CodeType) -> dict[str, Any]:
309+
name = getattr(co, "co_qualname", None) or co.co_name
310+
text = f"\nDisassembly of <code object {name} at line {co.co_firstlineno}>:"
311+
return {"text": text, "lines": [None] * (text.count("\n") + 1)}
312+
313+
314+
def _combine_views(*parts: dict[str, Any]) -> dict[str, Any]:
315+
text_segs = []
316+
src_lines = []
317+
for p in parts:
318+
text_segs.append(p["text"])
319+
src_lines.extend(p["lines"])
320+
return {"text": "\n".join(text_segs), "lines": src_lines}
321+
322+
323+
def _nested_compiled_views(co: types.CodeType) -> Iterator[dict[str, Any]]:
324+
for nested in _iter_nested_code_objects(co):
325+
yield _heading_view(nested)
326+
yield _disassemble(list(dis.Bytecode(nested)), list(nested.co_consts))
327+
328+
329+
def view_pseudo(code: str, *, optimize: bool = False) -> dict[str, Any]:
220330
insts, metadata = compiler_codegen(ast.parse(code, optimize=1), "<source>", 0)
221331
co_consts = _merge_co_consts(
222332
_co_consts_from_metadata(metadata), _compiled_co_consts(code)
@@ -228,16 +338,18 @@ def view_pseudo(code: str, *, optimize: bool = False) -> str:
228338
insts = optimize_cfg(insts, co_consts, 0)
229339
items = _instruction_items(insts)
230340
adjusted_consts = _apply_annotations_const_workaround(items, co_consts)
231-
return _disassemble(items, _fit_co_consts(items, adjusted_consts))
341+
top = _disassemble(items, _fit_co_consts(items, adjusted_consts))
342+
co = compile(code, "<source>", "exec", optimize=1)
343+
return _combine_views(top, *_nested_compiled_views(co))
232344

233345

234-
def view_compiled(code: str) -> str:
346+
def view_compiled(code: str) -> dict[str, Any]:
235347
# assemble_code_object requires metadata["consts"] that compiler_codegen
236348
# no longer emits. Fall back to the public compile() API which yields an
237349
# equivalent final code object (with real consts).
238350
co = compile(code, "<source>", "exec", optimize=1)
239-
items = list(dis.Bytecode(co))
240-
return _disassemble(items, list(co.co_consts))
351+
top = _disassemble(list(dis.Bytecode(co)), list(co.co_consts))
352+
return _combine_views(top, *_nested_compiled_views(co))
241353

242354

243355
VIEWS = {
@@ -258,9 +370,11 @@ def main() -> int:
258370
result = {"python_version": sys.version.split()[0]}
259371
for name, fn in VIEWS.items():
260372
try:
261-
result[name] = fn(code)
373+
view = fn(code)
262374
except Exception:
263-
result[name] = traceback.format_exc()
375+
text = traceback.format_exc()
376+
view = {"text": text, "lines": [None] * len(text.splitlines())}
377+
result[name] = view
264378
json.dump(result, sys.stdout)
265379
return 0
266380

web/index.html

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,26 @@
117117
line-height: 1.45;
118118
white-space: pre;
119119
}
120+
.panel > .content .lines {
121+
display: inline-block;
122+
min-width: 100%;
123+
}
124+
.panel > .content .line {
125+
display: block;
126+
min-height: 1.45em;
127+
}
128+
.panel > .content .line[data-src-line] { cursor: pointer; }
129+
.panel > .content .line[data-src-line]:hover {
130+
background: rgba(255, 215, 0, 0.15);
131+
}
132+
.panel > .content .line.highlight {
133+
background: rgba(255, 215, 0, 0.35);
134+
}
135+
.ace-codoscope-highlight {
136+
position: absolute;
137+
background: rgba(255, 215, 0, 0.35);
138+
z-index: 20;
139+
}
120140
#editor {
121141
flex: 1;
122142
min-height: 0;
@@ -208,21 +228,78 @@ <h2>Code Object</h2><pre class="content"></pre>
208228
}
209229
};
210230

211-
const setPanelContent = (view, text) => {
212-
document.querySelector(`.panel[data-view="${view}"] .content`).textContent = text;
231+
const setPanelView = (view, data) => {
232+
const content = document.querySelector(`.panel[data-view="${view}"] .content`);
233+
content.innerHTML = "";
234+
const text = typeof data === "string" ? data : data.text;
235+
const lines = typeof data === "string" ? [] : (data.lines || []);
236+
const parts = text.split("\n");
237+
const wrap = document.createElement("div");
238+
wrap.className = "lines";
239+
for (let i = 0; i < parts.length; i++) {
240+
const div = document.createElement("div");
241+
div.className = "line";
242+
div.textContent = parts[i] === "" ? " " : parts[i];
243+
const srcLine = lines[i];
244+
if (typeof srcLine === "number" && srcLine > 0) {
245+
div.dataset.srcLine = String(srcLine);
246+
}
247+
wrap.appendChild(div);
248+
}
249+
content.appendChild(wrap);
213250
};
214251

215252
const showError = (text) => {
216253
const short = String(text).split("\n")[0];
217254
status.textContent = `error: ${short}`;
218255
pyver.textContent = "failed to start CPython (WASM)";
219-
for (const v of VIEWS) setPanelContent(v, String(text));
256+
for (const v of VIEWS) setPanelView(v, String(text));
220257
};
221258

222259
const clearAllPanels = (placeholder) => {
223-
for (const v of VIEWS) setPanelContent(v, placeholder);
260+
for (const v of VIEWS) setPanelView(v, placeholder);
224261
};
225262

263+
let currentSrcLine = null;
264+
let aceMarkerId = null;
265+
266+
const applyHighlight = (srcLine) => {
267+
currentSrcLine = srcLine;
268+
for (const el of document.querySelectorAll(".panel .content .line.highlight")) {
269+
el.classList.remove("highlight");
270+
}
271+
if (aceMarkerId != null && window.editor) {
272+
window.editor.session.removeMarker(aceMarkerId);
273+
aceMarkerId = null;
274+
}
275+
if (!srcLine) return;
276+
const selector = `.panel .content .line[data-src-line="${srcLine}"]`;
277+
for (const el of document.querySelectorAll(selector)) {
278+
el.classList.add("highlight");
279+
}
280+
if (window.editor && window.ace) {
281+
const Range = window.ace.require("ace/range").Range;
282+
aceMarkerId = window.editor.session.addMarker(
283+
new Range(srcLine - 1, 0, srcLine - 1, Infinity),
284+
"ace-codoscope-highlight",
285+
"fullLine",
286+
);
287+
}
288+
};
289+
290+
document.addEventListener("click", (e) => {
291+
const line = e.target.closest(".panel .content .line[data-src-line]");
292+
if (!line) return;
293+
applyHighlight(Number(line.dataset.srcLine));
294+
});
295+
296+
window.addEventListener("codoscope:editor-ready", () => {
297+
window.editor.on("click", () => {
298+
const row = window.editor.getCursorPosition().row;
299+
applyHighlight(row + 1);
300+
});
301+
});
302+
226303
const run = () => {
227304
runBtn.disabled = true;
228305
status.textContent = "compiling…";
@@ -262,7 +339,8 @@ <h2>Code Object</h2><pre class="content"></pre>
262339
try {
263340
const result = JSON.parse(String.fromCharCode(...stdout));
264341
pyver.textContent = `CPython ${result.python_version} (WASM)`;
265-
for (const v of VIEWS) setPanelContent(v, result[v] ?? "");
342+
for (const v of VIEWS) setPanelView(v, result[v] ?? "");
343+
applyHighlight(currentSrcLine);
266344
status.textContent = "";
267345
} catch (err) {
268346
const errText = [
@@ -336,6 +414,7 @@ <h2>Code Object</h2><pre class="content"></pre>
336414
window.editor.setTheme("ace/theme/tomorrow_night");
337415
window.editor.setOption("fontSize", "12px");
338416
window.editor.renderer.setShowGutter(true);
417+
window.dispatchEvent(new Event("codoscope:editor-ready"));
339418
});
340419
</script>
341420
</body>

0 commit comments

Comments
 (0)