|
| 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 (assignment targets, def/class name |
| 245 | + // bindings, augmented assignment targets, for-loop targets, etc.) |
| 246 | + e.(Name).isDefinition() |
| 247 | + or |
| 248 | + // Tuple/List/Starred nodes in assignment or for-loop targets are |
| 249 | + // structural unpack patterns, not evaluations |
| 250 | + (e instanceof Tuple or e instanceof List or e instanceof Starred) and |
| 251 | + e = any(AssignStmt a).getATarget().getASubExpression*() |
| 252 | + or |
| 253 | + (e instanceof Tuple or e instanceof List or e instanceof Starred) and |
| 254 | + e = any(For f).getTarget().getASubExpression*() |
| 255 | + or |
| 256 | + // The decorator call node wrapping a function/class definition, |
| 257 | + // and its sub-expressions (the decorator name itself) |
| 258 | + e = any(FunctionExpr func).getADecoratorCall().getASubExpression*() |
| 259 | + or |
| 260 | + e = any(ClassExpr cls).getADecoratorCall().getASubExpression*() |
| 261 | + or |
| 262 | + // Augmented assignment (x += e): the implicit BinaryExpr for the operation |
| 263 | + e = any(AugAssign aug).getOperation() |
| 264 | + or |
| 265 | + // with-statement `as` variables are bindings |
| 266 | + (e instanceof Name or e instanceof Tuple or e instanceof List) and |
| 267 | + e = any(With w).getOptionalVars().getASubExpression*() |
| 268 | + or |
| 269 | + // except-clause exception type and `as` variable are part of except syntax |
| 270 | + exists(ExceptStmt ex | e = ex.getType() or e = ex.getName()) |
| 271 | + or |
| 272 | + // match/case pattern expressions are part of pattern syntax |
| 273 | + e.getParent+() instanceof Pattern |
| 274 | + or |
| 275 | + // Subscript/Attribute nodes on the LHS of an assignment are store |
| 276 | + // operations, not value expressions (including nested ones like d["a"][1]) |
| 277 | + (e instanceof Subscript or e instanceof Attribute) and |
| 278 | + e = any(AssignStmt a).getATarget().getASubExpression*() |
| 279 | + or |
| 280 | + // Match/case guard nodes are part of case syntax |
| 281 | + e instanceof Guard |
| 282 | + or |
| 283 | + // Yield/YieldFrom in statement position — the return value is |
| 284 | + // discarded and cannot be meaningfully annotated |
| 285 | + (e instanceof Yield or e instanceof YieldFrom) and |
| 286 | + e.getParent() instanceof ExprStmt |
| 287 | + or |
| 288 | + // Synthetic nodes inside desugared comprehensions |
| 289 | + e.getScope() = any(Comp c).getFunction() and |
| 290 | + ( |
| 291 | + e.(Name).getId() = ".0" |
| 292 | + or |
| 293 | + e instanceof Tuple and e.getParent() instanceof Yield |
| 294 | + ) |
| 295 | +} |
0 commit comments