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
diffgenome/dependence.py
ADDED
|
@@ -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
|
+
]
|