cg-code-graph 0.10.1__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.
- cg_code_graph-0.10.1.dist-info/METADATA +678 -0
- cg_code_graph-0.10.1.dist-info/RECORD +174 -0
- cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
- cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
- cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
- cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
- codegraph/__init__.py +2 -0
- codegraph/aitools.py +129 -0
- codegraph/apps.py +76 -0
- codegraph/blindspots.py +428 -0
- codegraph/bridges.py +1701 -0
- codegraph/cli.py +725 -0
- codegraph/concepts.py +362 -0
- codegraph/config.py +559 -0
- codegraph/core/__init__.py +0 -0
- codegraph/core/cache.py +375 -0
- codegraph/core/detect.py +80 -0
- codegraph/core/extractors.py +187 -0
- codegraph/core/fsutil.py +61 -0
- codegraph/core/generated.py +575 -0
- codegraph/core/model.py +174 -0
- codegraph/core/paths.py +175 -0
- codegraph/core/plugin.py +160 -0
- codegraph/core/store.py +80 -0
- codegraph/core/syntax_errors.py +132 -0
- codegraph/coverage.py +928 -0
- codegraph/doctor.py +453 -0
- codegraph/external.py +613 -0
- codegraph/indexer.py +336 -0
- codegraph/link.py +434 -0
- codegraph/lint_async.py +524 -0
- codegraph/mcp_server.py +1303 -0
- codegraph/parity.py +473 -0
- codegraph/parity_structure.py +307 -0
- codegraph/payload.py +321 -0
- codegraph/plans.py +1285 -0
- codegraph/platform_scan.py +643 -0
- codegraph/platforms.py +1369 -0
- codegraph/plugins/__init__.py +0 -0
- codegraph/plugins/cfamily/__init__.py +0 -0
- codegraph/plugins/cfamily/plugin.py +930 -0
- codegraph/plugins/cfamily/syntax.py +881 -0
- codegraph/plugins/dart/__init__.py +0 -0
- codegraph/plugins/dart/bridges.py +345 -0
- codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
- codegraph/plugins/dart/extractor/pubspec.lock +149 -0
- codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
- codegraph/plugins/dart/http.py +904 -0
- codegraph/plugins/dart/models.py +308 -0
- codegraph/plugins/dart/plugin.py +625 -0
- codegraph/plugins/dart/program.py +907 -0
- codegraph/plugins/django/__init__.py +0 -0
- codegraph/plugins/django/extras.py +378 -0
- codegraph/plugins/django/models.py +508 -0
- codegraph/plugins/django/plugin.py +728 -0
- codegraph/plugins/django/schemas.py +339 -0
- codegraph/plugins/django/shapes.py +216 -0
- codegraph/plugins/django/urls.py +603 -0
- codegraph/plugins/express/__init__.py +0 -0
- codegraph/plugins/express/plugin.py +428 -0
- codegraph/plugins/flutter/__init__.py +0 -0
- codegraph/plugins/flutter/plugin.py +538 -0
- codegraph/plugins/kotlin/__init__.py +0 -0
- codegraph/plugins/kotlin/exact.py +457 -0
- codegraph/plugins/kotlin/plugin.py +1961 -0
- codegraph/plugins/kotlin/reparse.py +234 -0
- codegraph/plugins/laravel/__init__.py +0 -0
- codegraph/plugins/laravel/broadcast.py +351 -0
- codegraph/plugins/laravel/plugin.py +863 -0
- codegraph/plugins/laravel/tests.py +262 -0
- codegraph/plugins/laravel/values.py +728 -0
- codegraph/plugins/native/__init__.py +0 -0
- codegraph/plugins/native/gates.py +286 -0
- codegraph/plugins/native/runner.py +183 -0
- codegraph/plugins/native/scipread.py +194 -0
- codegraph/plugins/native/ts.py +54 -0
- codegraph/plugins/nest/__init__.py +0 -0
- codegraph/plugins/nest/plugin.py +654 -0
- codegraph/plugins/nextjs/__init__.py +0 -0
- codegraph/plugins/nextjs/plugin.py +336 -0
- codegraph/plugins/nuxt/__init__.py +0 -0
- codegraph/plugins/nuxt/plugin.py +308 -0
- codegraph/plugins/php/__init__.py +0 -0
- codegraph/plugins/php/extractor/composer.json +5 -0
- codegraph/plugins/php/extractor/composer.lock +76 -0
- codegraph/plugins/php/extractor/extract.php +743 -0
- codegraph/plugins/php/gating.py +573 -0
- codegraph/plugins/php/plugin.py +668 -0
- codegraph/plugins/php/strings.py +197 -0
- codegraph/plugins/python/__init__.py +0 -0
- codegraph/plugins/python/aitools.py +664 -0
- codegraph/plugins/python/external.py +245 -0
- codegraph/plugins/python/fields.py +107 -0
- codegraph/plugins/python/plugin.py +1733 -0
- codegraph/plugins/python/refs.py +485 -0
- codegraph/plugins/python/roots.py +412 -0
- codegraph/plugins/python/socketio.py +210 -0
- codegraph/plugins/python/subproc.py +864 -0
- codegraph/plugins/python/tests.py +1040 -0
- codegraph/plugins/python/values.py +179 -0
- codegraph/plugins/pyweb/__init__.py +0 -0
- codegraph/plugins/pyweb/plugin.py +1334 -0
- codegraph/plugins/pyweb/values.py +68 -0
- codegraph/plugins/rust/__init__.py +0 -0
- codegraph/plugins/rust/cargo.py +226 -0
- codegraph/plugins/rust/plugin.py +980 -0
- codegraph/plugins/rust/syntax.py +678 -0
- codegraph/plugins/scip/__init__.py +0 -0
- codegraph/plugins/scip/importer.py +129 -0
- codegraph/plugins/scip/scip.proto +962 -0
- codegraph/plugins/scip/scip_pb2.py +97 -0
- codegraph/plugins/stubs/__init__.py +0 -0
- codegraph/plugins/stubs/plugins.py +38 -0
- codegraph/plugins/swift/__init__.py +0 -0
- codegraph/plugins/swift/baseurl.py +109 -0
- codegraph/plugins/swift/exact.py +415 -0
- codegraph/plugins/swift/indexstore.py +209 -0
- codegraph/plugins/swift/packages.py +174 -0
- codegraph/plugins/swift/plugin.py +2890 -0
- codegraph/plugins/ts/__init__.py +0 -0
- codegraph/plugins/ts/baseurl.py +185 -0
- codegraph/plugins/ts/extractor/extract.mjs +2652 -0
- codegraph/plugins/ts/extractor/fw.mjs +685 -0
- codegraph/plugins/ts/extractor/package-lock.json +205 -0
- codegraph/plugins/ts/extractor/package.json +9 -0
- codegraph/plugins/ts/plugin.py +480 -0
- codegraph/plugins/tsweb/__init__.py +0 -0
- codegraph/plugins/tsweb/common.py +290 -0
- codegraph/plugins/tsweb/data.py +276 -0
- codegraph/presets/__init__.py +146 -0
- codegraph/presets/c_cpp.yaml +9 -0
- codegraph/presets/common.yaml +66 -0
- codegraph/presets/dart.yaml +9 -0
- codegraph/presets/django-ninja.yaml +15 -0
- codegraph/presets/django.yaml +25 -0
- codegraph/presets/djangorestframework.yaml +17 -0
- codegraph/presets/express.yaml +17 -0
- codegraph/presets/kotlin.yaml +11 -0
- codegraph/presets/laravel.yaml +40 -0
- codegraph/presets/nest.yaml +11 -0
- codegraph/presets/nextjs.yaml +15 -0
- codegraph/presets/nuxt.yaml +9 -0
- codegraph/presets/php.yaml +5 -0
- codegraph/presets/python.yaml +10 -0
- codegraph/presets/rust.yaml +5 -0
- codegraph/presets/swift.yaml +10 -0
- codegraph/presets/typescript.yaml +13 -0
- codegraph/process_runs.py +328 -0
- codegraph/protocols/__init__.py +299 -0
- codegraph/protocols/builtin.py +67 -0
- codegraph/protocols/matchers.py +144 -0
- codegraph/protocols/view.py +334 -0
- codegraph/query.py +2089 -0
- codegraph/realtime.py +260 -0
- codegraph/roundtrip.py +346 -0
- codegraph/routes.py +442 -0
- codegraph/starters.py +218 -0
- codegraph/tests_index.py +117 -0
- codegraph/viz/__init__.py +0 -0
- codegraph/viz/graph.py +369 -0
- codegraph/viz/server.py +198 -0
- codegraph/viz/static/app.css +148 -0
- codegraph/viz/static/app.js +1082 -0
- codegraph/viz/static/index.html +81 -0
- codegraph/viz/static/layered.js +237 -0
- codegraph/viz/static/vendor/VERSIONS.txt +4 -0
- codegraph/viz/static/vendor/cose-base.js +3214 -0
- codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
- codegraph/viz/static/vendor/cytoscape.min.js +31 -0
- codegraph/viz/static/vendor/layout-base.js +5230 -0
- codegraph/viz/tools/package-lock.json +303 -0
- codegraph/viz/tools/package.json +7 -0
- codegraph/viz/tools/shoot.mjs +165 -0
- codegraph/xcode.py +251 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
"""Deterministic guard / gating analysis for PHP, computed from the source files alone.
|
|
2
|
+
|
|
3
|
+
Given a *gate scenario* (e.g. "new_inventory": the tenant's settings have
|
|
4
|
+
features.new_inventory.enabled = true),
|
|
5
|
+
an abstract interpreter walks each function's control skeleton (emitted by extract.php) and
|
|
6
|
+
marks the fact ranges that cannot execute under the scenario. Edges emitted from those facts
|
|
7
|
+
get `gate=<scenario>` and a guard evidence `file:line` (the predicate call that decided it).
|
|
8
|
+
|
|
9
|
+
Values are abstract: concrete PHP literals, TRUTHY / FALSY (known truthiness), or TOP (unknown).
|
|
10
|
+
Conditions become boolean formulas over unknown atoms (same canonical expression text = same
|
|
11
|
+
atom); a branch is dead when its path condition is unsatisfiable (brute-force over <=12 atoms)
|
|
12
|
+
*and* the decision depends on a scenario fact. Code that is dead regardless of the scenario is
|
|
13
|
+
never labelled gated.
|
|
14
|
+
|
|
15
|
+
Scenario facts are seeded from settings reads (`$x->getSetting('<key>')`, key may be a class
|
|
16
|
+
constant) and propagate through method summaries (a method whose every return evaluates to the
|
|
17
|
+
same value under the scenario becomes a derived predicate, e.g. FeatureGate::usesNewInventory).
|
|
18
|
+
Summaries are evaluated per literal-argument tuple (one level of call-site sensitivity, so
|
|
19
|
+
`browse($r, 'authors')` is evaluated with $action = 'authors').
|
|
20
|
+
|
|
21
|
+
Assumption "inputs present": a branch whose condition only tests a bare variable for
|
|
22
|
+
null/empty (`!$tenant`, `$x === null`, `is_null($x)`, `empty($x)`) while the variable's value is
|
|
23
|
+
unknown is treated as not taken (never marked dead). This lets `if (!$tenant) return false;`
|
|
24
|
+
prologues not spoil a predicate.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import itertools
|
|
29
|
+
import json
|
|
30
|
+
from collections import defaultdict
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _S:
|
|
34
|
+
def __init__(self, n):
|
|
35
|
+
self.n = n
|
|
36
|
+
|
|
37
|
+
def __repr__(self):
|
|
38
|
+
return self.n
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
TOP, TRUTHY, FALSY, EMPTY = _S("TOP"), _S("TRUTHY"), _S("FALSY"), _S("EMPTY")
|
|
42
|
+
NONE = frozenset()
|
|
43
|
+
NONNULL_FUNCS = {"response", "redirect", "view", "collect", "now", "today", "back", "url", "route", "app", "request"}
|
|
44
|
+
SCALAR_RET = {"bool", "int", "float", "string", "array", "mixed", "void", "null", "false", "true", "iterable", "callable", "never", "object", "static", "self"}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def truth(v):
|
|
48
|
+
if v is TOP:
|
|
49
|
+
return None
|
|
50
|
+
if v is TRUTHY:
|
|
51
|
+
return True
|
|
52
|
+
if v is FALSY or v is EMPTY:
|
|
53
|
+
return False
|
|
54
|
+
if isinstance(v, str):
|
|
55
|
+
return v not in ("", "0")
|
|
56
|
+
return bool(v)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def concrete(v):
|
|
60
|
+
return not isinstance(v, _S)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def join(vals):
|
|
64
|
+
vals = list(vals)
|
|
65
|
+
if not vals:
|
|
66
|
+
return (TOP, NONE)
|
|
67
|
+
why = frozenset().union(*(w for _, w in vals))
|
|
68
|
+
first = vals[0][0]
|
|
69
|
+
if all((concrete(v) == concrete(first)) and (v == first if concrete(v) else v is first) and type(v) is type(first) for v, _ in vals):
|
|
70
|
+
return (first, why)
|
|
71
|
+
ts = {truth(v) for v, _ in vals}
|
|
72
|
+
if ts == {True}:
|
|
73
|
+
return (TRUTHY, why)
|
|
74
|
+
if ts == {False}:
|
|
75
|
+
return (FALSY, why)
|
|
76
|
+
return (TOP, NONE)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---- boolean formulas over unknown atoms ----
|
|
80
|
+
# ("c", bool, why) | ("a", key) | ("n", f) | ("&", f, g) | ("|", f, g)
|
|
81
|
+
|
|
82
|
+
def f_atoms(f, out):
|
|
83
|
+
t = f[0]
|
|
84
|
+
if t == "a":
|
|
85
|
+
out.add(f[1])
|
|
86
|
+
elif t == "n":
|
|
87
|
+
f_atoms(f[1], out)
|
|
88
|
+
elif t in "&|":
|
|
89
|
+
f_atoms(f[1], out)
|
|
90
|
+
f_atoms(f[2], out)
|
|
91
|
+
return out
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def f_why(f, out=None):
|
|
95
|
+
out = set() if out is None else out
|
|
96
|
+
t = f[0]
|
|
97
|
+
if t == "c":
|
|
98
|
+
out |= f[2]
|
|
99
|
+
elif t == "n":
|
|
100
|
+
f_why(f[1], out)
|
|
101
|
+
elif t in "&|":
|
|
102
|
+
f_why(f[1], out)
|
|
103
|
+
f_why(f[2], out)
|
|
104
|
+
return out
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def f_eval(f, asg):
|
|
108
|
+
t = f[0]
|
|
109
|
+
if t == "c":
|
|
110
|
+
return f[1]
|
|
111
|
+
if t == "a":
|
|
112
|
+
return asg[f[1]]
|
|
113
|
+
if t == "n":
|
|
114
|
+
return not f_eval(f[1], asg)
|
|
115
|
+
if t == "&":
|
|
116
|
+
return f_eval(f[1], asg) and f_eval(f[2], asg)
|
|
117
|
+
return f_eval(f[1], asg) or f_eval(f[2], asg)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def f_sat(f, max_atoms=12):
|
|
121
|
+
atoms = sorted(f_atoms(f, set()))
|
|
122
|
+
if len(atoms) > max_atoms:
|
|
123
|
+
return True
|
|
124
|
+
for bits in itertools.product((False, True), repeat=len(atoms)):
|
|
125
|
+
if f_eval(f, dict(zip(atoms, bits))):
|
|
126
|
+
return True
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def f_and(fs):
|
|
131
|
+
out = ("c", True, NONE)
|
|
132
|
+
for f in fs:
|
|
133
|
+
out = ("&", out, f)
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def canon(e):
|
|
138
|
+
if isinstance(e, dict):
|
|
139
|
+
return "{" + ",".join(f"{k}:{canon(v)}" for k, v in sorted(e.items()) if k not in ("r", "f", "line")) + "}"
|
|
140
|
+
if isinstance(e, list):
|
|
141
|
+
return "[" + ",".join(canon(x) for x in e) + "]"
|
|
142
|
+
return json.dumps(e)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class Frame:
|
|
146
|
+
def __init__(self, fn, env, record):
|
|
147
|
+
self.fn, self.env, self.record = fn, env, record
|
|
148
|
+
self.returns = []
|
|
149
|
+
self.decisions = set() # whys of scenario-decided branches (control dependence)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class GateEvaluator:
|
|
153
|
+
def __init__(self, prog, scenario: dict):
|
|
154
|
+
self.prog = prog
|
|
155
|
+
self.name = scenario["name"]
|
|
156
|
+
self.accessors = {a.lower() for a in scenario.get("setting_accessors", ["getSetting"])}
|
|
157
|
+
self.true_keys = set(scenario.get("true_settings", []))
|
|
158
|
+
self.false_keys = set(scenario.get("false_settings", []))
|
|
159
|
+
self.memo = {}
|
|
160
|
+
self.active = set()
|
|
161
|
+
self.target_cache = {}
|
|
162
|
+
self.predicates = {}
|
|
163
|
+
self.stats = defaultdict(int)
|
|
164
|
+
|
|
165
|
+
# ---------- helpers ----------
|
|
166
|
+
def const_value(self, cls, name):
|
|
167
|
+
for a in self.prog.ancestors(cls) if cls else []:
|
|
168
|
+
c = self.prog.cls(a)
|
|
169
|
+
if not c:
|
|
170
|
+
continue
|
|
171
|
+
for k in c.consts:
|
|
172
|
+
if k["name"] == name:
|
|
173
|
+
return k.get("value")
|
|
174
|
+
return TOP
|
|
175
|
+
|
|
176
|
+
def targets(self, fr, fi):
|
|
177
|
+
key = (fr.fn.id, fi)
|
|
178
|
+
if key not in self.target_cache:
|
|
179
|
+
from .plugin import ResolveCtx
|
|
180
|
+
f = fr.fn.facts[fi]
|
|
181
|
+
self.target_cache[key] = self.prog.call_targets(fr.fn, f, ResolveCtx(self.prog, fr.fn))[0] if f.get("t") == "call" else []
|
|
182
|
+
return self.target_cache[key]
|
|
183
|
+
|
|
184
|
+
def mark(self, fr, r, why, lo_hi=None):
|
|
185
|
+
if why:
|
|
186
|
+
fr.decisions |= why
|
|
187
|
+
if not fr.record or not why or not r:
|
|
188
|
+
return
|
|
189
|
+
lo, hi = r
|
|
190
|
+
if hi <= lo:
|
|
191
|
+
return
|
|
192
|
+
w = sorted(why)[0]
|
|
193
|
+
site = w.split("@", 1)[1] if "@" in w else f"{fr.fn.file}"
|
|
194
|
+
info = {"gate": self.name, "guard": site, "expr": "; ".join(sorted(why))[:300], "src": fr.fn.id}
|
|
195
|
+
for i in range(lo, hi):
|
|
196
|
+
fr.fn.dead.setdefault(i, info)
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def span(stmts):
|
|
200
|
+
rs = [s.get("r") for s in stmts or [] if s.get("r")]
|
|
201
|
+
return (rs[0][0], rs[-1][1]) if rs else None
|
|
202
|
+
|
|
203
|
+
# ---------- summaries ----------
|
|
204
|
+
def summary(self, m, argvals, depth):
|
|
205
|
+
key = (m.id, tuple(v if concrete(v) and not isinstance(v, (list, dict)) else repr(v) for v, _ in argvals))
|
|
206
|
+
if key in self.memo:
|
|
207
|
+
return self.memo[key]
|
|
208
|
+
if key in self.active or depth > 5 or not m.skel:
|
|
209
|
+
return (TOP, NONE)
|
|
210
|
+
self.active.add(key)
|
|
211
|
+
env = {}
|
|
212
|
+
for i, p in enumerate(m.params):
|
|
213
|
+
env[p["name"]] = argvals[i] if i < len(argvals) else (TOP, NONE)
|
|
214
|
+
fr = Frame(m, env, record=False)
|
|
215
|
+
self.exec_block(fr, m.skel, depth + 1)
|
|
216
|
+
self.active.discard(key)
|
|
217
|
+
res = join(fr.returns) if fr.returns else (TOP, NONE)
|
|
218
|
+
if res[0] is not TOP and fr.decisions:
|
|
219
|
+
res = (res[0], res[1] | frozenset(fr.decisions))
|
|
220
|
+
self.memo[key] = res
|
|
221
|
+
return res
|
|
222
|
+
|
|
223
|
+
def call_value(self, fr, e, argvals, recv, depth):
|
|
224
|
+
fi = e.get("f")
|
|
225
|
+
name = (e.get("m") or "").lower()
|
|
226
|
+
fnname = e.get("fn")
|
|
227
|
+
if fi is None:
|
|
228
|
+
return (TOP, NONE)
|
|
229
|
+
fact = fr.fn.facts[fi]
|
|
230
|
+
if not name and fact.get("kind") == "static":
|
|
231
|
+
name = (fact.get("m") or "").lower()
|
|
232
|
+
line = fact.get("line")
|
|
233
|
+
if name in self.accessors and argvals:
|
|
234
|
+
k = argvals[0][0]
|
|
235
|
+
if isinstance(k, str) and (k in self.true_keys or k in self.false_keys):
|
|
236
|
+
self.stats["setting_reads"] += 1
|
|
237
|
+
return (k in self.true_keys, frozenset({f"setting '{k}'@{fr.fn.file}:{line}"}))
|
|
238
|
+
tg = self.targets(fr, fi)
|
|
239
|
+
if tg:
|
|
240
|
+
vals = [self.summary(m, argvals, depth) for m, _ in tg]
|
|
241
|
+
v, why = join(vals)
|
|
242
|
+
if v is not TOP and why:
|
|
243
|
+
label = "/".join(sorted({m.id.split(":", 1)[1].split("\\")[-1] for m, _ in tg}))
|
|
244
|
+
return (v, frozenset({f"{label}()@{fr.fn.file}:{line}"}))
|
|
245
|
+
if v is not TOP:
|
|
246
|
+
return (v, NONE)
|
|
247
|
+
if all(not m.returns_nullable and m.returns and all(t.lower() not in SCALAR_RET for t in m.returns) for m, _ in tg):
|
|
248
|
+
return (TRUTHY, NONE)
|
|
249
|
+
return (TOP, NONE)
|
|
250
|
+
if fnname in NONNULL_FUNCS:
|
|
251
|
+
return (TRUTHY, NONE)
|
|
252
|
+
if recv is not None and recv[0] is TRUTHY and fr.fn.facts[fi].get("kind") == "method" and name in ("json", "make", "noContent", "download", "stream", "view", "to", "route", "with"):
|
|
253
|
+
return (TRUTHY, NONE)
|
|
254
|
+
return (TOP, NONE)
|
|
255
|
+
|
|
256
|
+
# ---------- expressions ----------
|
|
257
|
+
def is_presence_check(self, e, fr):
|
|
258
|
+
k = e.get("k")
|
|
259
|
+
x = None
|
|
260
|
+
if k == "not":
|
|
261
|
+
x = e["x"]
|
|
262
|
+
elif k == "cmp" and e.get("op") in ("===", "=="):
|
|
263
|
+
l, r = e["l"], e["rr"]
|
|
264
|
+
if r.get("k") == "lit" and r.get("v") is None:
|
|
265
|
+
x = l
|
|
266
|
+
elif l.get("k") == "lit" and l.get("v") is None:
|
|
267
|
+
x = r
|
|
268
|
+
elif k == "call" and e.get("fn") in ("is_null", "empty") and e.get("args"):
|
|
269
|
+
x = e["args"][0]
|
|
270
|
+
return x is not None and x.get("k") == "var" and fr.env.get(x["n"], (TOP, NONE))[0] is TOP
|
|
271
|
+
|
|
272
|
+
def cond(self, fr, e, depth):
|
|
273
|
+
"""Formula for the truthiness of e (also performs e's side effects / dead marking)."""
|
|
274
|
+
k = e.get("k")
|
|
275
|
+
if k == "not":
|
|
276
|
+
return ("n", self.cond(fr, e["x"], depth))
|
|
277
|
+
if k in ("and", "or"):
|
|
278
|
+
fl = self.cond(fr, e["l"], depth)
|
|
279
|
+
short_val = (k == "or")
|
|
280
|
+
if not f_sat(("n", fl) if short_val else fl): # left decides
|
|
281
|
+
self.mark(fr, e["rr"].get("r"), f_why(fl))
|
|
282
|
+
return fl
|
|
283
|
+
env0 = dict(fr.env)
|
|
284
|
+
fr_ = self.cond(fr, e["rr"], depth)
|
|
285
|
+
# right side may not run: variables it assigned become unknown
|
|
286
|
+
for kk in set(fr.env) | set(env0):
|
|
287
|
+
if fr.env.get(kk) != env0.get(kk):
|
|
288
|
+
fr.env[kk] = join([fr.env.get(kk, (TOP, NONE)), env0.get(kk, (TOP, NONE))])
|
|
289
|
+
return ("&" if k == "and" else "|", fl, fr_)
|
|
290
|
+
v, why = self.val(fr, e, depth)
|
|
291
|
+
t = truth(v)
|
|
292
|
+
if t is not None:
|
|
293
|
+
return ("c", t, why)
|
|
294
|
+
return ("a", canon(e))
|
|
295
|
+
|
|
296
|
+
def decide(self, f):
|
|
297
|
+
if not f_sat(f):
|
|
298
|
+
return False
|
|
299
|
+
if not f_sat(("n", f)):
|
|
300
|
+
return True
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
def val(self, fr, e, depth=0):
|
|
304
|
+
if e is None:
|
|
305
|
+
return (None, NONE)
|
|
306
|
+
k = e.get("k")
|
|
307
|
+
if k == "lit":
|
|
308
|
+
return (e.get("v"), NONE)
|
|
309
|
+
if k == "const":
|
|
310
|
+
v = self.const_value(e.get("class"), e.get("n"))
|
|
311
|
+
return (v, NONE) if v is not None and v is not TOP else (TOP, NONE)
|
|
312
|
+
if k == "var":
|
|
313
|
+
return fr.env.get(e["n"], (TOP, NONE))
|
|
314
|
+
if k in ("not", "and", "or"):
|
|
315
|
+
f = self.cond(fr, e, depth)
|
|
316
|
+
d = self.decide(f)
|
|
317
|
+
return (TOP, NONE) if d is None else (d, frozenset(f_why(f)))
|
|
318
|
+
if k == "cmp":
|
|
319
|
+
(a, wa), (b, wb) = self.val(fr, e["l"], depth), self.val(fr, e["rr"], depth)
|
|
320
|
+
op = e["op"]
|
|
321
|
+
res = None
|
|
322
|
+
if concrete(a) and concrete(b):
|
|
323
|
+
eq = (a == b and type(a) is type(b)) if op in ("===", "!==") else (a == b or (truth(a) is False and truth(b) is False and (a is None or b is None)))
|
|
324
|
+
res = eq if op in ("===", "==") else not eq
|
|
325
|
+
elif (a is TRUTHY and b is None) or (b is TRUTHY and a is None):
|
|
326
|
+
res = op in ("!==", "!=")
|
|
327
|
+
return (TOP, NONE) if res is None else (res, wa | wb)
|
|
328
|
+
if k == "asg":
|
|
329
|
+
v = self.val(fr, e["x"], depth)
|
|
330
|
+
fr.env[e["n"]] = v
|
|
331
|
+
return v
|
|
332
|
+
if k == "cast":
|
|
333
|
+
v, w = self.val(fr, e["x"], depth)
|
|
334
|
+
if e.get("to") == "bool":
|
|
335
|
+
t = truth(v)
|
|
336
|
+
return (TOP, NONE) if t is None else (t, w)
|
|
337
|
+
return (v, w)
|
|
338
|
+
if k == "tern":
|
|
339
|
+
f = self.cond(fr, e["c"], depth)
|
|
340
|
+
d = self.decide(f)
|
|
341
|
+
why = frozenset(f_why(f))
|
|
342
|
+
if d is True:
|
|
343
|
+
if e.get("e"):
|
|
344
|
+
self.mark(fr, e["e"].get("r"), why)
|
|
345
|
+
v, w = self.val(fr, e["t"], depth) if e.get("t") else (True, NONE)
|
|
346
|
+
return (v, w | why)
|
|
347
|
+
if d is False:
|
|
348
|
+
if e.get("t"):
|
|
349
|
+
self.mark(fr, e["t"].get("r"), why)
|
|
350
|
+
v, w = self.val(fr, e["e"], depth)
|
|
351
|
+
return (v, w | why)
|
|
352
|
+
env0 = dict(fr.env)
|
|
353
|
+
v1 = self.val(fr, e["t"], depth) if e.get("t") else (TOP, NONE)
|
|
354
|
+
env1, fr.env = fr.env, dict(env0)
|
|
355
|
+
v2 = self.val(fr, e["e"], depth)
|
|
356
|
+
fr.env = self.merge([env1, fr.env])
|
|
357
|
+
return join([v1, v2])
|
|
358
|
+
if k == "coal":
|
|
359
|
+
l = self.val(fr, e["l"], depth)
|
|
360
|
+
if l[0] is None:
|
|
361
|
+
v, w = self.val(fr, e["rr"], depth)
|
|
362
|
+
return (v, w | l[1])
|
|
363
|
+
if (concrete(l[0]) and l[0] is not None) or l[0] is TRUTHY:
|
|
364
|
+
self.mark(fr, e["rr"].get("r"), l[1])
|
|
365
|
+
return l
|
|
366
|
+
return join([l, self.val(fr, e["rr"], depth)])
|
|
367
|
+
if k == "arr":
|
|
368
|
+
for c in e.get("ch", []):
|
|
369
|
+
self.val(fr, c, depth)
|
|
370
|
+
return (EMPTY, NONE) if e.get("empty") else (TRUTHY, NONE)
|
|
371
|
+
if k == "new":
|
|
372
|
+
for a in e.get("args", []):
|
|
373
|
+
self.val(fr, a, depth)
|
|
374
|
+
return (TRUTHY, NONE)
|
|
375
|
+
if k == "match":
|
|
376
|
+
subj = self.val(fr, e["c"], depth)
|
|
377
|
+
decided, possible, why = False, [], set(subj[1])
|
|
378
|
+
for arm in e["arms"]:
|
|
379
|
+
if decided:
|
|
380
|
+
self.mark(fr, arm.get("r"), frozenset(why))
|
|
381
|
+
continue
|
|
382
|
+
if arm["conds"] is None:
|
|
383
|
+
st = True
|
|
384
|
+
else:
|
|
385
|
+
sts = []
|
|
386
|
+
for c in arm["conds"]:
|
|
387
|
+
cv = self.val(fr, c, depth)
|
|
388
|
+
why |= cv[1]
|
|
389
|
+
if concrete(subj[0]) and concrete(cv[0]):
|
|
390
|
+
sts.append(subj[0] == cv[0] and type(subj[0]) is type(cv[0]))
|
|
391
|
+
else:
|
|
392
|
+
sts.append(None)
|
|
393
|
+
st = True if True in sts else (False if all(s is False for s in sts) else None)
|
|
394
|
+
if st is False:
|
|
395
|
+
self.mark(fr, arm.get("r"), frozenset(why) if subj[1] else NONE)
|
|
396
|
+
continue
|
|
397
|
+
possible.append(arm)
|
|
398
|
+
if st is True:
|
|
399
|
+
decided = True
|
|
400
|
+
vals = [self.val(fr, a["body"], depth) for a in possible]
|
|
401
|
+
v = join(vals)
|
|
402
|
+
return (v[0], v[1] | frozenset(why)) if v[0] is not TOP else v
|
|
403
|
+
if k == "clo":
|
|
404
|
+
# closure body: may run (0..n times) with the captured environment; its returns are its own
|
|
405
|
+
sub = Frame(fr.fn, dict(fr.env), fr.record)
|
|
406
|
+
sub.fn = fr.fn
|
|
407
|
+
for p in e.get("params", []):
|
|
408
|
+
sub.env[p] = (TOP, NONE)
|
|
409
|
+
if "b" in e:
|
|
410
|
+
self.exec_block(sub, e["b"], depth)
|
|
411
|
+
elif e.get("x"):
|
|
412
|
+
self.val(sub, e["x"], depth)
|
|
413
|
+
fr.decisions |= sub.decisions
|
|
414
|
+
return (TRUTHY, NONE)
|
|
415
|
+
if k == "call":
|
|
416
|
+
recv = self.val(fr, e["recv"], depth) if e.get("recv") else None
|
|
417
|
+
args = [self.val(fr, a, depth) for a in e.get("args", [])]
|
|
418
|
+
return self.call_value(fr, e, args, recv, depth)
|
|
419
|
+
for c in e.get("ch", []):
|
|
420
|
+
self.val(fr, c, depth)
|
|
421
|
+
return (TOP, NONE)
|
|
422
|
+
|
|
423
|
+
# ---------- statements ----------
|
|
424
|
+
def merge(self, envs):
|
|
425
|
+
if len(envs) == 1:
|
|
426
|
+
return envs[0]
|
|
427
|
+
out = {}
|
|
428
|
+
for kk in set().union(*envs):
|
|
429
|
+
out[kk] = join([en.get(kk, (TOP, NONE)) for en in envs])
|
|
430
|
+
return out
|
|
431
|
+
|
|
432
|
+
def exec_block(self, fr, stmts, depth):
|
|
433
|
+
for i, s in enumerate(stmts or []):
|
|
434
|
+
term, why = self.exec_stmt(fr, s, depth)
|
|
435
|
+
if term:
|
|
436
|
+
if why and i + 1 < len(stmts):
|
|
437
|
+
self.mark(fr, self.span(stmts[i + 1:]), why)
|
|
438
|
+
return True, why
|
|
439
|
+
return False, NONE
|
|
440
|
+
|
|
441
|
+
def exec_stmt(self, fr, s, depth):
|
|
442
|
+
k = s.get("k")
|
|
443
|
+
if k == "x":
|
|
444
|
+
x = s["x"]
|
|
445
|
+
if x.get("k") == "call" and x.get("fn") in ("abort", "abort_if", "abort_unless"):
|
|
446
|
+
args = [self.val(fr, a, depth) for a in x.get("args", [])]
|
|
447
|
+
if x["fn"] == "abort":
|
|
448
|
+
return True, NONE
|
|
449
|
+
if args:
|
|
450
|
+
t = truth(args[0][0])
|
|
451
|
+
if (x["fn"] == "abort_if" and t is True) or (x["fn"] == "abort_unless" and t is False):
|
|
452
|
+
return True, args[0][1]
|
|
453
|
+
return False, NONE
|
|
454
|
+
self.val(fr, x, depth)
|
|
455
|
+
return False, NONE
|
|
456
|
+
if k == "ret":
|
|
457
|
+
v = self.val(fr, s["x"], depth) if s.get("x") else (None, NONE)
|
|
458
|
+
fr.returns.append(v)
|
|
459
|
+
return True, NONE
|
|
460
|
+
if k in ("term", "brk"):
|
|
461
|
+
return True, NONE
|
|
462
|
+
if k == "seq":
|
|
463
|
+
return self.exec_block(fr, s["b"], depth)
|
|
464
|
+
if k == "if":
|
|
465
|
+
return self.exec_if(fr, s, depth)
|
|
466
|
+
if k == "blk":
|
|
467
|
+
for p in s.get("pre", []):
|
|
468
|
+
self.val(fr, p, depth)
|
|
469
|
+
for v in s.get("av", []):
|
|
470
|
+
fr.env[v] = (TOP, NONE)
|
|
471
|
+
base = dict(fr.env)
|
|
472
|
+
for b in s.get("b", []):
|
|
473
|
+
fr.env = dict(base)
|
|
474
|
+
self.exec_block(fr, b, depth)
|
|
475
|
+
fr.env = base
|
|
476
|
+
return False, NONE
|
|
477
|
+
if k == "try":
|
|
478
|
+
t, w = self.exec_block(fr, s["b"], depth)
|
|
479
|
+
after = dict(fr.env)
|
|
480
|
+
for v in s.get("av", []):
|
|
481
|
+
after[v] = (TOP, NONE)
|
|
482
|
+
for c in s.get("catches", []):
|
|
483
|
+
fr.env = dict(after)
|
|
484
|
+
self.exec_block(fr, c, depth)
|
|
485
|
+
fr.env = after
|
|
486
|
+
if s.get("fin"):
|
|
487
|
+
self.exec_block(fr, s["fin"], depth)
|
|
488
|
+
return (t and not s.get("catches")), (w if t and not s.get("catches") else NONE)
|
|
489
|
+
return False, NONE
|
|
490
|
+
|
|
491
|
+
def exec_if(self, fr, s, depth):
|
|
492
|
+
branches = [(s["c"], s["t"], None)] + [(ei["c"], ei["b"], ei.get("r")) for ei in s.get("ei", [])]
|
|
493
|
+
else_b = s.get("e")
|
|
494
|
+
prior = [] # formulas of earlier conditions (all false to reach here)
|
|
495
|
+
outs, whys = [], set()
|
|
496
|
+
base = fr.env
|
|
497
|
+
taken = False
|
|
498
|
+
for c, body, whole in branches:
|
|
499
|
+
if taken:
|
|
500
|
+
self.mark(fr, whole or self.span(body), frozenset(whys))
|
|
501
|
+
continue
|
|
502
|
+
fr.env = dict(base)
|
|
503
|
+
if self.is_presence_check(c, fr):
|
|
504
|
+
f = ("c", False, NONE) # inputs-present assumption: not taken, never marked
|
|
505
|
+
else:
|
|
506
|
+
f = self.cond(fr, c, depth)
|
|
507
|
+
base = fr.env # condition side effects (assignments) persist
|
|
508
|
+
path = f_and([("n", p) for p in prior] + [f])
|
|
509
|
+
w = f_why(f)
|
|
510
|
+
for p in prior:
|
|
511
|
+
f_why(p, w)
|
|
512
|
+
if not f_sat(path):
|
|
513
|
+
whys |= w
|
|
514
|
+
self.mark(fr, self.span(body), frozenset(w))
|
|
515
|
+
prior.append(f)
|
|
516
|
+
continue
|
|
517
|
+
fr.env = dict(base)
|
|
518
|
+
t, tw = self.exec_block(fr, body, depth)
|
|
519
|
+
outs.append((fr.env, t))
|
|
520
|
+
if t:
|
|
521
|
+
whys |= tw
|
|
522
|
+
if not f_sat(f_and([("n", p) for p in prior] + [("n", f)])): # this branch is certain
|
|
523
|
+
whys |= w
|
|
524
|
+
taken = True
|
|
525
|
+
prior.append(f)
|
|
526
|
+
if not taken:
|
|
527
|
+
rest = f_and([("n", p) for p in prior])
|
|
528
|
+
if else_b is not None:
|
|
529
|
+
if not f_sat(rest):
|
|
530
|
+
self.mark(fr, s.get("er") and tuple(s["er"]) or self.span(else_b), frozenset(whys | f_why(rest)))
|
|
531
|
+
else:
|
|
532
|
+
fr.env = dict(base)
|
|
533
|
+
t, tw = self.exec_block(fr, else_b, depth)
|
|
534
|
+
outs.append((fr.env, t))
|
|
535
|
+
if t:
|
|
536
|
+
whys |= tw
|
|
537
|
+
elif f_sat(rest):
|
|
538
|
+
outs.append((dict(base), False))
|
|
539
|
+
else:
|
|
540
|
+
whys |= f_why(rest)
|
|
541
|
+
elif else_b is not None:
|
|
542
|
+
self.mark(fr, s.get("er") and tuple(s["er"]) or self.span(else_b), frozenset(whys))
|
|
543
|
+
live = [en for en, t in outs if not t]
|
|
544
|
+
if not live:
|
|
545
|
+
fr.env = base
|
|
546
|
+
return True, frozenset(whys)
|
|
547
|
+
fr.env = self.merge(live)
|
|
548
|
+
return False, NONE
|
|
549
|
+
|
|
550
|
+
# ---------- driver ----------
|
|
551
|
+
def run(self):
|
|
552
|
+
for fn in self.prog.all_funcs:
|
|
553
|
+
if not fn.skel:
|
|
554
|
+
continue
|
|
555
|
+
fr = Frame(fn, {p["name"]: (TOP, NONE) for p in fn.params}, record=True)
|
|
556
|
+
try:
|
|
557
|
+
self.exec_block(fr, fn.skel, 0)
|
|
558
|
+
except RecursionError:
|
|
559
|
+
self.stats["recursion_errors"] += 1
|
|
560
|
+
continue
|
|
561
|
+
if fn.dead:
|
|
562
|
+
self.stats["functions_with_gated_code"] += 1
|
|
563
|
+
self.stats["gated_facts"] += len(fn.dead)
|
|
564
|
+
byid = {f.id: f for f in self.prog.all_funcs}
|
|
565
|
+
for mid in sorted({mid for mid, _ in list(self.memo)}):
|
|
566
|
+
m = byid.get(mid)
|
|
567
|
+
if not m:
|
|
568
|
+
continue
|
|
569
|
+
v, why = self.summary(m, [(TOP, NONE)] * len(m.params), 0)
|
|
570
|
+
if concrete(v) and isinstance(v, bool) and why:
|
|
571
|
+
self.predicates[mid] = (v, "; ".join(sorted(why))[:300])
|
|
572
|
+
self.stats["predicates"] = len(self.predicates)
|
|
573
|
+
return dict(self.stats)
|