-
Notifications
You must be signed in to change notification settings - Fork 1.9k
IR-based guards library #197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
@dave-bartolomeo there's a small change to the IR in the first two commits that you should look at. I expect one of @geoffw0 or @jbj to do the main review. |
| } | ||
|
|
||
| class CompareLTInstruction extends CompareInstruction { | ||
| class RelationalInstruction extends CompareInstruction { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know that many of the classes in this file are missing QLDoc comments, but please add QLDoc for the new class and its abstract methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added documentation
dave-bartolomeo
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After you make any changes to IR library files, please run python3 buildutils-internal\scripts\pr-checks\sync-identical-files.py --latest to sync the copies of those files with the version that you just changed.
|
Ran the script and pushed changes |
dave-bartolomeo
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IR changes LGTM. I'll let the others review the rest.
|
@dave-bartolomeo it occurred to me that the IR has |
I don't know, but when I experimented with a nullness query for IR I found that |
jbj
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great stuff. Can you describe how we should interpret the changes in test results vs. the current library?
| /** | ||
| * Holds if this relational instruction is strict (is not an "or-equal" instruction). | ||
| */ | ||
| abstract predicate isStrict(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please avoid abstract predicates (and classes) in the Instruction class hierarchy. Users should be able to extend these classes without getting warnings about unimplemented predicates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
| * operands of logical operators but not switch statements. Note that `&&` and `||` | ||
| * don't have an explicit representation in the IR, and therefore will not appear as | ||
| * IRGuardConditions. | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we improve this? @dave-bartolomeo, what do you think about adding a Nop instruction before a && or || or ! that gets compiled into jumps in the IR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think there's a useful place to put those: you'd want them to be unconditional, but they also need to depend on their right operand which is only executed conditionally...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd put Nop where we put the logical operators in the CFG of AST nodes: before its operands. So the a && b in if (A && B) would get the IR CFG Nop -> A -> [ more edges ], where the Nop instruction's getAST() is &&.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dave-bartolomeo @jbj Do we want to make these changes, and if so, do we want to do it as part of this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On further thought, I don't think that adding Nops to represent these values will actually improve our analysis of the IR. It will make mapping back to the AST easier, but @dave-bartolomeo had an alternate proposal that involved tagging the ConditionalBranchInstructions with the logical expression that they are associated with.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rdmarsh2 you're right, I don't think the changes we're discussing here need to block this PR.
| succ.dominates(controlled) and | ||
| forall(IRBlock pred | ||
| | pred.getASuccessor() = succ | ||
| | pred = thisblock or succ.dominates(pred)))) // removed reachability condition - is that OK? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We generally need reachability when doing anything with dominance because the definition of dominance involves "for all paths from the function entry to this block, something holds", and when there are no such paths then anything holds.
@dave-bartolomeo are all IRBlocks reachable from a function entry? Will they still be if we start doing CFG pruning? I don't know how we handle expressions that are not in a function, like initializers of variables that are run before main.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every IRBlock is a member of exactly one FunctionIR, which corresponds to a single Function. Expressions that are not in a function don't have IR currently.
There's no particular guarantee that a given IRBlock is reachable from the entry block of its FunctionIR, though. Right now, we construct blocks regardless of reachability. My assumption was that we'd want to keep unreachable blocks, so that we could detect unreachable code in a query. If we decide that, for our most common usage, it would be better to leave unreachable code out of the IR, though, I think I'd be OK with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case I think we need to add back the reachability requirement here. I suggest adding an IRBlock.isReachableFromFunctionEntry() predicate since this sort of side condition will be needed on most code that uses the dominance predicates that are already exposed on IRBlock.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm adding that predicate and using it here - it causes one additional result in the test, which is actually a false positive because we don't have exception edges in the IR yet.
| /** Gets the underlying expression of `e`. */ | ||
| private Expr remove_conversions(Expr e) { | ||
| if e instanceof Conversion | ||
| then result = e.(Conversion).getExpr*() and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This * can be a +, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, fixed
| } | ||
|
|
||
| /** Gets the underlying expression of `e`. */ | ||
| private Expr remove_conversions(Expr e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing that this sort of helper predicate will often be needed when we translate back from IR to AST. How about building it into the IR library so multiple libraries don't all have to apply this trick? We can split Instruction.getAST into two versions: getConvertedAST and getUnconvertedAST. Then the caller is forced to choose which one is intended. @dave-bartolomeo ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
rdmarsh2
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some commentary about the test results, I'll fix the conversion issue in my next push
| | test.c:146:7:146:8 | ! ... | | ||
| | test.c:146:8:146:8 | x | | ||
| | test.cpp:18:8:18:10 | call to get | | ||
| | test.cpp:18:8:18:12 | (bool)... | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This entry is a mistake - I'll add a call to remove_conversions in the charpred of GuardConditionFromIR
| | test.c:126:7:126:7 | 1 | true | 126 | 128 | | ||
| | test.c:126:7:126:7 | 1 | true | 131 | 131 | | ||
| | test.c:126:7:126:7 | 1 | true | 131 | 132 | | ||
| | test.c:126:7:126:7 | 1 | true | 134 | 123 | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These results are constant-folded in IR generation
| | 18 | call to get != call to get+0 when (bool)... is true | | ||
| | 18 | call to get != call to get+0 when call to get is true | | ||
| | 18 | call to get == call to get+0 when (bool)... is false | | ||
| | 18 | call to get == call to get+0 when call to get is false | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The conversion to bool here is the same result as above. The equality constraints are new results of comparesEq, which I believe are correct, but may not be particularly useful.
| | test.c:131:7:131:7 | b | true | 131 | 132 | | ||
| | test.c:137:7:137:7 | 0 | false | 142 | 136 | | ||
| | test.c:137:7:137:7 | 0 | true | 137 | 138 | | ||
| | test.c:137:7:137:7 | 0 | true | 138 | 139 | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what's going on in the old Guards library, but I think the new results are correct
| | test.cpp:18:8:18:10 | call to get | false | 20 | 16 | | ||
| | test.cpp:18:8:18:10 | call to get | true | 19 | 19 | | ||
| | test.cpp:18:8:18:12 | (bool)... | true | 19 | 19 | | ||
| | test.cpp:31:7:31:13 | ... == ... | false | 30 | 30 | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what this block is. Something to do with the throw?
| | test.cpp:18:8:18:12 | (bool)... | true | 19 | 19 | | ||
| | test.cpp:31:7:31:13 | ... == ... | false | 30 | 30 | | ||
| | test.cpp:31:7:31:13 | ... == ... | false | 34 | 34 | | ||
| | test.cpp:31:7:31:13 | ... == ... | true | 30 | 30 | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same with this one.
9ed08d0 to
846c5b8
Compare
For ease of reviewing, I've checked in the .expected files from the AST-based guards library. The next commit accepts output for these tests and adds tests that use getAST rather than the translation layer.
846c5b8 to
9011e13
Compare
jbj
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is performance on a large snapshot? Can you share the summary of the most expensive predicates from a run where the cache was empty (or only the IR was cached)?
| or | ||
| // no binary operators in the IR | ||
| exists(Instruction ir | | ||
| this.(BinaryLogicalOperation).getAnOperand().getFullyConverted() = ir.getAST() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should Instruction ir here be IRGuardCondition ir? Otherwise I don't understand this case. Also, should there be a condition corresponding to the not exists ... in the !x case? Please add a comment about what that condition is for. If it's for filtering out the !x in y = !x; then don't we also want to filter out the a && b in y = a && b;?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is to avoid having the same expression be both a GuardConditionFromIR and a GuardConditionFromShortCircuitNot, as in
y = !x;
if(y) {
...
}
|
Running IRGuardsEnsure.ql on ChakraCore with the IR cached: |
|
And running ASTGuards.ql: |
|
(both of those are just the predicates that took more than 500ms) |
The IR for the conversion to bool results in a comparison where the left hand side is not the result of any expression in the AST, so they can't be usefully converted back to the AST
|
I'm considering merging the tests into one file with multiple query predicates to reduce compilation time; the cache is discarded between tests, so there's currently about 10 minutes of unnecessary recompilation of the IR library. @jbj does merging these tests sound reasonable to you? |
This is motivated by test performance; IR compilation happens separately for each test and takes a bit over a minute, so combining these 8 tests saves about 10 minutes of test running.
|
I've raised the compilation cache issue on Slack, and improvements are coming in 1.19. That won't be out until Christmas, so it sounds reasonable to merge those tests into one file for now. |
|
I don't see any unaddressed comments. @jbj I think this is ready to merge |
Sync Main (autogenerated)
This PR adds a new library for guard conditions based on the IR. There's a shim layer intended to provide the same API and results as the existing guards library, but it's not precisely identical.
The first two changes add a
RelationalInstructionclass to hold some useful member predicates. The third adds the library and a copy of the tests from the old library. The fourth accepts test output and adds a second set of tests that don't go through the shim layer.