codeanalyzer-python 0.3.0__py3-none-any.whl → 1.0.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 (46) hide show
  1. codeanalyzer/__main__.py +77 -4
  2. codeanalyzer/core.py +174 -72
  3. codeanalyzer/dataflow/__init__.py +35 -0
  4. codeanalyzer/dataflow/access_paths.py +563 -0
  5. codeanalyzer/dataflow/alias.py +93 -0
  6. codeanalyzer/dataflow/builder.py +688 -0
  7. codeanalyzer/dataflow/cfg.py +605 -0
  8. codeanalyzer/dataflow/defuse.py +113 -0
  9. codeanalyzer/dataflow/dominance.py +140 -0
  10. codeanalyzer/dataflow/identity.py +91 -0
  11. codeanalyzer/dataflow/pdg.py +100 -0
  12. codeanalyzer/dataflow/scalpel_oracle.py +269 -0
  13. codeanalyzer/dataflow/scc.py +91 -0
  14. codeanalyzer/dataflow/sdg.py +424 -0
  15. codeanalyzer/dataflow/slicing.py +93 -0
  16. codeanalyzer/dataflow/summaries.py +217 -0
  17. codeanalyzer/dataflow/syntactic.py +26 -0
  18. codeanalyzer/neo4j/__init__.py +1 -1
  19. codeanalyzer/neo4j/bolt.py +19 -4
  20. codeanalyzer/neo4j/cypher.py +9 -3
  21. codeanalyzer/neo4j/emit.py +10 -5
  22. codeanalyzer/neo4j/project.py +307 -60
  23. codeanalyzer/neo4j/rows.py +18 -15
  24. codeanalyzer/neo4j/schema.py +297 -15
  25. codeanalyzer/options/options.py +4 -0
  26. codeanalyzer/provenance.py +61 -0
  27. codeanalyzer/schema/__init__.py +19 -0
  28. codeanalyzer/schema/assign_ids.py +37 -0
  29. codeanalyzer/schema/call_graph_ids.py +12 -0
  30. codeanalyzer/schema/ids.py +23 -0
  31. codeanalyzer/schema/l1_body.py +29 -0
  32. codeanalyzer/schema/l2_callees.py +36 -0
  33. codeanalyzer/schema/py_schema.py +175 -26
  34. codeanalyzer/semantic_analysis/call_graph.py +24 -27
  35. codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +10 -10
  36. codeanalyzer/semantic_analysis/pycg/shard_planner.py +7 -7
  37. codeanalyzer/syntactic_analysis/import_resolver.py +67 -0
  38. codeanalyzer/syntactic_analysis/symbol_table_builder.py +103 -20
  39. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/METADATA +233 -43
  40. codeanalyzer_python-1.0.0.dist-info/RECORD +59 -0
  41. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/WHEEL +1 -1
  42. codeanalyzer/neo4j/catalog.py +0 -245
  43. codeanalyzer_python-0.3.0.dist-info/RECORD +0 -38
  44. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/entry_points.txt +0 -0
  45. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/licenses/LICENSE +0 -0
  46. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,424 @@
1
+ ################################################################################
2
+ # Copyright IBM Corporation 2025
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ ################################################################################
16
+
17
+ """Stage 7 of the level-3 dataflow ladder: SDG assembly (Horwitz–Reps–Binkley).
18
+
19
+ Parameter-passing structure per function and callsite:
20
+
21
+ - **formal_in** nodes: one per parameter (var = the parameter name), one per
22
+ captured variable (``<capture>:name``), one per transitively-read global
23
+ (``<global>:module::name``);
24
+ - **formal_out** nodes: the return value (``<return>``), each caller-visibly
25
+ mutated parameter, each written global;
26
+ - **actual_in / actual_out** nodes at each callsite, mirroring the callee's
27
+ formals that the callsite binds (positional/keyword-matched arguments, the
28
+ receiver as ``self``, globals from the callee's summary footprint);
29
+ - closure captures bind at the nested function's *definition* statement: an
30
+ ``actual_in`` at the def node, ``PARAM_IN`` to the nested callable's
31
+ ``<capture>`` formal.
32
+
33
+ Parameter nodes share the owning function's node-id space, allocated after
34
+ EXIT (the CFG keeps its ``ENTRY = 0 … EXIT = last CFG id`` contract; parameter
35
+ nodes are PDG/SDG-level, deterministically ordered). Intra-function wiring
36
+ (defs → formal_out, formal_in → uses, defs → actual_in, actual_out → callsite)
37
+ is emitted as ordinary DDG/CDG edges of the function's PDG; cross-function
38
+ ``CALL`` / ``PARAM_IN`` / ``PARAM_OUT`` edges and same-signature ``SUMMARY``
39
+ edges (actual_in → actual_out, encoding the callee's transitive flow) form the
40
+ ``sdg_edges`` section.
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ from dataclasses import dataclass, field
46
+ from typing import Dict, List, Optional, Set, Tuple
47
+
48
+ from codeanalyzer.dataflow.access_paths import RETURN_PATH, base_of, interferes, suffix_of
49
+ from codeanalyzer.dataflow.defuse import DDGEdge
50
+ from codeanalyzer.dataflow.pdg import FunctionPDG, PDGEdge
51
+ from codeanalyzer.dataflow.summaries import (
52
+ CallSite,
53
+ FunctionInfo,
54
+ FunctionSummary,
55
+ solve_function,
56
+ )
57
+
58
+ CAPTURE_PREFIX = "<capture>:"
59
+ GLOBAL_PREFIX = "<global>:"
60
+
61
+
62
+ @dataclass
63
+ class ParamNode:
64
+ id: int
65
+ kind: str # formal_in | formal_out | actual_in | actual_out
66
+ var: str
67
+ call_node: Optional[int] = None # owning callsite statement (actuals)
68
+ start_line: int = -1
69
+ end_line: int = -1
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class SDGEdge:
74
+ source_sig: str
75
+ source_node: int
76
+ target_sig: str
77
+ target_node: int
78
+ type: str # CALL | PARAM_IN | PARAM_OUT | SUMMARY
79
+ var: Optional[str] = None
80
+
81
+
82
+ @dataclass
83
+ class FunctionGraphs:
84
+ """One callable's complete level-3 graphs, ready for emission."""
85
+
86
+ pdg: FunctionPDG
87
+ ddg: List[DDGEdge] = field(default_factory=list) # augmented, final
88
+ param_nodes: List[ParamNode] = field(default_factory=list)
89
+ extra_edges: List[PDGEdge] = field(default_factory=list) # param wiring
90
+ summary: Optional[FunctionSummary] = None
91
+
92
+
93
+ @dataclass
94
+ class ProgramGraphsIR:
95
+ functions: Dict[str, FunctionGraphs] = field(default_factory=dict)
96
+ sdg_edges: List[SDGEdge] = field(default_factory=list)
97
+ k_limit: int = 3
98
+
99
+
100
+ def _formal_key_to_var(key: str) -> str:
101
+ kind, _, name = key.partition(":")
102
+ if kind == "param":
103
+ return name
104
+ if kind == "capture":
105
+ return CAPTURE_PREFIX + name
106
+ return GLOBAL_PREFIX + name
107
+
108
+
109
+ class _FunctionAssembler:
110
+ """Allocates parameter nodes and wiring edges for one function."""
111
+
112
+ def __init__(self, info: FunctionInfo, summary: FunctionSummary, facts, ddg):
113
+ self.info = info
114
+ self.summary = summary
115
+ self.facts = facts
116
+ self.ddg = ddg
117
+ self.cfg = info.pdg.cfg
118
+ self.scope = info.pdg.scope
119
+ self.next_id = len(self.cfg.nodes)
120
+ self.param_nodes: List[ParamNode] = []
121
+ self.extra: List[PDGEdge] = []
122
+ self.formal_in: Dict[str, int] = {} # var -> node id
123
+ self.formal_out: Dict[str, int] = {}
124
+ # (call_node, var) -> node id
125
+ self.actual_in: Dict[Tuple[int, str], int] = {}
126
+ self.actual_out: Dict[Tuple[int, str], int] = {}
127
+ entry = self.cfg.node_by_id(self.cfg.entry_id)
128
+ exit_ = self.cfg.node_by_id(self.cfg.exit_id)
129
+ self._entry_span = (entry.start_line, entry.end_line)
130
+ self._exit_span = (exit_.start_line, exit_.end_line)
131
+
132
+ def _alloc(self, kind: str, var: str, span, call_node=None) -> int:
133
+ nid = self.next_id
134
+ self.next_id += 1
135
+ self.param_nodes.append(
136
+ ParamNode(
137
+ id=nid, kind=kind, var=var, call_node=call_node,
138
+ start_line=span[0], end_line=span[1],
139
+ )
140
+ )
141
+ return nid
142
+
143
+ # ---------------------------------------------------------------- formals
144
+
145
+ def build_formals(self) -> None:
146
+ scope, summary = self.scope, self.summary
147
+ params = list(scope.params)
148
+ for p in params:
149
+ self.formal_in[p] = self._alloc("formal_in", p, self._entry_span)
150
+ for c in sorted(scope.captures):
151
+ var = CAPTURE_PREFIX + c
152
+ self.formal_in[var] = self._alloc("formal_in", var, self._entry_span)
153
+ for g in sorted(summary.global_reads):
154
+ var = GLOBAL_PREFIX + g
155
+ self.formal_in[var] = self._alloc("formal_in", var, self._entry_span)
156
+
157
+ self.formal_out[RETURN_PATH] = self._alloc(
158
+ "formal_out", RETURN_PATH, self._exit_span
159
+ )
160
+ for p in sorted(summary.mutated_params):
161
+ self.formal_out[p] = self._alloc("formal_out", p, self._exit_span)
162
+ for g in sorted(summary.global_writes):
163
+ var = GLOBAL_PREFIX + g
164
+ self.formal_out[var] = self._alloc("formal_out", var, self._exit_span)
165
+
166
+ # Wiring: formal_in → first uses (mirror the ENTRY-def DDG edges).
167
+ entry = self.cfg.entry_id
168
+ for e in self.ddg:
169
+ if e.source != entry:
170
+ continue
171
+ b = base_of(e.var)
172
+ if b in self.formal_in:
173
+ fid = self.formal_in[b]
174
+ elif CAPTURE_PREFIX + b in self.formal_in:
175
+ fid = self.formal_in[CAPTURE_PREFIX + b]
176
+ elif "::" in b and GLOBAL_PREFIX + b in self.formal_in:
177
+ fid = self.formal_in[GLOBAL_PREFIX + b]
178
+ else:
179
+ continue
180
+ self.extra.append(PDGEdge(source=fid, target=e.target, type="DDG", var=e.var))
181
+
182
+ # Wiring: defining nodes → formal_out.
183
+ param_names = set(scope.params)
184
+ if scope.self_name:
185
+ param_names.add(scope.self_name)
186
+ for nid, f in self.facts.items():
187
+ if nid == entry:
188
+ continue
189
+ if RETURN_PATH in f.defs:
190
+ self.extra.append(
191
+ PDGEdge(
192
+ source=nid,
193
+ target=self.formal_out[RETURN_PATH],
194
+ type="DDG",
195
+ var=RETURN_PATH,
196
+ )
197
+ )
198
+ for d in f.defs:
199
+ b = base_of(d)
200
+ if "::" in b and GLOBAL_PREFIX + b in self.formal_out:
201
+ self.extra.append(
202
+ PDGEdge(
203
+ source=nid,
204
+ target=self.formal_out[GLOBAL_PREFIX + b],
205
+ type="DDG",
206
+ var=d,
207
+ )
208
+ )
209
+ elif b in param_names and suffix_of(d) and b in self.formal_out:
210
+ self.extra.append(
211
+ PDGEdge(source=nid, target=self.formal_out[b], type="DDG", var=d)
212
+ )
213
+
214
+ # ---------------------------------------------------------------- actuals
215
+
216
+ def _defs_reaching_call_matching(self, call_node: int, path: Optional[str]):
217
+ """Sources of DDG in-edges of the call node whose var matches the
218
+ actual's access path (all of them when the actual is an expression)."""
219
+ sources = []
220
+ for e in self.ddg:
221
+ if e.target != call_node:
222
+ continue
223
+ if path is None or interferes(e.var, path) or interferes(path, e.var):
224
+ sources.append((e.source, e.var))
225
+ return sources
226
+
227
+ def build_actuals(
228
+ self,
229
+ summaries: Dict[str, FunctionSummary],
230
+ formal_ids: Dict[str, Dict[str, int]],
231
+ sdg_edges: List[SDGEdge],
232
+ ) -> None:
233
+ sig = self.info.signature
234
+ node_span = {
235
+ n.id: (n.start_line, n.end_line) for n in self.cfg.nodes
236
+ }
237
+
238
+ for cs in sorted(self.info.call_sites, key=lambda c: (c.node_id, c.targets)):
239
+ span = node_span.get(cs.node_id, (-1, -1))
240
+ for target in cs.targets:
241
+ callee_summary = summaries.get(target)
242
+ callee_formals = formal_ids.get(target)
243
+ if callee_summary is None or callee_formals is None:
244
+ continue # external — conservative pass-through already applies
245
+
246
+ # CALL: callsite statement → callee ENTRY.
247
+ sdg_edges.append(
248
+ SDGEdge(
249
+ source_sig=sig, source_node=cs.node_id,
250
+ target_sig=target, target_node=0, type="CALL",
251
+ )
252
+ )
253
+
254
+ bound_in: Dict[str, int] = {} # formal key -> actual_in id
255
+ bound_out: Dict[str, int] = {} # formal key -> actual_out id
256
+
257
+ # Argument actual_ins for the callee formals this site binds.
258
+ for param, path in cs.arg_paths:
259
+ if param not in callee_formals:
260
+ continue
261
+ key = (cs.node_id, f"{target}::{param}")
262
+ if key not in self.actual_in:
263
+ aid = self._alloc("actual_in", param, span, cs.node_id)
264
+ self.actual_in[key] = aid
265
+ self.extra.append(
266
+ PDGEdge(source=cs.node_id, target=aid, type="CDG")
267
+ )
268
+ for src, var in self._defs_reaching_call_matching(
269
+ cs.node_id, path
270
+ ):
271
+ self.extra.append(
272
+ PDGEdge(source=src, target=aid, type="DDG", var=var)
273
+ )
274
+ bound_in[f"param:{param}"] = self.actual_in[key]
275
+ sdg_edges.append(
276
+ SDGEdge(
277
+ source_sig=sig, source_node=self.actual_in[key],
278
+ target_sig=target,
279
+ target_node=callee_formals[param],
280
+ type="PARAM_IN", var=param,
281
+ )
282
+ )
283
+
284
+ # Global actual_ins from the callee's read footprint.
285
+ for g in sorted(callee_summary.global_reads):
286
+ fvar = GLOBAL_PREFIX + g
287
+ if fvar not in callee_formals:
288
+ continue
289
+ key = (cs.node_id, f"{target}::{fvar}")
290
+ if key not in self.actual_in:
291
+ aid = self._alloc("actual_in", fvar, span, cs.node_id)
292
+ self.actual_in[key] = aid
293
+ self.extra.append(
294
+ PDGEdge(source=cs.node_id, target=aid, type="CDG")
295
+ )
296
+ for src, var in self._defs_reaching_call_matching(
297
+ cs.node_id, g
298
+ ):
299
+ self.extra.append(
300
+ PDGEdge(source=src, target=aid, type="DDG", var=var)
301
+ )
302
+ bound_in[f"global:{g}"] = self.actual_in[key]
303
+ sdg_edges.append(
304
+ SDGEdge(
305
+ source_sig=sig, source_node=self.actual_in[key],
306
+ target_sig=target, target_node=callee_formals[fvar],
307
+ type="PARAM_IN", var=fvar,
308
+ )
309
+ )
310
+
311
+ # actual_outs: return, mutated bound params, written globals.
312
+ out_specs: List[Tuple[str, str]] = [("return", RETURN_PATH)]
313
+ for p in sorted(callee_summary.mutated_params):
314
+ if cs.arg_path_of(p) is not None:
315
+ out_specs.append((f"param:{p}", p))
316
+ for g in sorted(callee_summary.global_writes):
317
+ out_specs.append((f"global:{g}", GLOBAL_PREFIX + g))
318
+
319
+ callee_formal_outs = formal_ids.get(f"{target}<out>", {})
320
+ for key_name, fvar in out_specs:
321
+ if fvar not in callee_formal_outs:
322
+ continue
323
+ key = (cs.node_id, f"{target}::out::{fvar}")
324
+ if key not in self.actual_out:
325
+ oid = self._alloc("actual_out", fvar, span, cs.node_id)
326
+ self.actual_out[key] = oid
327
+ self.extra.append(
328
+ PDGEdge(source=cs.node_id, target=oid, type="CDG")
329
+ )
330
+ self.extra.append(
331
+ PDGEdge(source=oid, target=cs.node_id, type="DDG", var=fvar)
332
+ )
333
+ bound_out[key_name] = self.actual_out[key]
334
+ sdg_edges.append(
335
+ SDGEdge(
336
+ source_sig=target,
337
+ source_node=callee_formal_outs[fvar],
338
+ target_sig=sig, target_node=self.actual_out[key],
339
+ type="PARAM_OUT", var=fvar,
340
+ )
341
+ )
342
+
343
+ # SUMMARY: actual_in → actual_out per callee transitive flow.
344
+ for in_key, out_key in sorted(callee_summary.flows):
345
+ a_in = bound_in.get(in_key)
346
+ a_out = bound_out.get(out_key)
347
+ if a_in is not None and a_out is not None:
348
+ sdg_edges.append(
349
+ SDGEdge(
350
+ source_sig=sig, source_node=a_in,
351
+ target_sig=sig, target_node=a_out,
352
+ type="SUMMARY", var=None,
353
+ )
354
+ )
355
+
356
+ # Closure captures: bind at the nested callable's def statement.
357
+ for def_node, nested_sig in sorted(self.info.nested_defs):
358
+ nested_formals = formal_ids.get(nested_sig)
359
+ if not nested_formals:
360
+ continue
361
+ span = node_span.get(def_node, (-1, -1))
362
+ for fvar, fid in sorted(nested_formals.items()):
363
+ if not fvar.startswith(CAPTURE_PREFIX):
364
+ continue
365
+ name = fvar[len(CAPTURE_PREFIX):]
366
+ key = (def_node, f"{nested_sig}::{fvar}")
367
+ if key not in self.actual_in:
368
+ aid = self._alloc("actual_in", fvar, span, def_node)
369
+ self.actual_in[key] = aid
370
+ self.extra.append(PDGEdge(source=def_node, target=aid, type="CDG"))
371
+ for src, var in self._defs_reaching_call_matching(def_node, name):
372
+ self.extra.append(
373
+ PDGEdge(source=src, target=aid, type="DDG", var=var)
374
+ )
375
+ sdg_edges.append(
376
+ SDGEdge(
377
+ source_sig=self.info.signature,
378
+ source_node=self.actual_in[key],
379
+ target_sig=nested_sig, target_node=fid,
380
+ type="PARAM_IN", var=fvar,
381
+ )
382
+ )
383
+
384
+
385
+ def assemble_sdg(
386
+ infos: Dict[str, FunctionInfo],
387
+ summaries: Dict[str, FunctionSummary],
388
+ k: int,
389
+ ) -> ProgramGraphsIR:
390
+ """Stitch every function's PDG into the whole-program SDG."""
391
+ ir = ProgramGraphsIR(k_limit=k)
392
+
393
+ # Pass 1: solve each function against the final summaries and lay out its
394
+ # formal nodes (their ids must exist before callsites reference them).
395
+ assemblers: Dict[str, _FunctionAssembler] = {}
396
+ formal_ids: Dict[str, Dict[str, int]] = {}
397
+ for sig in sorted(infos):
398
+ info = infos[sig]
399
+ summary, facts, ddg = solve_function(info, summaries)
400
+ asm = _FunctionAssembler(info, summary, facts, ddg)
401
+ asm.build_formals()
402
+ assemblers[sig] = asm
403
+ formal_ids[sig] = dict(asm.formal_in)
404
+ formal_ids[f"{sig}<out>"] = dict(asm.formal_out)
405
+
406
+ # Pass 2: callsite actuals and cross-function edges.
407
+ sdg_edges: List[SDGEdge] = []
408
+ for sig in sorted(assemblers):
409
+ assemblers[sig].build_actuals(summaries, formal_ids, sdg_edges)
410
+
411
+ for sig, asm in assemblers.items():
412
+ ir.functions[sig] = FunctionGraphs(
413
+ pdg=asm.info.pdg,
414
+ ddg=asm.ddg,
415
+ param_nodes=asm.param_nodes,
416
+ extra_edges=asm.extra,
417
+ summary=asm.summary,
418
+ )
419
+
420
+ ir.sdg_edges = sorted(
421
+ set(sdg_edges),
422
+ key=lambda e: (e.source_sig, e.source_node, e.target_sig, e.target_node, e.type, e.var or ""),
423
+ )
424
+ return ir
@@ -0,0 +1,93 @@
1
+ ################################################################################
2
+ # Copyright IBM Corporation 2025
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ ################################################################################
16
+
17
+ """Stage 8 of the level-3 dataflow ladder: backward slicing as an SDG query.
18
+
19
+ The classic Horwitz–Reps–Binkley two-phase traversal, which is what makes the
20
+ slice *context-sensitive* without re-descending into callees:
21
+
22
+ - **Phase 1** walks backward over every dependence edge **except PARAM_OUT**:
23
+ it ascends from the criterion to callers (PARAM_IN/CALL reversed) and steps
24
+ *across* callsites through SUMMARY edges, but never descends into a callee.
25
+ - **Phase 2** starts from everything phase 1 reached and walks backward over
26
+ every edge **except PARAM_IN and CALL**: it descends into callees
27
+ (PARAM_OUT reversed) but never re-ascends — which is exactly what prevents
28
+ infeasible call–return mismatches.
29
+
30
+ Slicing consumes the assembled :class:`~codeanalyzer.dataflow.sdg.
31
+ ProgramGraphsIR`; taint is the same labeled traversal with a model pack and
32
+ is deliberately left to the CLDK SDK (language-independent once the SDG is
33
+ emitted — see #67).
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ from typing import Dict, List, Set, Tuple
39
+
40
+ from codeanalyzer.dataflow.sdg import ProgramGraphsIR
41
+
42
+ Node = Tuple[str, int] # (signature, node_id)
43
+
44
+
45
+ def _reverse_adjacency(ir: ProgramGraphsIR) -> Dict[Node, List[Tuple[Node, str]]]:
46
+ """target → [(source, edge_type)] over intra- and inter-procedural edges."""
47
+ radj: Dict[Node, List[Tuple[Node, str]]] = {}
48
+
49
+ def add(src: Node, tgt: Node, kind: str) -> None:
50
+ radj.setdefault(tgt, []).append((src, kind))
51
+
52
+ for sig, fg in ir.functions.items():
53
+ for e in fg.pdg.edges:
54
+ if e.type == "CDG":
55
+ add((sig, e.source), (sig, e.target), "CDG")
56
+ for e in fg.ddg:
57
+ add((sig, e.source), (sig, e.target), "DDG")
58
+ for e in fg.extra_edges:
59
+ add((sig, e.source), (sig, e.target), e.type)
60
+ for e in ir.sdg_edges:
61
+ add(
62
+ (e.source_sig, e.source_node),
63
+ (e.target_sig, e.target_node),
64
+ e.type,
65
+ )
66
+ return radj
67
+
68
+
69
+ def backward_slice(ir: ProgramGraphsIR, signature: str, node_id: int) -> Set[Node]:
70
+ """Context-sensitive backward slice of ``(signature, node_id)``."""
71
+ if signature not in ir.functions:
72
+ raise KeyError(f"unknown signature: {signature}")
73
+ radj = _reverse_adjacency(ir)
74
+ criterion: Node = (signature, node_id)
75
+
76
+ def sweep(seeds: Set[Node], skip: Set[str]) -> Set[Node]:
77
+ seen: Set[Node] = set()
78
+ stack = list(seeds)
79
+ while stack:
80
+ node = stack.pop()
81
+ if node in seen:
82
+ continue
83
+ seen.add(node)
84
+ for src, kind in radj.get(node, ()):
85
+ if kind in skip:
86
+ continue
87
+ if src not in seen:
88
+ stack.append(src)
89
+ return seen
90
+
91
+ phase1 = sweep({criterion}, skip={"PARAM_OUT"})
92
+ phase2 = sweep(phase1, skip={"PARAM_IN", "CALL"})
93
+ return phase1 | phase2