Skip to content
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

Skip unreachable catch clauses with constant false filter #19294

Merged
merged 3 commits into from
May 18, 2017

Conversation

elijah6
Copy link
Contributor

@elijah6 elijah6 commented May 5, 2017

Customer scenario

Write a catch clause with a filter that happens to be a constant whose value is false. This doesn't typically occur intentionally. In that case the compiler produces bad code that will not verify.

Bugs this fixes:

#18678

Workarounds, if any

Remove the "offending" catch block. The compiler produces a warning that can help guide the programmer to the issue.

Risk

Low. The fix is quite tiny and well-tested.

Performance impact

None expected.

Is this a regression from a previous update?

No.

Root cause analysis:

When the filter expression for a catch clause is false and the end of the try block is unreachable (e.g. because of throw in try block), then the implicit return was not generated, and the unreachable catch block was branching out (leave.s) to an invalid address. More details described in #18678

How was the bug found?

Customer-reported. Note that the bug fix is community-contributed.

@sharwell
Copy link
Member

sharwell commented May 9, 2017

@elijah6 I know a bunch of the best qualified reviewers for changes like this are busy preparing for Build. Even though it's not my area, I like what you're getting done here. I'm tracking this to try and get follow-up on it in a timely manner.

@sharwell
Copy link
Member

@jcouv @gafter Since we have a PR submitted with tests, can you take a quick look today to determine if this would be a candidate for 15.3?

@@ -880,7 +880,7 @@ static void Main()
// System.Console.Write("catch");
Diagnostic(ErrorCode.WRN_UnreachableCode, "System").WithLocation(12, 13)
);
//CompileAndVerify(comp); // PEVerify failed with Branch out of the method. Follow-up issue: https://github.com/dotnet/roslyn/issues/18678
CompileAndVerify(comp);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you verify the execution of the compiled program by adding expectedOutput: ""?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The program has unhandled exception, and does not have an output. So I added another test that captures a specific exception outside the function (PEVerifyWithUnreachableCatch2). Without the fix, this program will throw InvalidOperationException and output "OtherExceptions" instead.

@@ -739,8 +739,6 @@ public override void CloseScope(ILBuilder builder)

internal override void GetExceptionHandlerRegions(ArrayBuilder<Cci.ExceptionHandlerRegion> regions)
{
Debug.Assert(_handlers.Count > 1);

ExceptionHandlerScope tryScope = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your removing the assertion here implies that _handlers.Count can be zero. Is that the case?

{
return sequence.Value is BoundSequencePointExpression expression
&& expression.Expression?.ConstantValue?.BooleanValue == false;
} else if(catchBlock.ExceptionFilterOpt is BoundSequencePointExpression expression)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: formatting is incorrect in this method. The compiler code has spaces in conditionals if ( and braces on their own line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the review. I will check the overall formatting again and add some extra tests you suggested first

{
throw new Exception();
}
catch (Exception) when (false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test for some expression that is statically known to be false, but not literally false, like true == false.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added more cases in TryCatchConstantFalseFilter3()

{
if(catchBlock.ExceptionFilterOpt is BoundSequence sequence)
{
return sequence.Value is BoundSequencePointExpression expression
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use of expression in different scopes is confusing.
@gafter, I don't remember exactly the scoping rules for is-pattern variable declarations. Does this seem right?

private static bool IsFilterConstantFalse(BoundCatchBlock catchBlock)
{
// Depending on compilation options, BoundLiteral can be wrapped by BoundSequence or BoundSequencePointExpression
if(catchBlock.ExceptionFilterOpt != null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider flipping this condition around: if (... == null) { return false; } (with correct formatting)

{
if(catchBlock.ExceptionFilterOpt is BoundSequence sequence)
{
return sequence.Value is BoundSequencePointExpression expression
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, this feels strange and makes me think maybe this logic of removing a catch block might be better suited in earlier phases (lowering). @gafter what do you think?

Copy link
Member

@VSadov VSadov May 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not mix two purposes of removing unreachable catch blocks

  1. Fixing the bug by removal of formally unreachable catches (as per language spec and control flow rules).
    This is easier to do in lowering. - take a look at VisitCatchBlock in LocalRewriter_TryStatements.cs
    Checking that the filter is a constant false and returning null could be sufficient to fix the bug.

  2. Additional optimizations where the condition becomes false as a result of lowering and further optimizations. That would be harder to do and it would need to be done either in the codegen or in the optimizer pass. And only when /o+ flag is set.
    This should be a separate item. Costs/Benefits/Risk for this part would not be as favorable as for the #1 above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that makes more sense... thank you for the clarification. I will clean up the code and try that path

@jcouv jcouv added this to the 15.3 milestone May 12, 2017
@jcouv
Copy link
Member

jcouv commented May 12, 2017

Thanks @elijah6 for the PR.
The question of which release this should go in depends on how fast the PR is reviewed and approved, because the bar (risks vs benefits) goes up as we get closer to release. Worse case (if it doesn't meet the bar), we'll have to keep the PR open until the master branch becomes the dev15.6 branch (in a couple of weeks, I suspect).

@dotnet/roslyn-compiler for review. In particular, I'm not sure whether some of the logic would be more natural fit for lowering phase, instead of emit phase.

if(catchBlock.ExceptionFilterOpt is BoundSequence sequence)
{
return sequence.Value is BoundSequencePointExpression expression
&& expression.Expression?.ConstantValue?.BooleanValue == false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct. Sequence that returns a literal is not the same as a literal. Sequence may have observable sideeffects so one cannot be replaced with another.

@elijah6
Copy link
Contributor Author

elijah6 commented May 13, 2017

Thank you @VSadov and @jcouv for the review. Now the fix is in the lowering step and is a lot simpler. Most of the changes are additional tests.

@@ -78,6 +78,10 @@ public override BoundNode VisitCatchBlock(BoundCatchBlock node)
{
return base.VisitCatchBlock(node);
}
if (node.ExceptionFilterOpt.ConstantValue?.BooleanValue == false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a blank line here before the if statement.

@@ -864,7 +864,7 @@ static void Main()
{
throw new System.Exception();
}
catch (System.Exception) when (false)
catch (System.Exception) when (default)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we delete the false test case here? Sure default should compile to false in this case, but that's what testing should verify. My initial reaction here was to wonder why we didn't have a test for false and default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test class is TargetTypedDefaultTest so I am assuming it is going to contain 'default' related tests. Other cases will be covered by the other set of additional tests that I've written, right? Or should I repeat them here as well as a pair?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair point. Should probably leave as is .

@gafter gafter self-assigned this May 18, 2017
Copy link
Member

@gafter gafter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@gafter
Copy link
Member

gafter commented May 18, 2017

/CC @MeiChin-Tsai for ask mode approval. This is a tiny and well-tested fix for bad code generation that was submitted by a community member. Most of the change is testing.

@MeiChin-Tsai
Copy link

Approved.

@sharwell sharwell added the Community The pull request was submitted by a contributor who is not a Microsoft employee. label May 18, 2017
@gafter gafter merged commit 8bc010c into dotnet:master May 18, 2017
@gafter
Copy link
Member

gafter commented May 18, 2017

Thank you for your contribution!

@gafter gafter added the Resolution-Fixed The bug has been fixed and/or the requested behavior has been implemented label May 18, 2017
@elijah6 elijah6 deleted the skipUnreachableCatchClauses branch May 18, 2017 18:50
@elijah6
Copy link
Contributor Author

elijah6 commented May 18, 2017

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers cla-already-signed Community The pull request was submitted by a contributor who is not a Microsoft employee. Resolution-Fixed The bug has been fixed and/or the requested behavior has been implemented
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants