|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import os.path |
| 4 | +import sys |
| 5 | +from html import escape |
| 6 | + |
| 7 | +from mypy.build import BuildResult |
| 8 | +from mypy.nodes import MypyFile |
| 9 | +from mypy.util import FancyFormatter |
| 10 | +from mypyc.ir.func_ir import FuncIR |
| 11 | +from mypyc.ir.module_ir import ModuleIR |
| 12 | +from mypyc.ir.ops import CallC, LoadLiteral, Value |
| 13 | + |
| 14 | +CSS = """\ |
| 15 | +.collapsible { |
| 16 | + cursor: pointer; |
| 17 | +} |
| 18 | +
|
| 19 | +.content { |
| 20 | + display: block; |
| 21 | + margin-top: 10px; |
| 22 | + margin-bottom: 10px; |
| 23 | +} |
| 24 | +
|
| 25 | +.hint { |
| 26 | + display: inline; |
| 27 | + border: 1px solid #ccc; |
| 28 | + padding: 5px; |
| 29 | +} |
| 30 | +""" |
| 31 | + |
| 32 | +JS = """\ |
| 33 | +document.querySelectorAll('.collapsible').forEach(function(collapsible) { |
| 34 | + collapsible.addEventListener('click', function() { |
| 35 | + const content = this.nextElementSibling; |
| 36 | + if (content.style.display === 'none') { |
| 37 | + content.style.display = 'block'; |
| 38 | + } else { |
| 39 | + content.style.display = 'none'; |
| 40 | + } |
| 41 | + }); |
| 42 | +}); |
| 43 | +""" |
| 44 | + |
| 45 | + |
| 46 | +class AnnotatedSource: |
| 47 | + def __init__(self, path: str, annotations: dict[int, list[str]]) -> None: |
| 48 | + self.path = path |
| 49 | + self.annotations = annotations |
| 50 | + |
| 51 | + |
| 52 | +def generate_annotated_html( |
| 53 | + html_fnam: str, result: BuildResult, modules: dict[str, ModuleIR] |
| 54 | +) -> None: |
| 55 | + annotations = [] |
| 56 | + for mod, mod_ir in modules.items(): |
| 57 | + path = result.graph[mod].path |
| 58 | + tree = result.graph[mod].tree |
| 59 | + assert tree is not None |
| 60 | + annotations.append(generate_annotations(path or "<source>", tree, mod_ir)) |
| 61 | + html = generate_html_report(annotations) |
| 62 | + with open(html_fnam, "w") as f: |
| 63 | + f.write(html) |
| 64 | + |
| 65 | + formatter = FancyFormatter(sys.stdout, sys.stderr, False) |
| 66 | + formatted = formatter.style(os.path.abspath(html_fnam), "none", underline=True, bold=True) |
| 67 | + print(f"\nWrote {formatted} -- open in browser to view\n") |
| 68 | + |
| 69 | + |
| 70 | +def generate_annotations(path: str, tree: MypyFile, ir: ModuleIR) -> AnnotatedSource: |
| 71 | + anns = {} |
| 72 | + for func_ir in ir.functions: |
| 73 | + anns.update(function_annotations(func_ir)) |
| 74 | + return AnnotatedSource(path, anns) |
| 75 | + |
| 76 | + |
| 77 | +def function_annotations(func_ir: FuncIR) -> dict[int, list[str]]: |
| 78 | + # TODO: check if func_ir.line is -1 |
| 79 | + anns: dict[int, list[str]] = {} |
| 80 | + for block in func_ir.blocks: |
| 81 | + for op in block.ops: |
| 82 | + if isinstance(op, CallC): |
| 83 | + name = op.function_name |
| 84 | + ann = None |
| 85 | + if name == "CPyObject_GetAttr": |
| 86 | + attr_name = get_str_literal(op.args[1]) |
| 87 | + if attr_name: |
| 88 | + ann = f'Get non-native attribute "{attr_name}".' |
| 89 | + else: |
| 90 | + ann = "Dynamic attribute lookup." |
| 91 | + elif name == "PyNumber_Add": |
| 92 | + ann = 'Generic "+" operation.' |
| 93 | + if ann: |
| 94 | + anns.setdefault(op.line, []).append(ann) |
| 95 | + return anns |
| 96 | + |
| 97 | + |
| 98 | +def get_str_literal(v: Value) -> str | None: |
| 99 | + if isinstance(v, LoadLiteral) and isinstance(v.value, str): |
| 100 | + return v.value |
| 101 | + return None |
| 102 | + |
| 103 | + |
| 104 | +def generate_html_report(sources: list[AnnotatedSource]) -> str: |
| 105 | + html = [] |
| 106 | + html.append("<html>\n<head>\n") |
| 107 | + html.append(f"<style>\n{CSS}\n</style>") |
| 108 | + html.append("</head>\n") |
| 109 | + html.append("<body>\n") |
| 110 | + for src in sources: |
| 111 | + html.append(f"<h2><tt>{src.path}</tt></h2>\n") |
| 112 | + html.append("<pre>") |
| 113 | + anns = src.annotations |
| 114 | + with open(src.path) as f: |
| 115 | + lines = f.readlines() |
| 116 | + for i, s in enumerate(lines): |
| 117 | + s = escape(s) |
| 118 | + line = i + 1 |
| 119 | + linenum = "%5d" % line |
| 120 | + if line in anns: |
| 121 | + hint = " ".join(anns[line]) |
| 122 | + s = colorize_line(linenum, s, hint_html=hint) |
| 123 | + else: |
| 124 | + s = linenum + " " + s |
| 125 | + html.append(s) |
| 126 | + html.append("</pre>") |
| 127 | + |
| 128 | + html.append("<script>") |
| 129 | + html.append(JS) |
| 130 | + html.append("</script>") |
| 131 | + |
| 132 | + html.append("</body></html>\n") |
| 133 | + return "".join(html) |
| 134 | + |
| 135 | + |
| 136 | +def colorize_line(linenum: str, s: str, hint_html: str) -> str: |
| 137 | + hint_prefix = " " * len(linenum) + " " |
| 138 | + line_span = f'<div class="collapsible" style="background-color: #fcc">{linenum} {s}</div>' |
| 139 | + hint_div = f'<div class="content">{hint_prefix}<div class="hint">{hint_html}</div></div>' |
| 140 | + return f"<span>{line_span}{hint_div}</span>" |
0 commit comments