-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Ruby: Add case string comparison barrier guard #11114
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
|
|
||
| final override SplatExpr getExpr() { result = super.getExpr() } | ||
|
|
||
| final ExprCfgNode getOperand() { result.getExpr() = e.(SplatExpr).getOperand() } |
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 add a class SplatExprChildMapping and change this to
| final ExprCfgNode getOperand() { result.getExpr() = e.(SplatExpr).getOperand() } | |
| final ExprCfgNode getLhs() { e.hasCfgChild(e.getOperand(), this, result) } |
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.
Sure, but why (the extra class)?
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.
Well regardless of why, I think that's fixed the issue below with SplatExprCfgNode.getAnOperand() not giving results for array literals. Still, in the absence of any documentation comments, it would be nice to know why.
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.
There may be multiple CFG nodes for the same AST node due to control flow graph splitting. The e.hasCfgChild predicate picks the CfgNode that matches the AstNode child node and is connected to the current CfgNode.
| branch = true and | ||
| exists(CfgNodes::ExprNodes::CaseExprCfgNode case | | ||
| case.getValue() = testedNode and | ||
| exists(CfgNodes::ExprNodes::WhenClauseCfgNode branchNode | |
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.
What about InClauses?
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 doesn't handle in clauses. It looks much harder to be sure that an expression is guarded by an in pattern, because the patterns can be complex. I guess we'd have to check that there are no new variable bindings in the pattern or something. Anyway, I think that's something to tackle in the future.
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've pushed a commit that handles very simple in clauses where the pattern is a single string literal, such as
case foo
in "foo"
foo
end| ) { | ||
| branch = true and | ||
| exists(CfgNodes::ExprNodes::CaseExprCfgNode case | | ||
| case.getValue() = testedNode 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.
Should we also handle the case where getValue does not exist?
case
when foo == "foo" then ...
end
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.
Weird, I didn't know you could do this. This appears to already be covered by our existing StringConstCompareBarrier. I'll add a test for it.
| pattern instanceof ExprNodes::StringLiteralCfgNode | ||
| or | ||
| // array literals behave weirdly in the CFG so we need to drop down to the AST level for this bit | ||
| // specifically: `SplatExprCfgNode.getOperand()` does not return results for array literals |
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 a bit surprised by this comment. It could be that there is a bug in the CFG library, but other than that getOperand should return an array literal.
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.
Your suggested change for SplatExprCfgNode appears to have fixed this. I've pushed an update.
This recognises barriers of the form
STRINGS = ["foo", "bar"]
case foo
when "some string literal"
foo
when *["other", "strings"]
foo
when *STRINGS
foo
end
where the reads of `foo` inside each `when` are guarded by the comparison
of `foo` with the string literals.
We don't yet recognise this construct:
case foo
when "foo", "bar"
foo
end
This is due to a limitation in the shared barrier guard logic.
| foo | ||
| end | ||
|
|
||
| if foo == "foo" or foo == "bar" # not recognised |
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 think this should be fixed by #11153
|
@hmac I trivially fixed all merge conflicts in the test cases through the GitHub UI. You probably need to refresh the expected output, though. |
Recognise if statements with conditionals made up or logical `and` or `or` clauses as barrier guards.
The `when` node now acts as a join point for patterns in the when
clause, with match/no-match completions. This is similar to how `or`
expressions work.
The result of this is that the `when` clause "controls" the body of the
`when`, which allows us to model barrier guards for multi-pattern when
clauses.
For this code
case x
when 1, 2
y
end
The old CFG was
x --> when --> 1 --no-match--> 2 ---no-match---> case
\ \ ^
\ \ |
\ --match----+ |
\ | |
\ | |
------match---------> y --+
The new CFG is
x --> 1 --no-match--> 2 --no-match--> [no-match] when --no-match--> case
\ \ ^
\ \ |
\ --match--> [match] when --match--> y -----+
\ /
\ /
-------match-----
i.e. all patterns flow to the `when` node, which is split based on
whether the pattern matched or not. The body of the when clause then has
a single predecessor `[match] when`, which acts as condition block that
controls `y`.
The `or` case ran extremely slowly before this change. Also exclude string interpolations from consideration, for correctness, and add some more tests.
a2fe67c to
f7d17d6
Compare
f7d17d6 to
2b4217b
Compare
|
I've updated this with quite a lot more stuff:
To support multi-pattern when clauses I reworked the CFG for when clauses so that control flows from patterns to the This means we get one edge to the body instead of one per pattern, and makes it easy to define a barrier guard for when clauses. The downside is it is a bit weird, and if there's a nicer way to do it I would love to know. Another result of this is a strange failure in one of the dataflow tests (see CI). This seems to imply that dataflow through |
I have pushed some CFG fixes that resolve this. |
The existing barrier guard machinery recognises guards such as `if x and y`, so there's no need to explicitly model them.
|
@hvitved is there anything remaining to change on this PR or do you think it's ready to go? |
| } | ||
|
|
||
| /** A control-flow node that wraps a `SplatExpr` AST expression. */ | ||
| class SplatExprCfgNode extends ExprCfgNode { |
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.
May as well extend UnaryOperationCfgNode.
|
|
||
| private class WhenTree extends PreOrderTree, WhenClause { | ||
| private class WhenTree extends ControlFlowTree, WhenClause { | ||
| final override predicate propagatesAbnormal(AstNode child) { child = this.getAPattern() } |
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 think we should add getBody() here as well.
| or | ||
| last(this.getBody(), last, c) | ||
| last(this.getBody(), last, c) and | ||
| c instanceof NormalCompletion |
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.
Not needed.
| * ``` | ||
| */ | ||
| private predicate stringConstCompareOr( | ||
| CfgNodes::ExprNodes::BinaryOperationCfgNode guard, Ssa::Definition def, boolean branch |
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.
def is not used by the caller, so can be removed from the parameter list.
| exists(CfgNodes::ExprNodes::CaseExprCfgNode case | | ||
| case.getValue() = testedNode and | ||
| ( | ||
| exists(CfgNodes::ExprNodes::WhenClauseCfgNode branchNode | |
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 prefer guard = any(CfgNodes::ExprNodes::WhenClauseCfgNode branchNode |
| // when "foo", "bar" | ||
| pattern instanceof ExprNodes::StringLiteralCfgNode | ||
| or | ||
| exists(CfgNodes::ExprNodes::SplatExprCfgNode splat | splat = pattern | |
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.
Again, I prefer pattern = any(CfgNodes::ExprNodes::SplatExprCfgNode splat |
| c.(ConditionalCompletion).getValue() = false | ||
| or | ||
| last(this.getBody(), last, c) and | ||
| c instanceof NormalCompletion |
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.
Sorry; I was not clear: What I meant was that the c instanceof NormalCompletion should be removed; we still need last(this.getBody(), last, c) (I wonder why the CFG tests don't fail).
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 checked this and thought it was OK to remove because there's a clause in the succ predicate for case trees that I thought might cover it. On my phone so I can't link easily, but L431 of this file on main.
Happy to re-add this bit I deleted though. I'll do that tomorrow.
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.
Ah yes, it works because of the (somewhat non-compositional)
last(this.getABranch().(WhenClause).getBody(), pred, c)I still think we should add this back, though.
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'll also see if we can fix up that line to something like last(this.getABranch(), pred, c)
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.
Changing ^ caused some problems, so I've just left it as-is for now. The change in WhenTree is fixed though.
3e974fe to
2822c94
Compare
This recognises barriers of the form
where the reads of
fooinside eachwhenare guarded by the comparisonof
foowith the string literals.We don't yet recognise this construct:
This is due to a limitation in the shared barrier guard logic.