diffgenome 0.1.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.
- diffgenome/__init__.py +7 -0
- diffgenome/__main__.py +240 -0
- diffgenome/_collectors/go/dg/dg.go +623 -0
- diffgenome/_collectors/go/go.mod +3 -0
- diffgenome/_collectors/go/instrument/facts.go +346 -0
- diffgenome/_collectors/go/instrument/main.go +484 -0
- diffgenome/_collectors/node/instrument.js +289 -0
- diffgenome/_collectors/node/jest-setup.js +40 -0
- diffgenome/_collectors/node/package-lock.json +35 -0
- diffgenome/_collectors/node/package.json +11 -0
- diffgenome/_collectors/node/runtime.js +426 -0
- diffgenome/ambiguity.py +122 -0
- diffgenome/api.py +67 -0
- diffgenome/change.py +86 -0
- diffgenome/change_artifact.py +310 -0
- diffgenome/collect/__init__.py +2 -0
- diffgenome/collect/go_test.py +271 -0
- diffgenome/collect/node_jest.py +319 -0
- diffgenome/collect/py_monitoring.py +985 -0
- diffgenome/collect/py_runtime.py +116 -0
- diffgenome/collect/py_symbols.py +238 -0
- diffgenome/collect/pytest_plugin.py +130 -0
- diffgenome/compose.py +469 -0
- diffgenome/dependence.py +264 -0
- diffgenome/evaluate.py +669 -0
- diffgenome/frontends/__init__.py +0 -0
- diffgenome/frontends/python_ir.py +335 -0
- diffgenome/genome.py +1016 -0
- diffgenome/genome_pipeline.py +674 -0
- diffgenome/genome_prompt.py +33 -0
- diffgenome/genome_state.py +2118 -0
- diffgenome/graph.py +426 -0
- diffgenome/llm.py +189 -0
- diffgenome/model.py +364 -0
- diffgenome/mvp.py +398 -0
- diffgenome/probe.py +509 -0
- diffgenome/projection.py +308 -0
- diffgenome/py.typed +0 -0
- diffgenome/render.py +118 -0
- diffgenome/report.py +363 -0
- diffgenome/resolve.py +37 -0
- diffgenome/runtime.py +74 -0
- diffgenome/runtime_evidence.py +261 -0
- diffgenome/sandbox.py +166 -0
- diffgenome/serialize.py +96 -0
- diffgenome/sites.py +19 -0
- diffgenome/static_types.py +69 -0
- diffgenome/structure.py +462 -0
- diffgenome-0.1.0.dist-info/METADATA +139 -0
- diffgenome-0.1.0.dist-info/RECORD +53 -0
- diffgenome-0.1.0.dist-info/WHEEL +4 -0
- diffgenome-0.1.0.dist-info/entry_points.txt +2 -0
- diffgenome-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""Python front end: lower functions to the neutral IR of `diffgenome.dependence`.
|
|
2
|
+
|
|
3
|
+
Only what the conservative analysis needs. Anything else becomes an `opaque` statement
|
|
4
|
+
with a reason, never a silently dropped one.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from diffgenome.sites import site_id
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _path(e: ast.expr) -> str | None:
|
|
16
|
+
"""Dotted access path of a Name/Attribute chain, else None."""
|
|
17
|
+
if isinstance(e, ast.Name):
|
|
18
|
+
return e.id
|
|
19
|
+
if isinstance(e, ast.Attribute):
|
|
20
|
+
base = _path(e.value)
|
|
21
|
+
return f"{base}.{e.attr}" if base else None
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _Expr(ast.NodeVisitor):
|
|
26
|
+
"""Collect maximal read paths and calls of one expression."""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self.uses: list[str] = []
|
|
30
|
+
self.calls: list[dict[str, Any]] = []
|
|
31
|
+
|
|
32
|
+
def visit_Attribute(self, node: ast.Attribute) -> None:
|
|
33
|
+
p = _path(node)
|
|
34
|
+
if p:
|
|
35
|
+
self.uses.append(p)
|
|
36
|
+
else:
|
|
37
|
+
self.generic_visit(node)
|
|
38
|
+
|
|
39
|
+
def visit_Name(self, node: ast.Name) -> None:
|
|
40
|
+
self.uses.append(node.id)
|
|
41
|
+
|
|
42
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
43
|
+
func = node.func
|
|
44
|
+
if isinstance(func, ast.Attribute):
|
|
45
|
+
recv = _path(func.value)
|
|
46
|
+
if recv:
|
|
47
|
+
self.uses.append(recv)
|
|
48
|
+
else:
|
|
49
|
+
self.visit(func.value)
|
|
50
|
+
args = []
|
|
51
|
+
for a in [*node.args, *(k.value for k in node.keywords)]:
|
|
52
|
+
sub = _Expr()
|
|
53
|
+
sub.visit(a)
|
|
54
|
+
args.append(sub.uses or ["?"])
|
|
55
|
+
self.uses.extend(sub.uses)
|
|
56
|
+
self.calls.extend(sub.calls)
|
|
57
|
+
self.calls.append({"callee": ast.unparse(func), "args": args, "line": node.lineno})
|
|
58
|
+
|
|
59
|
+
def visit_Lambda(self, node: ast.Lambda) -> None:
|
|
60
|
+
return # a nested function body is not this function's dataflow
|
|
61
|
+
|
|
62
|
+
def _comprehension(
|
|
63
|
+
self, node: ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Comprehension targets are bound by the comprehension, not read from the
|
|
66
|
+
enclosing scope: their reads are dropped; the iterables' reads remain."""
|
|
67
|
+
bound = {
|
|
68
|
+
n.id for g in node.generators for n in ast.walk(g.target) if isinstance(n, ast.Name)
|
|
69
|
+
}
|
|
70
|
+
inner = _Expr()
|
|
71
|
+
for child in ast.iter_child_nodes(node):
|
|
72
|
+
inner.visit(child)
|
|
73
|
+
self.uses.extend(u for u in inner.uses if u.split(".", 1)[0] not in bound)
|
|
74
|
+
for c in inner.calls: # an argument that is a comprehension target is an element
|
|
75
|
+
c["args"] = [
|
|
76
|
+
[u for u in a if u.split(".", 1)[0] not in bound] or ["?"] for a in c["args"]
|
|
77
|
+
]
|
|
78
|
+
self.calls.append(c)
|
|
79
|
+
|
|
80
|
+
visit_ListComp = _comprehension
|
|
81
|
+
visit_SetComp = _comprehension
|
|
82
|
+
visit_DictComp = _comprehension
|
|
83
|
+
visit_GeneratorExp = _comprehension
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _expr(e: ast.expr | None) -> tuple[list[str], list[dict[str, Any]]]:
|
|
87
|
+
if e is None:
|
|
88
|
+
return [], []
|
|
89
|
+
v = _Expr()
|
|
90
|
+
v.visit(e)
|
|
91
|
+
return sorted(set(v.uses)), v.calls
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _targets(t: ast.expr) -> tuple[list[str], list[str]]:
|
|
95
|
+
"""(local defs, stored paths) of an assignment target."""
|
|
96
|
+
if isinstance(t, ast.Name):
|
|
97
|
+
return [t.id], []
|
|
98
|
+
if isinstance(t, ast.Tuple | ast.List):
|
|
99
|
+
defs: list[str] = []
|
|
100
|
+
stores: list[str] = []
|
|
101
|
+
for e in t.elts:
|
|
102
|
+
d, s = _targets(e)
|
|
103
|
+
defs += d
|
|
104
|
+
stores += s
|
|
105
|
+
return defs, stores
|
|
106
|
+
if isinstance(t, ast.Attribute):
|
|
107
|
+
return [], [_path(t) or "?"]
|
|
108
|
+
if isinstance(t, ast.Subscript):
|
|
109
|
+
return [], [(_path(t.value) or "?") + "[]"]
|
|
110
|
+
if isinstance(t, ast.Starred):
|
|
111
|
+
return _targets(t.value)
|
|
112
|
+
return [], ["?"]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class _Lower:
|
|
116
|
+
def __init__(self, rel_path: str) -> None:
|
|
117
|
+
self.rel = rel_path
|
|
118
|
+
|
|
119
|
+
def block(self, stmts: list[ast.stmt]) -> list[dict[str, Any]]:
|
|
120
|
+
out = []
|
|
121
|
+
for s in stmts:
|
|
122
|
+
out.extend(self.stmt(s))
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
def stmt(self, s: ast.stmt) -> list[dict[str, Any]]:
|
|
126
|
+
line = s.lineno
|
|
127
|
+
if isinstance(s, ast.Assign | ast.AnnAssign | ast.AugAssign):
|
|
128
|
+
targets = s.targets if isinstance(s, ast.Assign) else [s.target]
|
|
129
|
+
value = s.value
|
|
130
|
+
uses, calls = _expr(value)
|
|
131
|
+
defs: list[str] = []
|
|
132
|
+
stores: list[str] = []
|
|
133
|
+
for t in targets:
|
|
134
|
+
d, st = _targets(t)
|
|
135
|
+
defs += d
|
|
136
|
+
stores += st
|
|
137
|
+
if isinstance(s, ast.AugAssign):
|
|
138
|
+
uses = sorted(set(uses) | set(defs) | {p for p in stores if p != "?"})
|
|
139
|
+
unwrapped = value.value if isinstance(value, ast.Await) else value
|
|
140
|
+
return [
|
|
141
|
+
{
|
|
142
|
+
"k": "assign", "line": line, "defs": defs, "stores": stores, "uses": uses,
|
|
143
|
+
"calls": calls, "value_is_call": isinstance(unwrapped, ast.Call),
|
|
144
|
+
}
|
|
145
|
+
] # fmt: skip
|
|
146
|
+
if isinstance(s, ast.Expr):
|
|
147
|
+
uses, calls = _expr(s.value)
|
|
148
|
+
return [{"k": "expr", "line": line, "uses": uses, "calls": calls}]
|
|
149
|
+
if isinstance(s, ast.If):
|
|
150
|
+
t = s.test
|
|
151
|
+
span = (
|
|
152
|
+
t.lineno,
|
|
153
|
+
t.col_offset + 1,
|
|
154
|
+
t.end_lineno or t.lineno,
|
|
155
|
+
(t.end_col_offset or 0) + 1,
|
|
156
|
+
)
|
|
157
|
+
uses, calls = _expr(t)
|
|
158
|
+
return [
|
|
159
|
+
{
|
|
160
|
+
"k": "if", "line": line, "site": site_id(self.rel, span), "span": list(span),
|
|
161
|
+
"pred": ast.unparse(t), "uses": uses, "calls": calls,
|
|
162
|
+
"then": self.block(s.body), "else": self.block(s.orelse),
|
|
163
|
+
}
|
|
164
|
+
] # fmt: skip
|
|
165
|
+
if isinstance(s, ast.Return | ast.Raise):
|
|
166
|
+
e = s.value if isinstance(s, ast.Return) else s.exc
|
|
167
|
+
uses, calls = _expr(e)
|
|
168
|
+
return [
|
|
169
|
+
{
|
|
170
|
+
"k": "return" if isinstance(s, ast.Return) else "raise",
|
|
171
|
+
"line": line,
|
|
172
|
+
"uses": uses,
|
|
173
|
+
"calls": calls,
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
if isinstance(s, ast.For | ast.AsyncFor | ast.While):
|
|
177
|
+
it = s.iter if isinstance(s, ast.For | ast.AsyncFor) else s.test
|
|
178
|
+
uses, calls = _expr(it)
|
|
179
|
+
defs = _targets(s.target)[0] if isinstance(s, ast.For | ast.AsyncFor) else []
|
|
180
|
+
return [
|
|
181
|
+
{"k": "loop", "line": line, "uses": uses, "calls": calls, "defs": defs,
|
|
182
|
+
"body": self.block(s.body + s.orelse)}
|
|
183
|
+
] # fmt: skip
|
|
184
|
+
if isinstance(s, ast.With | ast.AsyncWith):
|
|
185
|
+
w_uses: list[str] = []
|
|
186
|
+
w_calls: list[dict[str, Any]] = []
|
|
187
|
+
w_defs: list[str] = []
|
|
188
|
+
for item in s.items:
|
|
189
|
+
u, c = _expr(item.context_expr)
|
|
190
|
+
w_uses += u
|
|
191
|
+
w_calls += c
|
|
192
|
+
if item.optional_vars is not None:
|
|
193
|
+
w_defs += _targets(item.optional_vars)[0]
|
|
194
|
+
return [
|
|
195
|
+
{
|
|
196
|
+
"k": "with",
|
|
197
|
+
"line": line,
|
|
198
|
+
"uses": w_uses,
|
|
199
|
+
"calls": w_calls,
|
|
200
|
+
"defs": w_defs,
|
|
201
|
+
"body": self.block(s.body),
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
if isinstance(s, ast.Try | ast.TryStar):
|
|
205
|
+
handlers = [self.block(h.body) for h in s.handlers]
|
|
206
|
+
return [
|
|
207
|
+
{"k": "try", "line": line, "body": self.block(s.body + s.orelse),
|
|
208
|
+
"handlers": handlers, "final": self.block(s.finalbody)}
|
|
209
|
+
] # fmt: skip
|
|
210
|
+
if isinstance(s, ast.Break | ast.Continue):
|
|
211
|
+
return [{"k": "break", "line": line}]
|
|
212
|
+
if isinstance(s, ast.Pass | ast.Global | ast.Nonlocal | ast.Import | ast.ImportFrom):
|
|
213
|
+
return []
|
|
214
|
+
if isinstance(s, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef):
|
|
215
|
+
# A nested definition binds a local name; its body is lowered as a function of
|
|
216
|
+
# its own (`lower_functions`), not as this function's dataflow. Decorators and
|
|
217
|
+
# default values are evaluated here.
|
|
218
|
+
d_uses: list[str] = []
|
|
219
|
+
d_calls: list[dict[str, Any]] = []
|
|
220
|
+
evaluated = list(s.decorator_list)
|
|
221
|
+
if not isinstance(s, ast.ClassDef):
|
|
222
|
+
evaluated += [*s.args.defaults, *(d for d in s.args.kw_defaults if d)]
|
|
223
|
+
for e in evaluated:
|
|
224
|
+
u, c = _expr(e)
|
|
225
|
+
d_uses += u
|
|
226
|
+
d_calls += c
|
|
227
|
+
return [
|
|
228
|
+
{"k": "assign", "line": line, "defs": [s.name], "stores": [],
|
|
229
|
+
"uses": sorted(set(d_uses)), "calls": d_calls, "value_is_call": False}
|
|
230
|
+
] # fmt: skip
|
|
231
|
+
if isinstance(s, ast.Assert):
|
|
232
|
+
uses, calls = _expr(s.test)
|
|
233
|
+
return [{"k": "expr", "line": line, "uses": uses, "calls": calls}]
|
|
234
|
+
if isinstance(s, ast.Match):
|
|
235
|
+
return [
|
|
236
|
+
{
|
|
237
|
+
"k": "opaque",
|
|
238
|
+
"line": line,
|
|
239
|
+
"reason": "match",
|
|
240
|
+
"blocks": [self.block(c.body) for c in s.cases],
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
return [{"k": "opaque", "line": line, "reason": type(s).__name__}]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _params(n: ast.FunctionDef | ast.AsyncFunctionDef) -> list[str]:
|
|
247
|
+
a = n.args
|
|
248
|
+
params = [x.arg for x in [*a.posonlyargs, *a.args, *a.kwonlyargs]]
|
|
249
|
+
return params + [x.arg for x in (a.vararg, a.kwarg) if x is not None]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _bound_names(n: ast.FunctionDef | ast.AsyncFunctionDef) -> set[str]:
|
|
253
|
+
"""Names bound in a function's own scope: parameters, assignment/loop/with/except
|
|
254
|
+
targets, imports and nested definitions. Nested function bodies are not entered."""
|
|
255
|
+
names = set(_params(n))
|
|
256
|
+
|
|
257
|
+
def walk(node: ast.AST) -> None:
|
|
258
|
+
for ch in ast.iter_child_nodes(node):
|
|
259
|
+
if isinstance(ch, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef):
|
|
260
|
+
names.add(ch.name)
|
|
261
|
+
continue # its body is another scope
|
|
262
|
+
if isinstance(ch, ast.Lambda):
|
|
263
|
+
continue
|
|
264
|
+
if isinstance(ch, ast.Name) and isinstance(ch.ctx, ast.Store):
|
|
265
|
+
names.add(ch.id)
|
|
266
|
+
elif isinstance(ch, ast.ExceptHandler) and ch.name:
|
|
267
|
+
names.add(ch.name)
|
|
268
|
+
elif isinstance(ch, ast.alias):
|
|
269
|
+
names.add((ch.asname or ch.name).split(".")[0])
|
|
270
|
+
walk(ch)
|
|
271
|
+
|
|
272
|
+
for st in n.body:
|
|
273
|
+
if isinstance(st, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef):
|
|
274
|
+
names.add(st.name)
|
|
275
|
+
else:
|
|
276
|
+
walk(st)
|
|
277
|
+
return names
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def lower_functions(
|
|
281
|
+
source: str, rel_path: str, module: str, qualnames: list[str] | None = None
|
|
282
|
+
) -> list[dict[str, Any]]:
|
|
283
|
+
"""Lower every function (or the given qualnames) of one Python file, including nested
|
|
284
|
+
functions. Symbols use the collector's naming: `py:<module>.<qualname>`, where a nested
|
|
285
|
+
function's qualname is `<outer>.<locals>.<name>` (Python's `co_qualname`). A nested
|
|
286
|
+
function's `closure` lists the names bound by its enclosing functions; reads of them
|
|
287
|
+
resolve to `closure:<name>` rather than `global:<name>`."""
|
|
288
|
+
tree = ast.parse(source)
|
|
289
|
+
out: list[dict[str, Any]] = []
|
|
290
|
+
|
|
291
|
+
def visit(body: list[ast.stmt], prefix: str, closure: set[str]) -> None:
|
|
292
|
+
for n in body:
|
|
293
|
+
if isinstance(n, ast.ClassDef):
|
|
294
|
+
# a class body is not an enclosing scope for its methods
|
|
295
|
+
visit(n.body, f"{prefix}{n.name}.", closure)
|
|
296
|
+
elif isinstance(n, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
297
|
+
q = f"{prefix}{n.name}"
|
|
298
|
+
if qualnames is None or q in qualnames:
|
|
299
|
+
fn: dict[str, Any] = {
|
|
300
|
+
"symbol": f"py:{module}.{q}", "file": rel_path, "params": _params(n),
|
|
301
|
+
"body": _Lower(rel_path).block(n.body),
|
|
302
|
+
} # fmt: skip
|
|
303
|
+
if closure:
|
|
304
|
+
fn["closure"] = sorted(closure - set(_params(n)))
|
|
305
|
+
out.append(fn)
|
|
306
|
+
visit(n.body, f"{q}.<locals>.", closure | _bound_names(n))
|
|
307
|
+
elif isinstance(
|
|
308
|
+
n,
|
|
309
|
+
ast.If
|
|
310
|
+
| ast.For
|
|
311
|
+
| ast.AsyncFor
|
|
312
|
+
| ast.While
|
|
313
|
+
| ast.With
|
|
314
|
+
| ast.AsyncWith
|
|
315
|
+
| ast.Try
|
|
316
|
+
| ast.TryStar,
|
|
317
|
+
):
|
|
318
|
+
# definitions nested in compound statements (module-level `if TYPE_CHECKING:`
|
|
319
|
+
# or inside a function body) belong to the same scope
|
|
320
|
+
for blk in _blocks(n):
|
|
321
|
+
visit(blk, prefix, closure)
|
|
322
|
+
|
|
323
|
+
visit(tree.body, "", set())
|
|
324
|
+
return out
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _blocks(n: ast.stmt) -> list[list[ast.stmt]]:
|
|
328
|
+
out: list[list[ast.stmt]] = []
|
|
329
|
+
for attr in ("body", "orelse", "finalbody"):
|
|
330
|
+
blk = getattr(n, attr, None)
|
|
331
|
+
if blk:
|
|
332
|
+
out.append(blk)
|
|
333
|
+
for h in getattr(n, "handlers", []) or []:
|
|
334
|
+
out.append(h.body)
|
|
335
|
+
return out
|