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.
Files changed (53) hide show
  1. diffgenome/__init__.py +7 -0
  2. diffgenome/__main__.py +240 -0
  3. diffgenome/_collectors/go/dg/dg.go +623 -0
  4. diffgenome/_collectors/go/go.mod +3 -0
  5. diffgenome/_collectors/go/instrument/facts.go +346 -0
  6. diffgenome/_collectors/go/instrument/main.go +484 -0
  7. diffgenome/_collectors/node/instrument.js +289 -0
  8. diffgenome/_collectors/node/jest-setup.js +40 -0
  9. diffgenome/_collectors/node/package-lock.json +35 -0
  10. diffgenome/_collectors/node/package.json +11 -0
  11. diffgenome/_collectors/node/runtime.js +426 -0
  12. diffgenome/ambiguity.py +122 -0
  13. diffgenome/api.py +67 -0
  14. diffgenome/change.py +86 -0
  15. diffgenome/change_artifact.py +310 -0
  16. diffgenome/collect/__init__.py +2 -0
  17. diffgenome/collect/go_test.py +271 -0
  18. diffgenome/collect/node_jest.py +319 -0
  19. diffgenome/collect/py_monitoring.py +985 -0
  20. diffgenome/collect/py_runtime.py +116 -0
  21. diffgenome/collect/py_symbols.py +238 -0
  22. diffgenome/collect/pytest_plugin.py +130 -0
  23. diffgenome/compose.py +469 -0
  24. diffgenome/dependence.py +264 -0
  25. diffgenome/evaluate.py +669 -0
  26. diffgenome/frontends/__init__.py +0 -0
  27. diffgenome/frontends/python_ir.py +335 -0
  28. diffgenome/genome.py +1016 -0
  29. diffgenome/genome_pipeline.py +674 -0
  30. diffgenome/genome_prompt.py +33 -0
  31. diffgenome/genome_state.py +2118 -0
  32. diffgenome/graph.py +426 -0
  33. diffgenome/llm.py +189 -0
  34. diffgenome/model.py +364 -0
  35. diffgenome/mvp.py +398 -0
  36. diffgenome/probe.py +509 -0
  37. diffgenome/projection.py +308 -0
  38. diffgenome/py.typed +0 -0
  39. diffgenome/render.py +118 -0
  40. diffgenome/report.py +363 -0
  41. diffgenome/resolve.py +37 -0
  42. diffgenome/runtime.py +74 -0
  43. diffgenome/runtime_evidence.py +261 -0
  44. diffgenome/sandbox.py +166 -0
  45. diffgenome/serialize.py +96 -0
  46. diffgenome/sites.py +19 -0
  47. diffgenome/static_types.py +69 -0
  48. diffgenome/structure.py +462 -0
  49. diffgenome-0.1.0.dist-info/METADATA +139 -0
  50. diffgenome-0.1.0.dist-info/RECORD +53 -0
  51. diffgenome-0.1.0.dist-info/WHEEL +4 -0
  52. diffgenome-0.1.0.dist-info/entry_points.txt +2 -0
  53. 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