@@ -192,6 +192,127 @@ def build_function_pdgs(
192192 return infos , func_asts
193193
194194
195+ def emit_l3_body (
196+ app : PyApplication ,
197+ infos : Dict [str , FunctionInfo ],
198+ sig_to_id : Dict [str , str ],
199+ graphs : Set [str ],
200+ ) -> None :
201+ """Project each callable's syntactic PDG onto the v2 tree at L3.
202+
203+ For every callable that produced a ``FunctionInfo`` in
204+ :func:`build_function_pdgs` (syntactic oracle), this writes onto the
205+ matching ``PyCallable`` in ``app``'s symbol table:
206+
207+ * ``body`` — one node per CFG node, keyed by its ordinal id
208+ (``<can:// id>@entry``/``@exit`` for the synthetic bookends,
209+ ``<can:// id>@line:col`` for real statements). A statement position an
210+ L1 pass already materialized as a ``call`` node keeps its ``call`` kind
211+ and L2-resolved ``callee``; it is only re-keyed onto its ordinal id (so
212+ the edge lists resolve to it and it is not duplicated) and given the
213+ byte-offset ``span`` L1 could not compute.
214+ * ``cfg`` — one ``CfgEdge`` per CFG edge, endpoints as ordinal ids.
215+ * ``cdg`` — the PDG's control-dependence edges.
216+ * ``ddg`` — the PDG's syntactic def-use edges, each with ``prov=["ssa"]``
217+ (no points-to provenance at L3; that is the L4 delta).
218+
219+ ``graphs`` scopes the edge lists exactly as the dormant
220+ :func:`to_program_graphs` does: ``cfg`` needs ``"cfg"``; ``cdg`` needs
221+ ``"pdg"``/``"sdg"``; ``ddg`` needs those or ``"dfg"``. ``body`` is always
222+ populated. Callables absent from ``infos`` (unrecovered AST) are skipped.
223+ """
224+ from codeanalyzer .dataflow .identity import IdentityMap
225+ from codeanalyzer .schema .py_schema import (
226+ BodyNode ,
227+ CdgEdge ,
228+ CfgEdge ,
229+ DdgEdge ,
230+ Span ,
231+ byte_offsets ,
232+ )
233+
234+ want_pdg = bool ({"pdg" , "sdg" } & graphs )
235+ want_cfg = "cfg" in graphs
236+ want_ddg = want_pdg or "dfg" in graphs
237+
238+ def _span_of (source : str , node ) -> Optional ["Span" ]:
239+ if not source or node .start_line < 1 :
240+ return None
241+ return Span (
242+ start = (node .start_line , node .start_column ),
243+ end = (node .end_line , node .end_column ),
244+ bytes = byte_offsets (
245+ source ,
246+ node .start_line ,
247+ node .start_column ,
248+ node .end_line ,
249+ node .end_column ,
250+ ),
251+ )
252+
253+ for module in app .symbol_table .values ():
254+ source = module .source
255+ for pycallable , _chain in _walk_callables (module ):
256+ info = infos .get (pycallable .signature )
257+ if info is None :
258+ continue
259+ pdg = info .pdg
260+ callable_id = sig_to_id .get (pycallable .signature ) or pycallable .id
261+ im = IdentityMap .for_function (callable_id , pdg )
262+
263+ for node in pdg .cfg .nodes :
264+ ordinal = im .ordinal (node .id )
265+ if node .id == pdg .cfg .entry_id :
266+ pycallable .body [ordinal ] = BodyNode (kind = "entry" )
267+ continue
268+ if node .id == pdg .cfg .exit_id :
269+ pycallable .body [ordinal ] = BodyNode (kind = "exit" )
270+ continue
271+ span = _span_of (source , node )
272+ # An L1 `call` node was keyed by its "line:col"; if this CFG node
273+ # sits at the same position, keep that node's `call` kind and
274+ # resolved `callee` and merely re-key it onto the ordinal id
275+ # (dedup + endpoint resolution), filling any missing span.
276+ existing = pycallable .body .get (ordinal )
277+ if existing is None :
278+ existing = pycallable .body .pop (
279+ f"{ node .start_line } :{ node .start_column } " , None
280+ )
281+ if existing is not None :
282+ if existing .span is None and span is not None :
283+ existing .span = span
284+ pycallable .body [ordinal ] = existing
285+ continue
286+ pycallable .body [ordinal ] = BodyNode (kind = node .kind , span = span )
287+
288+ if want_cfg :
289+ pycallable .cfg = [
290+ CfgEdge (
291+ src = im .ordinal (e .source ),
292+ dst = im .ordinal (e .target ),
293+ kind = e .kind ,
294+ )
295+ for e in pdg .cfg .edges
296+ ]
297+ if want_pdg :
298+ pycallable .cdg = [
299+ CdgEdge (src = im .ordinal (e .source ), dst = im .ordinal (e .target ))
300+ for e in pdg .edges
301+ if e .type == "CDG"
302+ ]
303+ if want_ddg :
304+ pycallable .ddg = [
305+ DdgEdge (
306+ src = im .ordinal (e .source ),
307+ dst = im .ordinal (e .target ),
308+ var = e .var ,
309+ prov = ["ssa" ],
310+ )
311+ for e in pdg .edges
312+ if e .type == "DDG"
313+ ]
314+
315+
195316def build_program_graphs (
196317 app : PyApplication ,
197318 k : int = DEFAULT_K_LIMIT ,
0 commit comments