Skip to content

Commit 79db96c

Browse files
yoffCopilot
andcommitted
Python: introduce shared-SSA adapter on the new CFG
Adds 'python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll', a minimal Python SSA implementation built on the shared SSA library ('codeql.ssa.Ssa::Make<Location, Cfg, Input>'). The structure mirrors Java's adapter at 'java/ql/lib/semmle/code/java/dataflow/internal/SsaImpl.qll'. Key design choices: * 'SourceVariable' wraps 'Py::Variable'. Only variables that are read or deleted somewhere are tracked - write-only variables don't benefit from SSA construction. * Variable references are positional ('BasicBlock', 'int') pairs looked up via 'Cfg::NameNode.defines'/'.uses'/'.deletes' (which themselves are one-line bridges to AST-level 'Name.defines' etc.). * Parameter writes are not synthesised: parameter Name nodes are already wired into the CFG (per the earlier C#-style parameter extension in 'AstNodeImpl.qll'), so the regular 'variableWrite' path handles them at their natural CFG index. * Non-local / captured / global / builtin variables read in a scope but not written in it receive a synthetic entry definition at index '-1' of the scope's entry basic block. This matches Java's 'hasEntryDef'. * 'del x' is modelled as a certain write at the deletion site. Includes an inline-expectations test under 'python/ql/test/library-tests/dataflow-new-ssa/' covering: plain parameter pass-through, simple assignment + read, reassignment with dead-write pruning, if/else with phi insertion at the join, and an undefined-name read (currently a known limitation - no SSA flow without an enclosing definition). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8448489 commit 79db96c

4 files changed

Lines changed: 278 additions & 0 deletions

File tree

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/**
2+
* Provides the Python SSA implementation built on the new (shared) CFG.
3+
*
4+
* Mirrors the Java SSA adapter at
5+
* `java/ql/lib/semmle/code/java/dataflow/internal/SsaImpl.qll`:
6+
* an `InputSig` is defined in terms of positional `(BasicBlock, int)`
7+
* variable references, and the shared
8+
* `codeql.ssa.Ssa::Make<Location, Cfg, Input>` module is then
9+
* instantiated.
10+
*
11+
* `SourceVariable` is the AST-level `Py::Variable`. Variable references
12+
* are looked up via the CFG facade's `NameNode.defines`/`uses`/`deletes`
13+
* predicates, which themselves are one-line bridges to AST-level
14+
* `Name.defines`/`uses`/`deletes`.
15+
*
16+
* Implicit-entry definitions are inserted for:
17+
* - non-local / global / builtin variables that are read in the scope
18+
* but never assigned (no enclosing CFG node defines them),
19+
* - captured variables (variables defined in an enclosing scope that
20+
* are read inside the scope), and
21+
* - parameters, but only if the corresponding parameter name is *not*
22+
* itself a CFG node. With the C#-style parameter wiring already
23+
* installed in `AstNodeImpl.qll`, parameter names *are* CFG nodes,
24+
* so the regular `variableWrite` path handles them — no `i = -1`
25+
* entry is needed for ordinary parameters.
26+
*/
27+
overlay[local?]
28+
module;
29+
30+
private import python as Py
31+
private import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl
32+
private import semmle.python.controlflow.internal.Cfg as Cfg
33+
private import codeql.ssa.Ssa as SsaImplCommon
34+
private import codeql.controlflow.BasicBlock as BB
35+
36+
/**
37+
* Adapts the Python `Cfg` facade to the shared SSA library's `CfgSig`.
38+
* All members are inherited from `Cfg::ControlFlowNode` and
39+
* `Cfg::BasicBlock`.
40+
*/
41+
private module CfgForSsa implements BB::CfgSig<Py::Location> {
42+
class ControlFlowNode = CfgImpl::ControlFlowNode;
43+
44+
class BasicBlock = CfgImpl::BasicBlock;
45+
46+
class EntryBasicBlock = CfgImpl::Cfg::EntryBasicBlock;
47+
48+
predicate dominatingEdge = CfgImpl::Cfg::dominatingEdge/2;
49+
}
50+
51+
/**
52+
* A source variable for SSA. Wraps a Python `Variable` (the AST-level
53+
* notion of a named binding within a scope) so that the shared SSA
54+
* implementation can use it as a `SourceVariable`.
55+
*
56+
* We only track variables that are read at least once in their scope —
57+
* tracking write-only variables is unnecessary work.
58+
*/
59+
private newtype TSsaSourceVariable =
60+
TPyVar(Py::Variable v) {
61+
// Has a use somewhere — read-relevant for SSA.
62+
exists(Cfg::NameNode n | n.uses(v))
63+
or
64+
// Or has a deletion (treated as a write that destroys the value).
65+
exists(Cfg::NameNode n | n.deletes(v))
66+
}
67+
68+
/**
69+
* A source variable for SSA, wrapping a Python AST `Variable`.
70+
*/
71+
class SsaSourceVariable extends TSsaSourceVariable {
72+
/** Gets the underlying Python AST variable. */
73+
Py::Variable getVariable() { this = TPyVar(result) }
74+
75+
/** Gets a textual representation of this source variable. */
76+
string toString() { result = this.getVariable().toString() }
77+
78+
/** Gets the location of this source variable. */
79+
Py::Location getLocation() { result = this.getVariable().getScope().getLocation() }
80+
}
81+
82+
/**
83+
* Holds if `v` is a non-local read in scope `s`, in the sense that `s`
84+
* uses `v` but does not write it within `s`. This includes globals,
85+
* builtins, and variables captured from an enclosing function scope.
86+
*/
87+
private predicate nonLocalReadIn(Py::Variable v, Py::Scope s) {
88+
exists(Cfg::NameNode n |
89+
n.uses(v) and
90+
n.getScope() = s and
91+
not exists(Cfg::NameNode def | def.defines(v) and def.getScope() = s)
92+
)
93+
}
94+
95+
/**
96+
* Holds if `v` should have an implicit entry definition at the start of
97+
* scope `s`. This covers:
98+
* - non-local / global / builtin variables (defined outside `s`), and
99+
* - captured variables (defined in an enclosing scope but read here).
100+
*
101+
* Parameters are *not* included: their bound `Name` is itself a CFG
102+
* node (per the C#-style parameter wiring), so `variableWrite` fires at
103+
* the parameter's natural CFG index.
104+
*/
105+
private predicate hasEntryDef(SsaSourceVariable v, Py::Scope s) {
106+
nonLocalReadIn(v.getVariable(), s)
107+
}
108+
109+
/**
110+
* Gets the entry basic block of scope `s`, where implicit entry
111+
* definitions are placed (at synthetic index `-1`).
112+
*/
113+
private CfgImpl::BasicBlock entryBlock(Py::Scope s) {
114+
exists(CfgImpl::ControlFlowNode entry |
115+
entry instanceof CfgImpl::ControlFlow::EntryNode and
116+
entry.getEnclosingCallable().asScope() = s and
117+
result = entry.getBasicBlock()
118+
)
119+
}
120+
121+
/**
122+
* The SSA `InputSig` for Python. References are positional
123+
* `(BasicBlock, int)` pairs into the new CFG.
124+
*/
125+
private module SsaImplInput implements SsaImplCommon::InputSig<Py::Location, CfgImpl::BasicBlock> {
126+
class SourceVariable = SsaSourceVariable;
127+
128+
predicate variableWrite(CfgImpl::BasicBlock bb, int i, SourceVariable v, boolean certain) {
129+
// Explicit binding at a CFG node — includes assignments,
130+
// parameter Names (wired in via the C# pattern), exception-handler
131+
// `as`-bindings, import aliases, and match-pattern captures.
132+
exists(Cfg::NameNode n |
133+
bb.getNode(i) = n and
134+
n.defines(v.getVariable()) and
135+
certain = true
136+
)
137+
or
138+
// `del x` — removes the binding. Modelled as a certain write that
139+
// makes any subsequent read invalid.
140+
exists(Cfg::NameNode n |
141+
bb.getNode(i) = n and
142+
n.deletes(v.getVariable()) and
143+
certain = true
144+
)
145+
or
146+
// Implicit entry definition for non-local / captured / global /
147+
// builtin variables read in the scope.
148+
bb = entryBlock(v.getVariable().getScope()) and
149+
hasEntryDef(v, v.getVariable().getScope()) and
150+
i = -1 and
151+
certain = true
152+
}
153+
154+
predicate variableRead(CfgImpl::BasicBlock bb, int i, SourceVariable v, boolean certain) {
155+
exists(Cfg::NameNode n |
156+
bb.getNode(i) = n and
157+
n.uses(v.getVariable()) and
158+
certain = true
159+
)
160+
}
161+
}
162+
163+
/**
164+
* The shared SSA instantiation for Python.
165+
*
166+
* Members:
167+
* - `Definition` — the union of explicit, uncertain, and phi definitions
168+
* - `WriteDefinition`, `UncertainWriteDefinition`, `PhiNode`
169+
* - the standard SSA predicates (`getAUse`, `getAnUltimateDefinition`, ...).
170+
*/
171+
module Ssa = SsaImplCommon::Make<Py::Location, CfgForSsa, SsaImplInput>;
172+
173+
final class Definition = Ssa::Definition;
174+
175+
final class WriteDefinition = Ssa::WriteDefinition;
176+
177+
final class UncertainWriteDefinition = Ssa::UncertainWriteDefinition;
178+
179+
final class PhiNode = Ssa::PhiNode;

python/ql/test/library-tests/dataflow-new-ssa/SsaTest.expected

Whitespace-only changes.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Inline-expectations test for the new-CFG SSA adapter
3+
* (`semmle.python.dataflow.new.internal.SsaImpl`).
4+
*
5+
* Tags:
6+
* - `def=<var>`: there is an SSA write definition of `<var>` at this
7+
* line (parameter init, plain assignment, augmented assignment,
8+
* exception-handler binding, deletion, etc.).
9+
* - `use=<var>`: `<var>` is used at this line, and some SSA definition
10+
* of `<var>` reaches the read.
11+
* - `phi=<var>`: there is an SSA phi definition of `<var>` whose BB
12+
* starts on this line.
13+
*/
14+
15+
import python
16+
import semmle.python.dataflow.new.internal.SsaImpl as SsaImpl
17+
import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl
18+
import semmle.python.controlflow.internal.Cfg as Cfg
19+
import utils.test.InlineExpectationsTest
20+
21+
module SsaTest implements TestSig {
22+
string getARelevantTag() { result = ["def", "use", "phi"] }
23+
24+
predicate hasActualResult(Location location, string element, string tag, string value) {
25+
// A `def=<id>` fires when an SSA WriteDefinition is at a CFG node
26+
// on the given line.
27+
exists(SsaImpl::Ssa::WriteDefinition def, CfgImpl::BasicBlock bb, int i, Cfg::NameNode n |
28+
def.definesAt(_, bb, i) and
29+
bb.getNode(i) = n and
30+
tag = "def" and
31+
location = n.getLocation() and
32+
element = n.toString() and
33+
value = n.getId()
34+
)
35+
or
36+
// A `use=<id>` fires when an SSA Definition reaches a read at this
37+
// CFG node.
38+
exists(SsaImpl::Ssa::Definition def, CfgImpl::BasicBlock bb, int i, Cfg::NameNode n |
39+
SsaImpl::Ssa::ssaDefReachesRead(_, def, bb, i) and
40+
bb.getNode(i) = n and
41+
tag = "use" and
42+
location = n.getLocation() and
43+
element = n.toString() and
44+
value = n.getId()
45+
)
46+
or
47+
// A `phi=<id>` fires when there is a phi node whose BB's first
48+
// CFG node is on the given line.
49+
exists(SsaImpl::Ssa::PhiNode phi, CfgImpl::BasicBlock bb |
50+
phi.definesAt(_, bb, _) and
51+
tag = "phi" and
52+
location = bb.getNode(0).getLocation() and
53+
element = bb.toString() and
54+
value = phi.getSourceVariable().(SsaImpl::SsaSourceVariable).getVariable().getId()
55+
)
56+
}
57+
}
58+
59+
import MakeTest<SsaTest>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Basic SSA tests for the new-CFG SSA adapter.
2+
#
3+
# The shared SSA implementation prunes its construction by liveness:
4+
# definitions of variables that are not read are never materialised.
5+
# This is by design — write-only variables would only bloat the SSA
6+
# graph. Tests therefore must always include a read of each variable
7+
# being verified.
8+
#
9+
# Annotations:
10+
# def=<var>: there is an SSA write definition of <var> at this line
11+
# use=<var>: <var> is used here and the read resolves to some def
12+
13+
14+
def basic_param(x): # $ def=x
15+
return x # $ use=x
16+
17+
18+
def basic_assign():
19+
y = 1 # $ def=y
20+
return y # $ use=y
21+
22+
23+
def reassignment():
24+
x = 1
25+
x = 2 # $ def=x
26+
return x # $ use=x
27+
28+
29+
def if_else_phi(cond): # $ def=cond
30+
if cond: # $ use=cond phi=x
31+
x = 1 # $ def=x
32+
else:
33+
x = 2 # $ def=x
34+
return x # $ use=x
35+
36+
37+
def use_global():
38+
return some_undefined # known limitation: undefined globals not resolved here
39+
40+

0 commit comments

Comments
 (0)