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.
Files changed (174) hide show
  1. cg_code_graph-0.10.1.dist-info/METADATA +678 -0
  2. cg_code_graph-0.10.1.dist-info/RECORD +174 -0
  3. cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
  4. cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
  5. cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
  6. cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
  7. codegraph/__init__.py +2 -0
  8. codegraph/aitools.py +129 -0
  9. codegraph/apps.py +76 -0
  10. codegraph/blindspots.py +428 -0
  11. codegraph/bridges.py +1701 -0
  12. codegraph/cli.py +725 -0
  13. codegraph/concepts.py +362 -0
  14. codegraph/config.py +559 -0
  15. codegraph/core/__init__.py +0 -0
  16. codegraph/core/cache.py +375 -0
  17. codegraph/core/detect.py +80 -0
  18. codegraph/core/extractors.py +187 -0
  19. codegraph/core/fsutil.py +61 -0
  20. codegraph/core/generated.py +575 -0
  21. codegraph/core/model.py +174 -0
  22. codegraph/core/paths.py +175 -0
  23. codegraph/core/plugin.py +160 -0
  24. codegraph/core/store.py +80 -0
  25. codegraph/core/syntax_errors.py +132 -0
  26. codegraph/coverage.py +928 -0
  27. codegraph/doctor.py +453 -0
  28. codegraph/external.py +613 -0
  29. codegraph/indexer.py +336 -0
  30. codegraph/link.py +434 -0
  31. codegraph/lint_async.py +524 -0
  32. codegraph/mcp_server.py +1303 -0
  33. codegraph/parity.py +473 -0
  34. codegraph/parity_structure.py +307 -0
  35. codegraph/payload.py +321 -0
  36. codegraph/plans.py +1285 -0
  37. codegraph/platform_scan.py +643 -0
  38. codegraph/platforms.py +1369 -0
  39. codegraph/plugins/__init__.py +0 -0
  40. codegraph/plugins/cfamily/__init__.py +0 -0
  41. codegraph/plugins/cfamily/plugin.py +930 -0
  42. codegraph/plugins/cfamily/syntax.py +881 -0
  43. codegraph/plugins/dart/__init__.py +0 -0
  44. codegraph/plugins/dart/bridges.py +345 -0
  45. codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
  46. codegraph/plugins/dart/extractor/pubspec.lock +149 -0
  47. codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
  48. codegraph/plugins/dart/http.py +904 -0
  49. codegraph/plugins/dart/models.py +308 -0
  50. codegraph/plugins/dart/plugin.py +625 -0
  51. codegraph/plugins/dart/program.py +907 -0
  52. codegraph/plugins/django/__init__.py +0 -0
  53. codegraph/plugins/django/extras.py +378 -0
  54. codegraph/plugins/django/models.py +508 -0
  55. codegraph/plugins/django/plugin.py +728 -0
  56. codegraph/plugins/django/schemas.py +339 -0
  57. codegraph/plugins/django/shapes.py +216 -0
  58. codegraph/plugins/django/urls.py +603 -0
  59. codegraph/plugins/express/__init__.py +0 -0
  60. codegraph/plugins/express/plugin.py +428 -0
  61. codegraph/plugins/flutter/__init__.py +0 -0
  62. codegraph/plugins/flutter/plugin.py +538 -0
  63. codegraph/plugins/kotlin/__init__.py +0 -0
  64. codegraph/plugins/kotlin/exact.py +457 -0
  65. codegraph/plugins/kotlin/plugin.py +1961 -0
  66. codegraph/plugins/kotlin/reparse.py +234 -0
  67. codegraph/plugins/laravel/__init__.py +0 -0
  68. codegraph/plugins/laravel/broadcast.py +351 -0
  69. codegraph/plugins/laravel/plugin.py +863 -0
  70. codegraph/plugins/laravel/tests.py +262 -0
  71. codegraph/plugins/laravel/values.py +728 -0
  72. codegraph/plugins/native/__init__.py +0 -0
  73. codegraph/plugins/native/gates.py +286 -0
  74. codegraph/plugins/native/runner.py +183 -0
  75. codegraph/plugins/native/scipread.py +194 -0
  76. codegraph/plugins/native/ts.py +54 -0
  77. codegraph/plugins/nest/__init__.py +0 -0
  78. codegraph/plugins/nest/plugin.py +654 -0
  79. codegraph/plugins/nextjs/__init__.py +0 -0
  80. codegraph/plugins/nextjs/plugin.py +336 -0
  81. codegraph/plugins/nuxt/__init__.py +0 -0
  82. codegraph/plugins/nuxt/plugin.py +308 -0
  83. codegraph/plugins/php/__init__.py +0 -0
  84. codegraph/plugins/php/extractor/composer.json +5 -0
  85. codegraph/plugins/php/extractor/composer.lock +76 -0
  86. codegraph/plugins/php/extractor/extract.php +743 -0
  87. codegraph/plugins/php/gating.py +573 -0
  88. codegraph/plugins/php/plugin.py +668 -0
  89. codegraph/plugins/php/strings.py +197 -0
  90. codegraph/plugins/python/__init__.py +0 -0
  91. codegraph/plugins/python/aitools.py +664 -0
  92. codegraph/plugins/python/external.py +245 -0
  93. codegraph/plugins/python/fields.py +107 -0
  94. codegraph/plugins/python/plugin.py +1733 -0
  95. codegraph/plugins/python/refs.py +485 -0
  96. codegraph/plugins/python/roots.py +412 -0
  97. codegraph/plugins/python/socketio.py +210 -0
  98. codegraph/plugins/python/subproc.py +864 -0
  99. codegraph/plugins/python/tests.py +1040 -0
  100. codegraph/plugins/python/values.py +179 -0
  101. codegraph/plugins/pyweb/__init__.py +0 -0
  102. codegraph/plugins/pyweb/plugin.py +1334 -0
  103. codegraph/plugins/pyweb/values.py +68 -0
  104. codegraph/plugins/rust/__init__.py +0 -0
  105. codegraph/plugins/rust/cargo.py +226 -0
  106. codegraph/plugins/rust/plugin.py +980 -0
  107. codegraph/plugins/rust/syntax.py +678 -0
  108. codegraph/plugins/scip/__init__.py +0 -0
  109. codegraph/plugins/scip/importer.py +129 -0
  110. codegraph/plugins/scip/scip.proto +962 -0
  111. codegraph/plugins/scip/scip_pb2.py +97 -0
  112. codegraph/plugins/stubs/__init__.py +0 -0
  113. codegraph/plugins/stubs/plugins.py +38 -0
  114. codegraph/plugins/swift/__init__.py +0 -0
  115. codegraph/plugins/swift/baseurl.py +109 -0
  116. codegraph/plugins/swift/exact.py +415 -0
  117. codegraph/plugins/swift/indexstore.py +209 -0
  118. codegraph/plugins/swift/packages.py +174 -0
  119. codegraph/plugins/swift/plugin.py +2890 -0
  120. codegraph/plugins/ts/__init__.py +0 -0
  121. codegraph/plugins/ts/baseurl.py +185 -0
  122. codegraph/plugins/ts/extractor/extract.mjs +2652 -0
  123. codegraph/plugins/ts/extractor/fw.mjs +685 -0
  124. codegraph/plugins/ts/extractor/package-lock.json +205 -0
  125. codegraph/plugins/ts/extractor/package.json +9 -0
  126. codegraph/plugins/ts/plugin.py +480 -0
  127. codegraph/plugins/tsweb/__init__.py +0 -0
  128. codegraph/plugins/tsweb/common.py +290 -0
  129. codegraph/plugins/tsweb/data.py +276 -0
  130. codegraph/presets/__init__.py +146 -0
  131. codegraph/presets/c_cpp.yaml +9 -0
  132. codegraph/presets/common.yaml +66 -0
  133. codegraph/presets/dart.yaml +9 -0
  134. codegraph/presets/django-ninja.yaml +15 -0
  135. codegraph/presets/django.yaml +25 -0
  136. codegraph/presets/djangorestframework.yaml +17 -0
  137. codegraph/presets/express.yaml +17 -0
  138. codegraph/presets/kotlin.yaml +11 -0
  139. codegraph/presets/laravel.yaml +40 -0
  140. codegraph/presets/nest.yaml +11 -0
  141. codegraph/presets/nextjs.yaml +15 -0
  142. codegraph/presets/nuxt.yaml +9 -0
  143. codegraph/presets/php.yaml +5 -0
  144. codegraph/presets/python.yaml +10 -0
  145. codegraph/presets/rust.yaml +5 -0
  146. codegraph/presets/swift.yaml +10 -0
  147. codegraph/presets/typescript.yaml +13 -0
  148. codegraph/process_runs.py +328 -0
  149. codegraph/protocols/__init__.py +299 -0
  150. codegraph/protocols/builtin.py +67 -0
  151. codegraph/protocols/matchers.py +144 -0
  152. codegraph/protocols/view.py +334 -0
  153. codegraph/query.py +2089 -0
  154. codegraph/realtime.py +260 -0
  155. codegraph/roundtrip.py +346 -0
  156. codegraph/routes.py +442 -0
  157. codegraph/starters.py +218 -0
  158. codegraph/tests_index.py +117 -0
  159. codegraph/viz/__init__.py +0 -0
  160. codegraph/viz/graph.py +369 -0
  161. codegraph/viz/server.py +198 -0
  162. codegraph/viz/static/app.css +148 -0
  163. codegraph/viz/static/app.js +1082 -0
  164. codegraph/viz/static/index.html +81 -0
  165. codegraph/viz/static/layered.js +237 -0
  166. codegraph/viz/static/vendor/VERSIONS.txt +4 -0
  167. codegraph/viz/static/vendor/cose-base.js +3214 -0
  168. codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
  169. codegraph/viz/static/vendor/cytoscape.min.js +31 -0
  170. codegraph/viz/static/vendor/layout-base.js +5230 -0
  171. codegraph/viz/tools/package-lock.json +303 -0
  172. codegraph/viz/tools/package.json +7 -0
  173. codegraph/viz/tools/shoot.mjs +165 -0
  174. 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)