pyc-revive 1.0.0__py3-none-any.whl
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.
- pyc_revive/__init__.py +6 -0
- pyc_revive/__main__.py +3 -0
- pyc_revive/autofix.py +247 -0
- pyc_revive/bytecode.py +166 -0
- pyc_revive/cfg.py +272 -0
- pyc_revive/cli.py +71 -0
- pyc_revive/emitter.py +2385 -0
- pyc_revive/finder.py +118 -0
- pyc_revive/loader.py +53 -0
- pyc_revive/normalizer.py +70 -0
- pyc_revive/pycdas.exe +0 -0
- pyc_revive/pycdas_parser.py +337 -0
- pyc_revive/pymagic.py +161 -0
- pyc_revive/reconstructor.py +109 -0
- pyc_revive/segmenter.py +122 -0
- pyc_revive/shim.py +182 -0
- pyc_revive/translator.py +766 -0
- pyc_revive/verifier.py +106 -0
- pyc_revive-1.0.0.dist-info/METADATA +95 -0
- pyc_revive-1.0.0.dist-info/RECORD +23 -0
- pyc_revive-1.0.0.dist-info/WHEEL +5 -0
- pyc_revive-1.0.0.dist-info/entry_points.txt +2 -0
- pyc_revive-1.0.0.dist-info/top_level.txt +1 -0
pyc_revive/__init__.py
ADDED
pyc_revive/__main__.py
ADDED
pyc_revive/autofix.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Self-validation loop: compile the revived source, diagnose failures,
|
|
2
|
+
apply targeted structural repairs, repeat until clean or out of budget.
|
|
3
|
+
|
|
4
|
+
This is the last gate before a build ships: it proves the artifact parses,
|
|
5
|
+
and when it does not, it fixes what it can (orphan handlers, bad indents,
|
|
6
|
+
empty suites) and reports what it could not. Returns the best source found
|
|
7
|
+
plus a repair log for the report.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _compile_errors(src: str) -> list[dict]:
|
|
13
|
+
try:
|
|
14
|
+
compile(src, "<revived>", "exec")
|
|
15
|
+
return []
|
|
16
|
+
except SyntaxError as e:
|
|
17
|
+
return [{"line": e.lineno or 0, "msg": str(e.msg or e), "text": (e.text or "").rstrip()}]
|
|
18
|
+
except Exception as e:
|
|
19
|
+
return [{"line": 0, "msg": str(e), "text": ""}]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _indent_of(line: str) -> int:
|
|
23
|
+
return len(line) - len(line.lstrip(" "))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _is_header(s: str) -> bool:
|
|
27
|
+
t = s.strip()
|
|
28
|
+
if not t or t.startswith("#"):
|
|
29
|
+
return False
|
|
30
|
+
head = t.split(":")[0].split()
|
|
31
|
+
if not head:
|
|
32
|
+
return False
|
|
33
|
+
if not t.rstrip().endswith(":"):
|
|
34
|
+
return False
|
|
35
|
+
return head[0] in (
|
|
36
|
+
"if", "elif", "else", "for", "while", "try", "except", "finally",
|
|
37
|
+
"with", "def", "class", "match", "case")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _fix_empty_suite(lines: list[str], err: dict) -> list[str] | None:
|
|
41
|
+
if "expected an indented block" not in err["msg"]:
|
|
42
|
+
return None
|
|
43
|
+
n = (err["line"] or 1) - 1
|
|
44
|
+
if n < 0 or n >= len(lines):
|
|
45
|
+
return None
|
|
46
|
+
hdr = lines[n]
|
|
47
|
+
if not _is_header(hdr):
|
|
48
|
+
return None
|
|
49
|
+
lvl = _indent_of(hdr)
|
|
50
|
+
nxt = next((k for k in range(n + 1, len(lines)) if lines[k].strip()), None)
|
|
51
|
+
if nxt is not None and _indent_of(lines[nxt]) > lvl:
|
|
52
|
+
return None # suite exists; different problem
|
|
53
|
+
out = list(lines)
|
|
54
|
+
out.insert(n + 1, " " * (lvl + 4) + "pass")
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _fix_orphan(lines: list[str], err: dict) -> list[str] | None:
|
|
59
|
+
msg = err["msg"]
|
|
60
|
+
n = (err["line"] or 1) - 1
|
|
61
|
+
if n < 0 or n >= len(lines):
|
|
62
|
+
return None
|
|
63
|
+
s = lines[n].strip()
|
|
64
|
+
if msg == "expected 'except' or 'finally' block" and not (s.startswith("except") or s.startswith("finally") or s == "try:"):
|
|
65
|
+
# try suite broke early (stray flat line closed it): find the
|
|
66
|
+
# enclosing try and either drop a stray pass or reindent the line in
|
|
67
|
+
for k in range(n - 1, -1, -1):
|
|
68
|
+
if not lines[k].strip():
|
|
69
|
+
continue
|
|
70
|
+
if _indent_of(lines[k]) > _indent_of(lines[n]):
|
|
71
|
+
continue
|
|
72
|
+
if lines[k].strip() == "try:" and _indent_of(lines[k]) <= _indent_of(lines[n]):
|
|
73
|
+
out = list(lines)
|
|
74
|
+
if s == "pass":
|
|
75
|
+
del out[n]
|
|
76
|
+
return out
|
|
77
|
+
lvl = _indent_of(lines[k]) + 4
|
|
78
|
+
delta = _indent_of(lines[n]) - lvl
|
|
79
|
+
if delta == 0:
|
|
80
|
+
return None
|
|
81
|
+
end = n
|
|
82
|
+
for m in range(n + 1, len(lines)):
|
|
83
|
+
if not lines[m].strip():
|
|
84
|
+
continue
|
|
85
|
+
if _indent_of(lines[m]) > _indent_of(lines[n]):
|
|
86
|
+
end = m
|
|
87
|
+
else:
|
|
88
|
+
break
|
|
89
|
+
if delta > 0:
|
|
90
|
+
for m in range(n, end + 1):
|
|
91
|
+
if lines[m].strip() and len(lines[m]) - len(lines[m].lstrip(" ")) >= delta:
|
|
92
|
+
out[m] = lines[m][delta:]
|
|
93
|
+
else:
|
|
94
|
+
for m in range(n, end + 1):
|
|
95
|
+
if lines[m].strip():
|
|
96
|
+
out[m] = " " * (-delta) + lines[m].lstrip(" ")
|
|
97
|
+
return out
|
|
98
|
+
break
|
|
99
|
+
return None
|
|
100
|
+
want: str | None = None
|
|
101
|
+
if s.startswith("except") or s.startswith("finally"):
|
|
102
|
+
want = "try"
|
|
103
|
+
elif s.startswith("else:"):
|
|
104
|
+
want = "if/for/while"
|
|
105
|
+
elif s.startswith("elif"):
|
|
106
|
+
want = "if"
|
|
107
|
+
if want is None:
|
|
108
|
+
if s == "try:":
|
|
109
|
+
out = list(lines)
|
|
110
|
+
out.insert(n + 1, " " * (_indent_of(lines[n]) + 4) + "pass")
|
|
111
|
+
return out
|
|
112
|
+
if "expected 'except' or 'finally'" not in msg:
|
|
113
|
+
return None
|
|
114
|
+
want = "body" # try suite ran dry; just needs any suite
|
|
115
|
+
lvl = _indent_of(lines[n])
|
|
116
|
+
# nearest shallower line decides: a matching opener reindents us to it
|
|
117
|
+
for k in range(n - 1, -1, -1):
|
|
118
|
+
if not lines[k].strip():
|
|
119
|
+
continue
|
|
120
|
+
ilk = _indent_of(lines[k])
|
|
121
|
+
if ilk > lvl:
|
|
122
|
+
continue
|
|
123
|
+
if ilk == lvl:
|
|
124
|
+
continue
|
|
125
|
+
t = lines[k].strip()
|
|
126
|
+
ok = ((want == "try" and t == "try:") or
|
|
127
|
+
(want == "if/for/while" and (t.startswith("if ") or t.startswith("if(") or t.startswith("for ") or t.startswith("while ") or t == "try:")) or
|
|
128
|
+
(want == "if" and (t.startswith("if ") or t.startswith("if("))) or
|
|
129
|
+
(want == "body"))
|
|
130
|
+
if ok:
|
|
131
|
+
delta = lvl - ilk
|
|
132
|
+
if delta <= 0:
|
|
133
|
+
return None
|
|
134
|
+
out = list(lines)
|
|
135
|
+
end = n
|
|
136
|
+
for m in range(n + 1, len(lines)):
|
|
137
|
+
if not lines[m].strip():
|
|
138
|
+
continue
|
|
139
|
+
if _indent_of(lines[m]) > lvl:
|
|
140
|
+
end = m
|
|
141
|
+
else:
|
|
142
|
+
break
|
|
143
|
+
for m in range(n, end + 1):
|
|
144
|
+
if lines[m].strip() and len(lines[m]) - len(lines[m].lstrip(" ")) >= delta:
|
|
145
|
+
out[m] = lines[m][delta:]
|
|
146
|
+
return out
|
|
147
|
+
break
|
|
148
|
+
# no partner: neutralize the orphan so the file parses; body stays visible
|
|
149
|
+
out = list(lines)
|
|
150
|
+
if s.startswith("except") or s.startswith("finally"):
|
|
151
|
+
out[n] = " " * lvl + "if False: # unattached handler kept visible"
|
|
152
|
+
elif s.startswith("else:") or s.startswith("elif"):
|
|
153
|
+
out[n] = " " * lvl + "if True: # unattached else kept visible"
|
|
154
|
+
else:
|
|
155
|
+
return None
|
|
156
|
+
return out
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _fix_indent(lines: list[str], err: dict) -> list[str] | None:
|
|
160
|
+
if "unexpected indent" not in err["msg"] and "unindent" not in err["msg"]:
|
|
161
|
+
return None
|
|
162
|
+
n = (err["line"] or 1) - 1
|
|
163
|
+
if n <= 0 or n >= len(lines):
|
|
164
|
+
return None
|
|
165
|
+
p = n - 1
|
|
166
|
+
while p >= 0 and not lines[p].strip():
|
|
167
|
+
p -= 1
|
|
168
|
+
if p < 0:
|
|
169
|
+
return None
|
|
170
|
+
plvl = _indent_of(lines[p])
|
|
171
|
+
cur = _indent_of(lines[n])
|
|
172
|
+
target = plvl + 4 if lines[p].rstrip().endswith(":") else plvl
|
|
173
|
+
if cur == target:
|
|
174
|
+
return None
|
|
175
|
+
if "unindent" in err["msg"] and cur in (plvl, plvl + 4) and target in (plvl, plvl + 4):
|
|
176
|
+
return None
|
|
177
|
+
delta = cur - target
|
|
178
|
+
if delta <= 0:
|
|
179
|
+
# needs indenting up (rare); shift line and suite right
|
|
180
|
+
out = list(lines)
|
|
181
|
+
end = n
|
|
182
|
+
for m in range(n + 1, len(lines)):
|
|
183
|
+
if not lines[m].strip():
|
|
184
|
+
continue
|
|
185
|
+
if _indent_of(lines[m]) > cur:
|
|
186
|
+
end = m
|
|
187
|
+
else:
|
|
188
|
+
break
|
|
189
|
+
for m in range(n, end + 1):
|
|
190
|
+
if lines[m].strip():
|
|
191
|
+
out[m] = " " * (-delta) + lines[m].lstrip(" ")
|
|
192
|
+
return out
|
|
193
|
+
out = list(lines)
|
|
194
|
+
end = n
|
|
195
|
+
for k in range(n + 1, len(lines)):
|
|
196
|
+
if not lines[k].strip():
|
|
197
|
+
continue
|
|
198
|
+
if _indent_of(lines[k]) > cur:
|
|
199
|
+
end = k
|
|
200
|
+
else:
|
|
201
|
+
break
|
|
202
|
+
for k in range(n, end + 1):
|
|
203
|
+
if lines[k].strip() and len(lines[k]) - len(lines[k].lstrip(" ")) >= delta:
|
|
204
|
+
out[k] = lines[k][delta:]
|
|
205
|
+
return out
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
_REPAIRS = (_fix_empty_suite, _fix_orphan, _fix_indent)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def autovalidate(src: str, budget: int = 8) -> tuple[str, dict]:
|
|
212
|
+
"""Compile-repair loop. Returns (best_src, log). Never raises."""
|
|
213
|
+
rounds: list[dict] = []
|
|
214
|
+
cur = src
|
|
215
|
+
errs = _compile_errors(cur)
|
|
216
|
+
if not errs:
|
|
217
|
+
return cur, {"compiles": True, "rounds": rounds, "repairs": 0}
|
|
218
|
+
best, best_key = cur, (len(errs) and -errs[0]["line"] or 0)
|
|
219
|
+
for _ in range(budget):
|
|
220
|
+
errs = _compile_errors(cur)
|
|
221
|
+
if not errs:
|
|
222
|
+
return cur, {"compiles": True, "rounds": rounds, "repairs": len(rounds)}
|
|
223
|
+
err = errs[0]
|
|
224
|
+
fixed = None
|
|
225
|
+
used = None
|
|
226
|
+
for fn in _REPAIRS:
|
|
227
|
+
try:
|
|
228
|
+
fixed = fn(cur.splitlines(), err)
|
|
229
|
+
except Exception:
|
|
230
|
+
fixed = None
|
|
231
|
+
if fixed is not None:
|
|
232
|
+
used = fn.__name__
|
|
233
|
+
break
|
|
234
|
+
if fixed is None:
|
|
235
|
+
rounds.append({"error": err, "repair": None})
|
|
236
|
+
break
|
|
237
|
+
cur = "\n".join(fixed).rstrip() + "\n"
|
|
238
|
+
rounds.append({"error": err, "repair": used})
|
|
239
|
+
key = 0
|
|
240
|
+
errs2 = _compile_errors(cur)
|
|
241
|
+
if not errs2:
|
|
242
|
+
return cur, {"compiles": True, "rounds": rounds, "repairs": len(rounds)}
|
|
243
|
+
key = -errs2[0]["line"]
|
|
244
|
+
if key >= best_key:
|
|
245
|
+
best, best_key = cur, key
|
|
246
|
+
return best, {"compiles": False, "rounds": rounds, "repairs": len(rounds),
|
|
247
|
+
"note": "budget spent; shipping closest parse"}
|
pyc_revive/bytecode.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Version-agnostic bytecode model: Inst + EditableBytecode.
|
|
2
|
+
|
|
3
|
+
Wraps stdlib `dis` into mutable instruction objects so the rest of the
|
|
4
|
+
pipeline (normalize / segment / cfg) never touches raw bytes directly.
|
|
5
|
+
Tracks opcode, oparg, value-stack depth, metadata tables.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import dis
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from types import CodeType
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
# opnames that transfer control — used by CFG, version-agnostic superset
|
|
14
|
+
_JUMP_OPS = {
|
|
15
|
+
"POP_JUMP_IF_FALSE", "POP_JUMP_IF_TRUE",
|
|
16
|
+
"POP_JUMP_FORWARD_IF_FALSE", "POP_JUMP_FORWARD_IF_TRUE",
|
|
17
|
+
"POP_JUMP_BACKWARD_IF_FALSE", "POP_JUMP_BACKWARD_IF_TRUE",
|
|
18
|
+
"JUMP_IF_FALSE_OR_POP", "JUMP_IF_TRUE_OR_POP",
|
|
19
|
+
"JUMP_IF_TRUE", "JUMP_IF_FALSE", "JUMP_IF_NOT_EXC_MATCH",
|
|
20
|
+
"CONTINUE_LOOP", "BREAK_LOOP",
|
|
21
|
+
"JUMP_FORWARD", "JUMP_BACKWARD", "JUMP_ABSOLUTE", "JUMP",
|
|
22
|
+
"JUMP_BACKWARD_NO_INTERRUPT", "JUMP_NO_INTERRUPT",
|
|
23
|
+
"FOR_ITER", "SEND",
|
|
24
|
+
"SETUP_FINALLY", "SETUP_EXCEPT", "SETUP_WITH", "SETUP_LOOP",
|
|
25
|
+
"SETUP_FINALLY_PRE", "BEFORE_WITH",
|
|
26
|
+
"RERAISE", "RAISE_VARARGS",
|
|
27
|
+
"RETURN_VALUE", "RETURN_CONST", "RETURN_GENERATOR",
|
|
28
|
+
}
|
|
29
|
+
_HASJUMP = set(dis.hasjump) if hasattr(dis, "hasjump") else set()
|
|
30
|
+
_COND_JUMPS = {
|
|
31
|
+
"POP_JUMP_IF_FALSE", "POP_JUMP_IF_TRUE",
|
|
32
|
+
"POP_JUMP_FORWARD_IF_FALSE", "POP_JUMP_FORWARD_IF_TRUE",
|
|
33
|
+
"POP_JUMP_BACKWARD_IF_FALSE", "POP_JUMP_BACKWARD_IF_TRUE",
|
|
34
|
+
"JUMP_IF_FALSE_OR_POP", "JUMP_IF_TRUE_OR_POP",
|
|
35
|
+
"JUMP_IF_TRUE", "JUMP_IF_FALSE", "JUMP_IF_NOT_EXC_MATCH",
|
|
36
|
+
"FOR_ITER", "SEND",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# cross-version aliases: canonical -> set of spellings
|
|
40
|
+
ALIASES = {
|
|
41
|
+
"BINARY_ADD": {"BINARY_ADD", "BINARY_OP"},
|
|
42
|
+
"LOAD_METHOD": {"LOAD_METHOD", "LOAD_ATTR"},
|
|
43
|
+
"CALL": {"CALL", "CALL_FUNCTION", "CALL_FUNCTION_EX", "CALL_METHOD", "CALL_KW", "PRECALL"},
|
|
44
|
+
"RETURN": {"RETURN_VALUE", "RETURN_CONST", "RETURN_GENERATOR"},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class Inst:
|
|
50
|
+
offset: int
|
|
51
|
+
opname: str
|
|
52
|
+
opcode: int
|
|
53
|
+
arg: Any = None
|
|
54
|
+
argval: Any = None
|
|
55
|
+
argrepr: str = ""
|
|
56
|
+
starts_line: Any = None
|
|
57
|
+
is_jump_target: bool = False
|
|
58
|
+
positions: Any = None
|
|
59
|
+
index: int = 0
|
|
60
|
+
stack_depth_before: int = 0
|
|
61
|
+
stack_effect: int = 0
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def is_jump(self) -> bool:
|
|
65
|
+
return self.opname in _JUMP_OPS or self.opcode in _HASJUMP
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def is_cond_jump(self) -> bool:
|
|
69
|
+
return self.opname in _COND_JUMPS
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def is_uncond_jump(self) -> bool:
|
|
73
|
+
return self.is_jump and not self.is_cond_jump and self.opname not in (
|
|
74
|
+
"FOR_ITER", "SEND", "SETUP_FINALLY", "SETUP_EXCEPT", "SETUP_WITH",
|
|
75
|
+
"SETUP_LOOP", "BEFORE_WITH", "RERAISE", "RAISE_VARARGS",
|
|
76
|
+
"RETURN_VALUE", "RETURN_CONST", "RETURN_GENERATOR",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def is_return(self) -> bool:
|
|
81
|
+
return self.opname in ("RETURN_VALUE", "RETURN_CONST", "RETURN_GENERATOR")
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def jump_target(self):
|
|
85
|
+
return self.argval if self.is_jump and isinstance(self.argval, int) else None
|
|
86
|
+
|
|
87
|
+
def canonical(self) -> str:
|
|
88
|
+
for canon, spellings in ALIASES.items():
|
|
89
|
+
if self.opname in spellings:
|
|
90
|
+
return canon
|
|
91
|
+
return self.opname
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class EditableBytecode:
|
|
96
|
+
code: CodeType
|
|
97
|
+
insts: list = field(default_factory=list)
|
|
98
|
+
consts: tuple = ()
|
|
99
|
+
names: tuple = ()
|
|
100
|
+
varnames: tuple = ()
|
|
101
|
+
cellvars: tuple = ()
|
|
102
|
+
freevars: tuple = ()
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def from_code(cls, code: CodeType) -> "EditableBytecode":
|
|
106
|
+
raw = list(dis.get_instructions(code))
|
|
107
|
+
insts: list[Inst] = []
|
|
108
|
+
depth = 0
|
|
109
|
+
for i, ins in enumerate(raw):
|
|
110
|
+
try:
|
|
111
|
+
eff = dis.stack_effect(ins.opcode, ins.arg if ins.arg is not None else 0)
|
|
112
|
+
except Exception:
|
|
113
|
+
eff = 0
|
|
114
|
+
insts.append(Inst(
|
|
115
|
+
offset=ins.offset, opname=ins.opname, opcode=ins.opcode,
|
|
116
|
+
arg=ins.arg, argval=ins.argval, argrepr=ins.argrepr or "",
|
|
117
|
+
starts_line=ins.starts_line, is_jump_target=bool(ins.is_jump_target),
|
|
118
|
+
positions=getattr(ins, "positions", None),
|
|
119
|
+
index=i, stack_depth_before=depth, stack_effect=eff,
|
|
120
|
+
))
|
|
121
|
+
depth = max(0, depth + eff)
|
|
122
|
+
# 3.11+ KW_NAMES references co_consts by index; resolve the tuple now
|
|
123
|
+
# so calls below can pair keyword arguments (dis leaves it unknown)
|
|
124
|
+
for _ins in insts:
|
|
125
|
+
if _ins.opname == "KW_NAMES" and isinstance(_ins.arg, int):
|
|
126
|
+
try:
|
|
127
|
+
_kc = code.co_consts[_ins.arg]
|
|
128
|
+
except Exception:
|
|
129
|
+
_kc = None
|
|
130
|
+
if isinstance(_kc, tuple):
|
|
131
|
+
_ins.argval = _kc
|
|
132
|
+
return cls(code=code, insts=insts, consts=code.co_consts,
|
|
133
|
+
names=code.co_names, varnames=code.co_varnames,
|
|
134
|
+
cellvars=getattr(code, "co_cellvars", ()),
|
|
135
|
+
freevars=getattr(code, "co_freevars", ()))
|
|
136
|
+
|
|
137
|
+
def __len__(self):
|
|
138
|
+
return len(self.insts)
|
|
139
|
+
|
|
140
|
+
def opnames(self) -> list[str]:
|
|
141
|
+
return [ins.canonical() for ins in self.insts]
|
|
142
|
+
|
|
143
|
+
def jump_map(self) -> dict[int, int]:
|
|
144
|
+
"""offset -> index for jump resolution."""
|
|
145
|
+
return {ins.offset: ins.index for ins in self.insts}
|
|
146
|
+
|
|
147
|
+
def targets(self) -> set[int]:
|
|
148
|
+
out: set[int] = set()
|
|
149
|
+
m = self.jump_map()
|
|
150
|
+
for ins in self.insts:
|
|
151
|
+
t = ins.jump_target
|
|
152
|
+
if t is not None and t in m:
|
|
153
|
+
out.add(m[t])
|
|
154
|
+
return out
|
|
155
|
+
|
|
156
|
+
def metadata_table(self) -> dict:
|
|
157
|
+
c = self.code
|
|
158
|
+
return {
|
|
159
|
+
"co_name": c.co_name, "co_filename": c.co_filename,
|
|
160
|
+
"co_firstlineno": c.co_firstlineno, "co_argcount": c.co_argcount,
|
|
161
|
+
"co_posonlyargcount": getattr(c, "co_posonlyargcount", 0),
|
|
162
|
+
"co_kwonlyargcount": c.co_kwonlyargcount,
|
|
163
|
+
"co_nlocals": c.co_nlocals, "co_flags": c.co_flags,
|
|
164
|
+
"co_consts": [type(x).__name__ for x in c.co_consts],
|
|
165
|
+
"co_names": list(c.co_names), "co_varnames": list(c.co_varnames),
|
|
166
|
+
}
|