Skip to content

Commit b46d74d

Browse files
committed
Python: Add self-validating CFG tests
These tests consist of various Python constructions (hopefully a somewhat comprehensive set) with specific timestamp annotations scattered throughout. When the tests are run using the Python 3 interpreter, these annotations are checked and compared to the "current timestamp" to see that they are in agreement. This is what makes the tests "self-validating". There are a few different kinds of annotations: the basic `t[4]` style (meaning this is executed at timestamp 4), the `t.dead[4]` variant (meaning this _would_ happen at timestamp 4, but it is in a dead branch), and `t.never` (meaning this is never executed at all). In addition to this, there is a query, MissingAnnotations, which checks whether we have applied these annotations maximally. Many expression nodes are not actually annotatable, so there is a sizeable list of excluded nodes for that query.
1 parent 6c52de9 commit b46d74d

22 files changed

+2151
-0
lines changed

python/ql/test/library-tests/ControlFlow/evaluation-order/MissingAnnotations.expected

Whitespace-only changes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Finds expressions in test functions that lack a timer annotation
3+
* and are not part of the timer mechanism or otherwise excluded.
4+
* An empty result means every annotatable expression is covered.
5+
*/
6+
7+
import python
8+
import TimerUtils
9+
10+
from TestFunction f, Expr e
11+
where
12+
e.getScope().getEnclosingScope*() = f and
13+
not isTimerMechanism(e, f) and
14+
not isUnannotatable(e)
15+
select e, "Missing annotation in $@", f, f.getName()
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
/**
2+
* Utility library for identifying timer annotations in evaluation-order tests.
3+
*
4+
* Identifies `expr @ t[n]` (matmul), `t(expr, n)` (call), and
5+
* `expr @ t.dead[n]` (dead-code) patterns, extracts timestamp values,
6+
* and provides predicates for traversing consecutive annotated CFG nodes.
7+
*/
8+
9+
import python
10+
11+
/**
12+
* A function decorated with `@test` from the timer module.
13+
* The first parameter is the timer object.
14+
*/
15+
class TestFunction extends Function {
16+
TestFunction() {
17+
this.getADecorator().(Name).getId() = "test" and
18+
this.getPositionalParameterCount() >= 1
19+
}
20+
21+
/** Gets the name of the timer parameter (first parameter). */
22+
string getTimerParamName() { result = this.getArgName(0) }
23+
}
24+
25+
/** Gets an IntegerLiteral from a timestamp expression (single int or tuple of ints). */
26+
private IntegerLiteral timestampLiteral(Expr timestamps) {
27+
result = timestamps
28+
or
29+
result = timestamps.(Tuple).getAnElt()
30+
}
31+
32+
/** A timer annotation in the AST. */
33+
private newtype TTimerAnnotation =
34+
/** `expr @ t[n]` or `expr @ t[n, m, ...]` */
35+
TMatmulAnnotation(TestFunction func, Expr annotated, Expr timestamps) {
36+
exists(BinaryExpr be |
37+
be.getOp() instanceof MatMult and
38+
be.getRight().(Subscript).getObject().(Name).getId() = func.getTimerParamName() and
39+
be.getScope().getEnclosingScope*() = func and
40+
annotated = be.getLeft() and
41+
timestamps = be.getRight().(Subscript).getIndex()
42+
)
43+
} or
44+
/** `t(expr, n)` */
45+
TCallAnnotation(TestFunction func, Expr annotated, Expr timestamps) {
46+
exists(Call call |
47+
call.getFunc().(Name).getId() = func.getTimerParamName() and
48+
call.getScope().getEnclosingScope*() = func and
49+
annotated = call.getArg(0) and
50+
timestamps = call.getArg(1)
51+
)
52+
} or
53+
/** `expr @ t.dead[n]` — dead-code annotation */
54+
TDeadAnnotation(TestFunction func, Expr annotated, Expr timestamps) {
55+
exists(BinaryExpr be |
56+
be.getOp() instanceof MatMult and
57+
be.getRight().(Subscript).getObject().(Attribute).getObject("dead").(Name).getId() =
58+
func.getTimerParamName() and
59+
be.getScope().getEnclosingScope*() = func and
60+
annotated = be.getLeft() and
61+
timestamps = be.getRight().(Subscript).getIndex()
62+
)
63+
} or
64+
/** `expr @ t.never` — annotation for code that should never be evaluated */
65+
TNeverAnnotation(TestFunction func, Expr annotated) {
66+
exists(BinaryExpr be |
67+
be.getOp() instanceof MatMult and
68+
be.getRight().(Attribute).getObject("never").(Name).getId() = func.getTimerParamName() and
69+
be.getScope().getEnclosingScope*() = func and
70+
annotated = be.getLeft()
71+
)
72+
}
73+
74+
/** A timer annotation (wrapping the newtype for a clean API). */
75+
class TimerAnnotation extends TTimerAnnotation {
76+
/** Gets a timestamp value from this annotation. */
77+
int getATimestamp() { exists(this.getTimestampExpr(result)) }
78+
79+
/** Gets the source expression for timestamp value `ts`. */
80+
IntegerLiteral getTimestampExpr(int ts) {
81+
result = timestampLiteral(this.getTimestampsExpr()) and
82+
result.getValue() = ts
83+
}
84+
85+
/** Gets the raw timestamp expression (single int or tuple). */
86+
abstract Expr getTimestampsExpr();
87+
88+
/** Gets the test function this annotation belongs to. */
89+
abstract TestFunction getTestFunction();
90+
91+
/** Gets the annotated expression (the LHS of `@` or the first arg of `t(...)`). */
92+
abstract Expr getAnnotatedExpr();
93+
94+
/** Gets the enclosing annotation expression (the `BinaryExpr` or `Call`). */
95+
abstract Expr getExpr();
96+
97+
/** Holds if this is a dead-code annotation (`t.dead[n]`). */
98+
predicate isDead() { this instanceof DeadTimerAnnotation }
99+
100+
/** Holds if this is a never-evaluated annotation (`t.never`). */
101+
predicate isNever() { this instanceof NeverTimerAnnotation }
102+
103+
string toString() { result = this.getExpr().toString() }
104+
105+
Location getLocation() { result = this.getExpr().getLocation() }
106+
}
107+
108+
/** A matmul-based timer annotation: `expr @ t[n]`. */
109+
class MatmulTimerAnnotation extends TMatmulAnnotation, TimerAnnotation {
110+
TestFunction func;
111+
Expr annotated;
112+
Expr timestamps;
113+
114+
MatmulTimerAnnotation() { this = TMatmulAnnotation(func, annotated, timestamps) }
115+
116+
override Expr getTimestampsExpr() { result = timestamps }
117+
118+
override TestFunction getTestFunction() { result = func }
119+
120+
override Expr getAnnotatedExpr() { result = annotated }
121+
122+
override BinaryExpr getExpr() { result.getLeft() = annotated }
123+
}
124+
125+
/** A call-based timer annotation: `t(expr, n)`. */
126+
class CallTimerAnnotation extends TCallAnnotation, TimerAnnotation {
127+
TestFunction func;
128+
Expr annotated;
129+
Expr timestamps;
130+
131+
CallTimerAnnotation() { this = TCallAnnotation(func, annotated, timestamps) }
132+
133+
override Expr getTimestampsExpr() { result = timestamps }
134+
135+
override TestFunction getTestFunction() { result = func }
136+
137+
override Expr getAnnotatedExpr() { result = annotated }
138+
139+
override Call getExpr() { result.getArg(0) = annotated }
140+
}
141+
142+
/** A dead-code timer annotation: `expr @ t.dead[n]`. */
143+
class DeadTimerAnnotation extends TDeadAnnotation, TimerAnnotation {
144+
TestFunction func;
145+
Expr annotated;
146+
Expr timestamps;
147+
148+
DeadTimerAnnotation() { this = TDeadAnnotation(func, annotated, timestamps) }
149+
150+
override Expr getTimestampsExpr() { result = timestamps }
151+
152+
override TestFunction getTestFunction() { result = func }
153+
154+
override Expr getAnnotatedExpr() { result = annotated }
155+
156+
override BinaryExpr getExpr() { result.getLeft() = annotated }
157+
}
158+
159+
/** A never-evaluated annotation: `expr @ t.never`. */
160+
class NeverTimerAnnotation extends TNeverAnnotation, TimerAnnotation {
161+
TestFunction func;
162+
Expr annotated;
163+
164+
NeverTimerAnnotation() { this = TNeverAnnotation(func, annotated) }
165+
166+
override Expr getTimestampsExpr() { none() }
167+
168+
override TestFunction getTestFunction() { result = func }
169+
170+
override Expr getAnnotatedExpr() { result = annotated }
171+
172+
override BinaryExpr getExpr() { result.getLeft() = annotated }
173+
}
174+
175+
/**
176+
* A CFG node corresponding to a timer annotation.
177+
*/
178+
class TimerCfgNode extends ControlFlowNode {
179+
private TimerAnnotation annot;
180+
181+
TimerCfgNode() { annot.getExpr() = this.getNode() }
182+
183+
/** Gets a timestamp value from this annotation. */
184+
int getATimestamp() { result = annot.getATimestamp() }
185+
186+
/** Gets the source expression for timestamp value `ts`. */
187+
IntegerLiteral getTimestampExpr(int ts) { result = annot.getTimestampExpr(ts) }
188+
189+
/** Gets the test function this annotation belongs to. */
190+
TestFunction getTestFunction() { result = annot.getTestFunction() }
191+
192+
/** Holds if this is a dead-code annotation. */
193+
predicate isDead() { annot.isDead() }
194+
195+
/** Holds if this is a never-evaluated annotation. */
196+
predicate isNever() { annot.isNever() }
197+
}
198+
199+
/**
200+
* Holds if `next` is the next timer annotation reachable from `n` via
201+
* CFG successors (both normal and exceptional), skipping non-annotated
202+
* intermediaries within the same scope.
203+
*/
204+
predicate nextTimerAnnotation(ControlFlowNode n, TimerCfgNode next) {
205+
next = n.getASuccessor() and
206+
next.getScope() = n.getScope()
207+
or
208+
exists(ControlFlowNode mid |
209+
mid = n.getASuccessor() and
210+
not mid instanceof TimerCfgNode and
211+
mid.getScope() = n.getScope() and
212+
nextTimerAnnotation(mid, next)
213+
)
214+
}
215+
216+
/**
217+
* Holds if `e` is part of the timer mechanism: a top-level timer
218+
* expression or a (transitive) sub-expression of one.
219+
*/
220+
predicate isTimerMechanism(Expr e, TestFunction f) {
221+
exists(TimerAnnotation a |
222+
a.getTestFunction() = f and
223+
e = a.getExpr().getASubExpression*()
224+
)
225+
}
226+
227+
/**
228+
* Holds if expression `e` cannot be annotated due to Python syntax
229+
* limitations (e.g., it is a definition target, a pattern, or part
230+
* of a decorator application).
231+
*/
232+
predicate isUnannotatable(Expr e) {
233+
// Function/class definitions
234+
e instanceof FunctionExpr
235+
or
236+
e instanceof ClassExpr
237+
or
238+
// Docstrings are string literals used as expression statements
239+
e instanceof StringLiteral and e.getParent() instanceof ExprStmt
240+
or
241+
// Function parameters are bound by the call, not evaluated in the body
242+
e instanceof Parameter
243+
or
244+
// Name nodes that are definitions or deletions (assignment targets, def/class
245+
// name bindings, augmented assignment targets, for-loop targets, del targets)
246+
e.(Name).isDefinition()
247+
or
248+
e.(Name).isDeletion()
249+
or
250+
// Tuple/List/Starred nodes in assignment or for-loop targets are
251+
// structural unpack patterns, not evaluations
252+
(e instanceof Tuple or e instanceof List or e instanceof Starred) and
253+
e = any(AssignStmt a).getATarget().getASubExpression*()
254+
or
255+
(e instanceof Tuple or e instanceof List or e instanceof Starred) and
256+
e = any(For f).getTarget().getASubExpression*()
257+
or
258+
// The decorator call node wrapping a function/class definition,
259+
// and its sub-expressions (the decorator name itself)
260+
e = any(FunctionExpr func).getADecoratorCall().getASubExpression*()
261+
or
262+
e = any(ClassExpr cls).getADecoratorCall().getASubExpression*()
263+
or
264+
// Augmented assignment (x += e): the implicit BinaryExpr for the operation
265+
e = any(AugAssign aug).getOperation()
266+
or
267+
// with-statement `as` variables are bindings
268+
(e instanceof Name or e instanceof Tuple or e instanceof List) and
269+
e = any(With w).getOptionalVars().getASubExpression*()
270+
or
271+
// except-clause exception type and `as` variable are part of except syntax
272+
exists(ExceptStmt ex | e = ex.getType() or e = ex.getName())
273+
or
274+
// match/case pattern expressions are part of pattern syntax
275+
e.getParent+() instanceof Pattern
276+
or
277+
// Subscript/Attribute nodes on the LHS of an assignment are store
278+
// operations, not value expressions (including nested ones like d["a"][1])
279+
(e instanceof Subscript or e instanceof Attribute) and
280+
e = any(AssignStmt a).getATarget().getASubExpression*()
281+
or
282+
// Match/case guard nodes are part of case syntax
283+
e instanceof Guard
284+
or
285+
// Yield/YieldFrom in statement position — the return value is
286+
// discarded and cannot be meaningfully annotated
287+
(e instanceof Yield or e instanceof YieldFrom) and
288+
e.getParent() instanceof ExprStmt
289+
or
290+
// Synthetic nodes inside desugared comprehensions
291+
e.getScope() = any(Comp c).getFunction() and
292+
(
293+
e.(Name).getId() = ".0"
294+
or
295+
e instanceof Tuple and e.getParent() instanceof Yield
296+
)
297+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Assert and raise statement evaluation order."""
2+
3+
from timer import test
4+
5+
6+
@test
7+
def test_assert_true(t):
8+
x = True @ t[0]
9+
assert x @ t[1]
10+
y = 1 @ t[2]
11+
12+
13+
@test
14+
def test_assert_true_with_message(t):
15+
x = True @ t[0]
16+
assert x @ t[1], "msg" @ t.dead[2]
17+
y = 1 @ t[2]
18+
19+
20+
@test
21+
def test_assert_false_caught(t):
22+
try:
23+
x = False @ t[0]
24+
assert x @ t[1], "fail" @ t[2]
25+
except AssertionError:
26+
y = 1 @ t[3]
27+
28+
29+
@test
30+
def test_raise_caught(t):
31+
try:
32+
x = 1 @ t[0]
33+
raise ((ValueError @ t[1])("test" @ t[2]) @ t[3])
34+
except ValueError:
35+
y = 2 @ t[4]
36+
37+
38+
@test
39+
def test_raise_from_caught(t):
40+
try:
41+
x = 1 @ t[0]
42+
raise ((ValueError @ t[1])("test" @ t[2]) @ t[3]) from ((RuntimeError @ t[4])("cause" @ t[5]) @ t[6])
43+
except ValueError:
44+
y = 2 @ t[7]
45+
46+
47+
@test
48+
def test_bare_reraise(t):
49+
try:
50+
try:
51+
raise ((ValueError @ t[0])("test" @ t[1]) @ t[2])
52+
except ValueError:
53+
x = 1 @ t[3]
54+
raise
55+
except ValueError:
56+
y = 2 @ t[4]

0 commit comments

Comments
 (0)