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,264 @@
1
+ """Conservative intra-procedural dependence and control facts over a neutral function IR.
2
+
3
+ Language front ends (Python via `ast`, Go via the instrumenter's `go/ast` walker) lower a
4
+ function to a small structured IR; this module is language-neutral. It establishes:
5
+
6
+ - **local def-use** by reaching definitions over structured code: every operand of every
7
+ decision, argument of every call and value of every field store is resolved to its
8
+ origins (`param:account.Balance`, `call:server.validAccount#0`, `global:settings.x`,
9
+ `closure:x` for a nested function reading its enclosing scope, `const`), or to
10
+ `unknown:<reason>` when the analysis cannot follow it;
11
+ - **control requirements**: for every call, store, return and decision, the conjunction of
12
+ (site, outcome) pairs that must hold to reach it. For structured code without goto or
13
+ labelled break this is the transitive control dependence a post-dominator computation
14
+ would give; early-exit guards (`if c { ...; return }`) make the rest of the block
15
+ require `c = false`. Loops, exception handlers and constructs a front end marks opaque
16
+ add an explicit unknown requirement instead of a guess;
17
+ - **decision order**: the pre-order position of every site, and which sites require which.
18
+
19
+ IR (JSON-able dicts). Function: {symbol, file, params, body}. Statements carry `k`, `line`
20
+ and, as applicable: `defs` (local names), `stores` (field/element paths written), `uses`
21
+ (paths read), `calls` ([{callee, args: [[paths]], line, addr: [locals whose address is
22
+ passed]}]), `site`/`span`/`pred`/`then`/`else` (if), `body` (loop, with, try), `handlers`,
23
+ `final` (try), `reason`/`blocks` (opaque). Kinds: assign, expr, if, return, raise, loop,
24
+ try, with, opaque, break.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from dataclasses import dataclass, field
30
+ from typing import Any
31
+
32
+ UNKNOWN = "UNKNOWN_DEPENDENCE"
33
+
34
+ Env = dict[str, frozenset[str]]
35
+ Req = tuple[tuple[str, bool | None], ...] # (site, outcome); site "?reason" with None = unknown
36
+
37
+
38
+ @dataclass
39
+ class Facts:
40
+ symbol: str
41
+ file: str
42
+ params: list[str]
43
+ sites: list[dict[str, Any]] = field(default_factory=list)
44
+ calls: list[dict[str, Any]] = field(default_factory=list)
45
+ stores: list[dict[str, Any]] = field(default_factory=list)
46
+ returns: list[dict[str, Any]] = field(default_factory=list)
47
+ unknown: list[dict[str, Any]] = field(default_factory=list)
48
+
49
+ def to_json(self) -> dict[str, Any]:
50
+ return {
51
+ "symbol": self.symbol,
52
+ "file": self.file,
53
+ "params": self.params,
54
+ "sites": self.sites,
55
+ "calls": self.calls,
56
+ "stores": self.stores,
57
+ "returns": self.returns,
58
+ "unknown": self.unknown,
59
+ }
60
+
61
+
62
+ def _req(r: Req) -> list[list[Any]]:
63
+ return [[s, o] for s, o in r]
64
+
65
+
66
+ class _Analyzer:
67
+ def __init__(self, fn: dict[str, Any]) -> None:
68
+ self.params = list(fn.get("params") or [])
69
+ self.closure = set(fn.get("closure") or []) # names bound by enclosing functions
70
+ self.facts = Facts(fn["symbol"], fn["file"], self.params)
71
+ self.order = 0
72
+
73
+ # ---- origins
74
+
75
+ def resolve(self, path: str, env: Env) -> frozenset[str]:
76
+ if not path or path == "?":
77
+ return frozenset({f"unknown:{UNKNOWN}:complex-expression"})
78
+ base, _, rest = path.partition(".")
79
+ suffix = f".{rest}" if rest else ""
80
+ if base in env:
81
+ out = set()
82
+ for o in env[base]:
83
+ if o.startswith(("unknown:", "const")):
84
+ out.add(o)
85
+ else:
86
+ out.add(o + suffix)
87
+ return frozenset(out)
88
+ if base in self.params:
89
+ return frozenset({f"param:{path}"})
90
+ if base in self.closure:
91
+ return frozenset({f"closure:{path}"})
92
+ return frozenset({f"global:{path}"})
93
+
94
+ def origins(self, paths: list[str], env: Env) -> list[str]:
95
+ out: set[str] = set()
96
+ for p in paths:
97
+ out |= self.resolve(p, env)
98
+ return sorted(out)
99
+
100
+ # ---- walk
101
+
102
+ def call_facts(self, calls: list[dict[str, Any]], env: Env, req: Req, line: int) -> Env:
103
+ for c in calls:
104
+ self.facts.calls.append(
105
+ {
106
+ "callee": c.get("callee", "?"),
107
+ "line": c.get("line", line),
108
+ "requires": _req(req),
109
+ "arg_origins": [self.origins(a, env) for a in c.get("args") or []],
110
+ }
111
+ )
112
+ # a local whose address is passed may be written by the callee
113
+ new = dict(env)
114
+ for c in calls:
115
+ for local in c.get("addr") or []:
116
+ new[local] = env.get(local, frozenset()) | {
117
+ f"unknown:{UNKNOWN}:address-passed-to:{c.get('callee', '?')}"
118
+ }
119
+ return new
120
+
121
+ def assign(self, st: dict[str, Any], env: Env, req: Req) -> Env:
122
+ env = self.call_facts(st.get("calls") or [], env, req, st["line"])
123
+ calls = st.get("calls") or []
124
+ defs = st.get("defs") or []
125
+ value = set(self.origins(st.get("uses") or [], env))
126
+ if len(calls) == 1 and st.get("value_is_call"):
127
+ callee = calls[0].get("callee", "?")
128
+ if len(defs) > 1:
129
+ env = dict(env)
130
+ for i, d in enumerate(defs):
131
+ if d != "_": # Go's blank identifier holds a position, binds nothing
132
+ env[d] = frozenset({f"call:{callee}#{i}"})
133
+ defs = []
134
+ else:
135
+ value = {f"call:{callee}"}
136
+ elif calls:
137
+ value |= {f"call:{c.get('callee', '?')}" for c in calls}
138
+ if not value and not st.get("uses"):
139
+ value = {"const"}
140
+ new = dict(env)
141
+ for d in defs:
142
+ if d != "_":
143
+ new[d] = frozenset(value)
144
+ for s in st.get("stores") or []:
145
+ self.facts.stores.append(
146
+ {"path": s, "line": st["line"], "requires": _req(req), "origins": sorted(value)}
147
+ )
148
+ return new
149
+
150
+ def block(self, stmts: list[dict[str, Any]], env: Env, req: Req) -> tuple[Env, bool]:
151
+ for st in stmts:
152
+ k = st.get("k")
153
+ if k in ("assign", "expr"):
154
+ env = self.assign(st, env, req)
155
+ elif k in ("return", "raise"):
156
+ env = self.call_facts(st.get("calls") or [], env, req, st["line"])
157
+ if k == "return":
158
+ self.facts.returns.append(
159
+ {
160
+ "line": st["line"],
161
+ "requires": _req(req),
162
+ "origins": self.origins(st.get("uses") or [], env)
163
+ + [f"call:{c.get('callee', '?')}" for c in st.get("calls") or []],
164
+ }
165
+ )
166
+ return env, True
167
+ elif k == "break":
168
+ return env, True # leaves the enclosing loop body
169
+ elif k == "if":
170
+ env = self.call_facts(st.get("calls") or [], env, req, st["line"])
171
+ site = st["site"]
172
+ rec = {
173
+ "site": site,
174
+ "span": st.get("span"),
175
+ "pred": st.get("pred", ""),
176
+ "line": st["line"],
177
+ "order": self.order,
178
+ "requires": _req(req),
179
+ "operands": [
180
+ {"path": p, "origins": sorted(self.resolve(p, env))}
181
+ for p in st.get("uses") or []
182
+ ],
183
+ "operand_calls": [c.get("callee", "?") for c in st.get("calls") or []],
184
+ }
185
+ self.order += 1
186
+ self.facts.sites.append(rec)
187
+ t_env, t_exit = self.block(st.get("then") or [], dict(env), (*req, (site, True)))
188
+ e_env, e_exit = self.block(st.get("else") or [], dict(env), (*req, (site, False)))
189
+ rec["then_exits"], rec["else_exits"] = t_exit, e_exit
190
+ if t_exit and e_exit:
191
+ return env, True
192
+ if t_exit:
193
+ env, req = e_env, (*req, (site, False))
194
+ elif e_exit:
195
+ env, req = t_env, (*req, (site, True))
196
+ else:
197
+ env = _join(t_env, e_env)
198
+ elif k == "loop":
199
+ env = self.call_facts(st.get("calls") or [], env, req, st["line"])
200
+ inner = dict(env)
201
+ for d in st.get("defs") or []:
202
+ inner[d] = frozenset({f"unknown:{UNKNOWN}:loop-variable"})
203
+ b_env, _ = self.block(st.get("body") or [], inner, (*req, ("?loop", None)))
204
+ env = _join(env, _taint(b_env, env, "assigned-in-loop"))
205
+ elif k == "with":
206
+ env = self.call_facts(st.get("calls") or [], env, req, st["line"])
207
+ env = dict(env)
208
+ for d in st.get("defs") or []:
209
+ env[d] = frozenset(
210
+ {f"call:{c.get('callee', '?')}" for c in st.get("calls") or []}
211
+ ) or frozenset({"const"})
212
+ env, exited = self.block(st.get("body") or [], env, req)
213
+ if exited:
214
+ return env, True
215
+ elif k == "try":
216
+ b_env, _ = self.block(st.get("body") or [], dict(env), req)
217
+ h_envs = [
218
+ self.block(h, dict(env), (*req, ("?exception-handler", None)))[0]
219
+ for h in st.get("handlers") or []
220
+ ]
221
+ env = _join(b_env, *h_envs) if h_envs else b_env
222
+ env, _ = self.block(st.get("final") or [], env, req)
223
+ else: # opaque: switch/select/match/goto/nested defs
224
+ self.facts.unknown.append({"line": st.get("line"), "reason": st.get("reason", k)})
225
+ for blk in st.get("blocks") or []:
226
+ b_env, _ = self.block(blk, dict(env), (*req, (f"?{st.get('reason', k)}", None)))
227
+ env = _join(env, _taint(b_env, env, st.get("reason", "opaque")))
228
+ return env, False
229
+
230
+
231
+ def _join(*envs: Env) -> Env:
232
+ out: dict[str, frozenset[str]] = {}
233
+ for e in envs:
234
+ for k, v in e.items():
235
+ out[k] = out.get(k, frozenset()) | v
236
+ return out
237
+
238
+
239
+ def _taint(inner: Env, outer: Env, reason: str) -> Env:
240
+ """Definitions made inside a loop or opaque region may or may not have happened."""
241
+ out = dict(inner)
242
+ for k, v in inner.items():
243
+ if outer.get(k) != v:
244
+ out[k] = v | {f"unknown:{UNKNOWN}:{reason}"}
245
+ return out
246
+
247
+
248
+ def analyze(fn: dict[str, Any]) -> Facts:
249
+ a = _Analyzer(fn)
250
+ a.block(fn.get("body") or [], {}, ())
251
+ return a.facts
252
+
253
+
254
+ def analyze_all(functions: list[dict[str, Any]]) -> list[dict[str, Any]]:
255
+ return [analyze(f).to_json() for f in functions]
256
+
257
+
258
+ def requirement_of(facts: dict[str, Any], callee_suffix: str) -> list[list[list[Any]]]:
259
+ """The control requirements of every call in `facts` whose callee ends with the suffix."""
260
+ return [
261
+ c["requires"]
262
+ for c in facts["calls"]
263
+ if c["callee"] == callee_suffix or c["callee"].endswith("." + callee_suffix)
264
+ ]